diff --git a/craft_parts/packages/errors.py b/craft_parts/packages/errors.py index 283f7f814..17c4e1b39 100644 --- a/craft_parts/packages/errors.py +++ b/craft_parts/packages/errors.py @@ -18,6 +18,8 @@ from collections.abc import Sequence +import humanize + from craft_parts.errors import PartsError from craft_parts.utils import formatting_utils @@ -57,7 +59,7 @@ class PackagesNotFound(PackagesError): def __init__(self, packages: Sequence[str]) -> None: self.packages = packages - missing_pkgs = formatting_utils.humanize_list(packages, "and") + missing_pkgs = humanize.natural_list([repr(pkg) for pkg in sorted(packages)]) brief = f"Failed to find installation candidate for packages: {missing_pkgs}." resolution = ( "Make sure the repository configuration and package names are correct." diff --git a/craft_parts/sources/errors.py b/craft_parts/sources/errors.py index 5c759fb0b..52d143466 100644 --- a/craft_parts/sources/errors.py +++ b/craft_parts/sources/errors.py @@ -18,6 +18,8 @@ from collections.abc import Sequence +import humanize + from craft_parts import errors from craft_parts.utils import formatting_utils @@ -74,7 +76,7 @@ class InvalidSourceOptions(SourceError): def __init__(self, *, source_type: str, options: list[str]) -> None: self.source_type = source_type self.options = options - humanized_options = formatting_utils.humanize_list(options, "and") + humanized_options = humanize.natural_list([repr(opt) for opt in sorted(options)]) brief = ( f"Failed to pull source: {humanized_options} cannot be used " f"with a {source_type} source." @@ -94,7 +96,7 @@ class IncompatibleSourceOptions(SourceError): def __init__(self, source_type: str, options: list[str]) -> None: self.source_type = source_type self.options = options - humanized_options = formatting_utils.humanize_list(options, "and") + humanized_options = humanize.natural_list([repr(opt) for opt in sorted(options)]) brief = ( f"Failed to pull source: cannot specify both {humanized_options} " f"for a {source_type} source." diff --git a/craft_parts/utils/formatting_utils.py b/craft_parts/utils/formatting_utils.py deleted file mode 100644 index 7df800875..000000000 --- a/craft_parts/utils/formatting_utils.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2016-2021 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 . - -"""Text formatting utilities.""" - -from collections.abc import Iterable - - -def humanize_list( - items: Iterable[str], conjunction: str, item_format: str = "{!r}" -) -> str: - """Format a list into a human-readable string. - - :param items: List to humanize. - :param conjunction: The conjunction used to join the final element to - the rest of the list (e.g. 'and'). - :param item_format: Format string to use per item. - """ - if not items: - return "" - - quoted_items = [item_format.format(item) for item in sorted(items)] - if len(quoted_items) == 1: - return quoted_items[0] - - humanized = ", ".join(quoted_items[:-1]) - - if len(quoted_items) > 2: # noqa: PLR2004 - humanized += "," - - return f"{humanized} {conjunction} {quoted_items[-1]}" diff --git a/pyproject.toml b/pyproject.toml index 26f75cbfa..1a7b36868 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] dependencies = [ + "humanize>=4.11.0", "overrides!=7.6.0", "pydantic>=2.0.0", "pyxdg", diff --git a/tests/unit/sources/test_rpm_source.py b/tests/unit/sources/test_rpm_source.py index 531024db7..4274087f6 100644 --- a/tests/unit/sources/test_rpm_source.py +++ b/tests/unit/sources/test_rpm_source.py @@ -97,7 +97,7 @@ def test_valid_options(partitions): "branch", "submodule", "depth", - "'source-branch', 'source-commit', 'source-depth', 'source-submodules', and 'source-tag'", + "'source-branch', 'source-commit', 'source-depth', 'source-submodules' and 'source-tag'", id="all-values-bad", ), ], diff --git a/tests/unit/utils/test_formatting_utils.py b/tests/unit/utils/test_formatting_utils.py deleted file mode 100644 index 00400ee54..000000000 --- a/tests/unit/utils/test_formatting_utils.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright 2021 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 . - -import pytest -from craft_parts.utils import formatting_utils - - -@pytest.mark.parametrize( - ("items", "result"), - [ - (None, ""), - ([], ""), - (["foo"], "'foo'"), - (["foo", "bar"], "'bar' & 'foo'"), - ([3, 2, 1], "1, 2, & 3"), - ], -) -def test_humanize_list(items, result): - hl = formatting_utils.humanize_list(items, "&") - assert hl == result - - -@pytest.mark.parametrize( - ("items", "item_format", "result"), - [ - (["foo"], "", ""), - (["foo"], "{!r}", "'foo'"), - ([1], "{!r}", "1"), - ([42], "{:2x}", "2a"), - ], -) -def test_humanize_list_item_format(items, item_format, result): - hl = formatting_utils.humanize_list(iter(items), "&", item_format) - assert hl == result diff --git a/uv.lock b/uv.lock index f89295f95..d93b15fba 100644 --- a/uv.lock +++ b/uv.lock @@ -334,9 +334,9 @@ toml = [ [[package]] name = "craft-parts" -version = "2.2.1.post21+gf142785.d20250114" source = { editable = "." } dependencies = [ + { name = "humanize" }, { name = "overrides" }, { name = "pydantic" }, { name = "pyxdg" }, @@ -435,6 +435,7 @@ requires-dist = [ { name = "canonical-sphinx", marker = "extra == 'docs'" }, { name = "codespell", marker = "extra == 'dev'" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = "~=7.4" }, + { name = "humanize", specifier = ">=4.11.0" }, { name = "hypothesis", marker = "extra == 'dev'" }, { name = "jsonschema", marker = "extra == 'dev'" }, { name = "mypy", extras = ["reports"], marker = "extra == 'dev'", specifier = "~=1.14.1" }, @@ -556,6 +557,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/48/e791a7ed487dbb9729ef32bb5d1af16693d8925f4366befef54119b2e576/furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", size = 341333 }, ] +[[package]] +name = "humanize" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/40/64a912b9330786df25e58127194d4a5a7441f818b400b155e748a270f924/humanize-4.11.0.tar.gz", hash = "sha256:e66f36020a2d5a974c504bd2555cf770621dbdbb6d82f94a6857c0b1ea2608be", size = 80374 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/75/4bc3e242ad13f2e6c12e0b0401ab2c5e5c6f0d7da37ec69bc808e24e0ccb/humanize-4.11.0-py3-none-any.whl", hash = "sha256:b53caaec8532bcb2fff70c8826f904c35943f8cecaca29d272d9df38092736c0", size = 128055 }, +] + [[package]] name = "hypothesis" version = "6.123.7"