Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

partitions: add utility to make partition compatible filepaths #497

Merged
merged 8 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions craft_parts/utils/partition_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Partition helpers."""

import re
from pathlib import PurePath, PurePosixPath
from typing import TypeVar, Union, cast

from craft_parts.features import Features

FlexiblePath = TypeVar("FlexiblePath", bound=Union[PurePath, str])


HAS_PARTITION_REGEX = re.compile(r"^\([a-z]+\)(/.*)?$")


def _has_partition(path: FlexiblePath) -> bool:
"""Check whether a path has an explicit partition."""
return bool(HAS_PARTITION_REGEX.match(str(path)))


def get_partition_compatible_filepath(filepath: FlexiblePath) -> FlexiblePath:
"""Get a filepath compatible with the partitions feature.

If the filepath begins with a partition, then the parentheses are stripped from the
the partition. For example, `(default)/file` is converted to `default/file`.

If the filepath does not begin with a partition, the `default` partition is
prepended. For example, `file` is converted to `default/file`.

:param filepath: The filepath to modify.

:returns: A filepath that is compatible with the partitions feature.
"""
if not Features().enable_partitions:
return filepath

str_path = str(filepath)

if str_path == "*":
return filepath

if _has_partition(str_path):
if "/" not in str_path:
partition = str_path.strip("()")
inner_path = ""
else:
partition, inner_path = str_path.split("/", maxsplit=1)
partition = partition.strip("()")
else:
partition = "default"
inner_path = str_path

new_filepath = PurePosixPath(partition, inner_path)
return cast(FlexiblePath, filepath.__class__(new_filepath))
115 changes: 115 additions & 0 deletions tests/unit/utils/test_partition_utils.py
lengau marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2023 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Unit tests for partition utilities."""
from pathlib import Path, PurePosixPath

import pytest

from craft_parts.utils.partition_utils import (
_has_partition,
get_partition_compatible_filepath,
)

PATH_CLASSES = [Path, PurePosixPath, str]

NON_PARTITION_PATHS = [
"/absolute/path",
"relative/path",
"",
]

PARTITION_PATHS = [
"(default)",
"(default)/",
"(default)/path",
"(partition)/path",
]

PARTITION_EXPECTED_PATHS = [
"default",
"default",
"default/path",
"partition/path",
]

# Prevent us from adding nonmatching paths for tests below.
assert len(PARTITION_PATHS) == len(
PARTITION_EXPECTED_PATHS
), "Expected partition paths and input partition paths need to match 1:1"


@pytest.mark.parametrize(
("full_path", "expected"),
[
("some/path", False),
("(default)", True),
("(default)/", True),
("(part)/some/path", True),
("(nota)partition", False),
("(not/a)partition", False),
("(not/a)/partition", False),
("(NOTA)/partition", False),
("(not1)/partition", False),
],
)
def test_has_partition(full_path, expected):
"""Test that the partition regex has the expected results."""
assert _has_partition(full_path) == expected


@pytest.mark.parametrize("path", NON_PARTITION_PATHS + PARTITION_PATHS)
@pytest.mark.parametrize("path_class", PATH_CLASSES)
def test_get_partition_compatible_filepath_disabled_passthrough(path, path_class):
"""Test that when partitions are disabled this is a no-op."""
actual = get_partition_compatible_filepath(path_class(path))

assert actual == path_class(path)
assert isinstance(actual, path_class)


@pytest.mark.parametrize("path", ["*"])
@pytest.mark.parametrize("path_class", PATH_CLASSES)
@pytest.mark.usefixtures("enable_partitions_feature")
def test_get_partition_compatible_filepath_glob(path, path_class):
expected = path_class(path)
actual = get_partition_compatible_filepath(expected)

assert actual == expected


@pytest.mark.parametrize("path", NON_PARTITION_PATHS)
@pytest.mark.parametrize("path_class", PATH_CLASSES)
@pytest.mark.usefixtures("enable_partitions_feature")
def test_get_partition_compatible_filepath_non_partition(path, path_class):
"""Non-partitioned paths get a default partition."""
actual = get_partition_compatible_filepath(path_class(path))

assert actual == path_class(PurePosixPath("default", path))
assert isinstance(actual, path_class)


@pytest.mark.parametrize(
("path", "expected"),
zip(PARTITION_PATHS, PARTITION_EXPECTED_PATHS),
)
@pytest.mark.parametrize("path_class", PATH_CLASSES)
@pytest.mark.usefixtures("enable_partitions_feature")
def test_get_partition_compatible_filepath_partition(path, expected, path_class):
"""Non-partitioned paths match their given partition."""
actual = get_partition_compatible_filepath(path_class(path))

assert actual == path_class(expected)
assert isinstance(actual, path_class)