From 6b8f9a65bb11daeb80e21f3e4a154361f3544930 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 8 Oct 2024 16:09:43 -0400 Subject: [PATCH 1/6] build(deps): update dependencies --- pyproject.toml | 5 ----- requirements-dev.txt | 10 +++++----- requirements.txt | 10 +++++----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6de2cd850..d89cd1eb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,13 +22,8 @@ dependencies = [ "pyyaml", "requests", "requests-toolbelt", - "requests-unixsocket", "snap-helpers", "tabulate", - # Needed until requests-unixsocket supports urllib3 v2 - # https://github.com/msabramo/requests-unixsocket/pull/69 - # When updating, remove the urllib3 constraint from renovate config. - "urllib3<2.0", ] classifiers = [ "Development Status :: 5 - Production/Stable", diff --git a/requirements-dev.txt b/requirements-dev.txt index d06c6ec8f..89cd3928d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,11 +6,11 @@ certifi==2024.8.30 cffi==1.17.1 charset-normalizer==3.3.2 coverage==7.6.1 -craft-application==4.2.5 +craft-application==4.2.6 craft-archives==2.0.0 craft-cli==2.7.0 craft-grammar==2.0.1 -craft-parts==2.1.1 +craft-parts==2.1.2 craft-platforms==0.3.1 craft-providers==2.0.4 craft-store==3.0.2 @@ -71,9 +71,9 @@ pytz==2024.1 pyxdg==0.28 pyyaml==6.0.2 referencing==0.35.1 -requests==2.31.0 +requests==2.32.3 requests-toolbelt==1.0.0 -requests-unixsocket==0.3.0 +requests-unixsocket==0.4.2 responses==0.25.3 rpds-py==0.20.0 ruamel-yaml==0.18.6 @@ -87,6 +87,6 @@ sortedcontainers==2.4.0 tabulate==0.9.0 tomlkit==0.13.2 typing-extensions==4.12.2 -urllib3==1.26.19 +urllib3==2.2.3 wadllib==1.3.9 zipp==3.20.2 diff --git a/requirements.txt b/requirements.txt index 61b3b162b..4823bfac2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,11 +4,11 @@ boolean-py==4.0 certifi==2024.8.30 cffi==1.17.1 charset-normalizer==3.3.2 -craft-application==4.2.5 +craft-application==4.2.6 craft-archives==2.0.0 craft-cli==2.7.0 craft-grammar==2.0.1 -craft-parts==2.1.1 +craft-parts==2.1.2 craft-platforms==0.3.1 craft-providers==2.0.4 craft-store==3.0.2 @@ -51,9 +51,9 @@ pytz==2024.1 pyxdg==0.28 pyyaml==6.0.2 referencing==0.35.1 -requests==2.31.0 +requests==2.32.3 requests-toolbelt==1.0.0 -requests-unixsocket==0.3.0 +requests-unixsocket2==0.4.2 rpds-py==0.20.0 ruamel-yaml==0.18.6 ruamel-yaml-clib==0.2.8 @@ -63,6 +63,6 @@ six==1.16.0 snap-helpers==0.4.2 tabulate==0.9.0 typing-extensions==4.12.2 -urllib3==1.26.19 +urllib3==2.2.3 wadllib==1.3.9 zipp==3.20.2 From 2399ddf8ecc5eb012e077fe50826de5339993e06 Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Tue, 8 Oct 2024 16:49:30 -0400 Subject: [PATCH 2/6] tests(spread): test snap parallel installs --- requirements-dev.txt | 2 +- .../parallel-install/charmcraft.yaml | 14 +++++++++ .../smoketests/parallel-install/task.yaml | 31 +++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/spread/smoketests/parallel-install/charmcraft.yaml create mode 100644 tests/spread/smoketests/parallel-install/task.yaml diff --git a/requirements-dev.txt b/requirements-dev.txt index 89cd3928d..3a34fd65a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -73,7 +73,7 @@ pyyaml==6.0.2 referencing==0.35.1 requests==2.32.3 requests-toolbelt==1.0.0 -requests-unixsocket==0.4.2 +requests-unixsocket2==0.4.2 responses==0.25.3 rpds-py==0.20.0 ruamel-yaml==0.18.6 diff --git a/tests/spread/smoketests/parallel-install/charmcraft.yaml b/tests/spread/smoketests/parallel-install/charmcraft.yaml new file mode 100644 index 000000000..e46febc45 --- /dev/null +++ b/tests/spread/smoketests/parallel-install/charmcraft.yaml @@ -0,0 +1,14 @@ +type: charm +name: test-charm +summary: test-charm +description: test-charm + +base: ubuntu@24.04 +platforms: + amd64: + arm64: + riscv64: + +parts: + my-part: + plugin: nil diff --git a/tests/spread/smoketests/parallel-install/task.yaml b/tests/spread/smoketests/parallel-install/task.yaml new file mode 100644 index 000000000..2ab50ff66 --- /dev/null +++ b/tests/spread/smoketests/parallel-install/task.yaml @@ -0,0 +1,31 @@ +summary: pack a charm with parallel-installed charmcraft versions + +# Run last since we change snapd settings +priority: -10 + +prepare: | + snap install yq + if [[ $(snap get system experimental.parallel-instances) != true ]]; then + snap set system experimental.parallel-instances=true + REBOOT + fi + snap install --classic --channel=latest/candidate charmcraft + snap install --dangerous --classic --name=charmcraft_dev /charmcraft/charmcraft_*.snap + +restore: | + if [[ $(snap get system experimental.parallel-instances) == true ]]; then + snap remove charmcraft_dev + snap set system experimental.parallel-instances=false + REBOOT + fi + snap install --classic --dangerous /charmcraft/charmcraft_*.snap + +execute: | + # Check that the candidate version used the correct version + charmcraft pack + [[ $(unzip -p *.charm manifest.yaml | yq .charmcraft-version) == $(charmcraft --version | cut -f2 -d' ') ]] + rm *.charm + + # Try the dev version + charmcraft_dev pack + [[ $(unzip -p *.charm manifest.yaml | yq .charmcraft-version) == $(charmcraft_dev --version | cut -f2 -d' ') ]] From d2b40dfbece3e9cc8577c19407c2dedbc30e9d2f Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Fri, 11 Oct 2024 18:10:08 -0400 Subject: [PATCH 3/6] style(lint): switch from black to ruff --- pyproject.toml | 1 - tox.ini | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d89cd1eb4..d46857efd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,6 @@ dev = [ # When updating these, also update the dev/lint/types groups in renovat "responses", ] lint = [ - "black~=24.0", "codespell[tomli]", "yamllint", ] diff --git a/tox.ini b/tox.ini index 483817d33..ca84fba42 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,9 @@ # 1. Docs not included # 2. Charmcraft currently doesn't distinguish unit from integration tests env_list = # Environments to run when called with no parameters. - format-{black,ruff,codespell} + format-{ruff,codespell} pre-commit - lint-{black,ruff,mypy,pyright,shellcheck,codespell,yaml} + lint-{ruff,mypy,pyright,shellcheck,codespell,yaml} test-py3.10 # By default, only run tests on core22's Python 3.10 minversion = 4.6 @@ -65,7 +65,7 @@ runner = ignore_env_name_mismatch find = git ls-files filter = file --mime-type -Nnf- | grep shellscript | cut -f1 -d: -[testenv:lint-{black,ruff,shellcheck,codespell,yaml}] +[testenv:lint-{ruff,shellcheck,codespell,yaml}] description = Lint the source code base = testenv, lint labels = lint @@ -75,8 +75,8 @@ allowlist_externals = commands_pre = shellcheck: bash -c '{[shellcheck]find} | {[shellcheck]filter} > {env_tmp_dir}/shellcheck_files' commands = - black: black --check --diff {tty:--color} {posargs} . ruff: ruff check --respect-gitignore {posargs:.} + ruff: ruff format --diff {posargs:.} shellcheck: xargs -ra {env_tmp_dir}/shellcheck_files shellcheck codespell: codespell --toml {tox_root}/pyproject.toml {posargs} yaml: yamllint {posargs} . @@ -95,15 +95,15 @@ commands = pyright: pyright {posargs} mypy: mypy --install-types --non-interactive {posargs} -[testenv:format-{black,ruff,codespell}] +[testenv:format-{ruff,codespell}] description = Automatically format source code base = testenv, lint labels = format allowlist_externals = ruff: ruff commands = - black: black {tty:--color} {posargs} . ruff: ruff check --fix --respect-gitignore {posargs:.} + ruff: ruff format {posargs:.} codespell: codespell --toml {tox_root}/pyproject.toml --write-changes {posargs} [testenv:pre-commit] From 5714179477189c698fb20400c3aed8c2c6083baa Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Fri, 11 Oct 2024 18:10:51 -0400 Subject: [PATCH 4/6] style(lint): format with ruff This is just the result of `tox -f format` --- charmcraft/application/commands/__init__.py | 4 +- charmcraft/application/commands/analyse.py | 1 + charmcraft/application/commands/base.py | 1 + charmcraft/application/commands/extensions.py | 1 + charmcraft/application/commands/init.py | 5 +- charmcraft/application/commands/lifecycle.py | 11 +- charmcraft/application/commands/remote.py | 21 +- charmcraft/application/commands/store.py | 237 +++++++++++++----- charmcraft/application/commands/test.py | 1 + charmcraft/application/commands/version.py | 1 + charmcraft/application/main.py | 14 +- charmcraft/charm_builder.py | 32 ++- charmcraft/const.py | 1 + charmcraft/dispatch.py | 4 +- charmcraft/env.py | 17 +- charmcraft/errors.py | 10 +- charmcraft/extensions/_utils.py | 4 +- charmcraft/extensions/app.py | 10 +- charmcraft/extensions/extension.py | 8 +- charmcraft/extensions/registry.py | 1 + charmcraft/jujuignore.py | 4 +- charmcraft/linters.py | 41 ++- charmcraft/models/basic.py | 1 + charmcraft/models/charmcraft.py | 9 +- charmcraft/models/config.py | 10 +- charmcraft/models/lint.py | 1 + charmcraft/models/manifest.py | 1 + charmcraft/models/project.py | 27 +- charmcraft/parts/__init__.py | 4 +- charmcraft/parts/lifecycle.py | 13 +- charmcraft/parts/plugins/_bundle.py | 1 + charmcraft/parts/plugins/_charm.py | 38 ++- charmcraft/parts/plugins/_poetry.py | 1 - charmcraft/parts/plugins/_python.py | 6 +- charmcraft/parts/plugins/_reactive.py | 10 +- charmcraft/preprocess.py | 1 + charmcraft/services/__init__.py | 1 + charmcraft/services/analysis.py | 25 +- charmcraft/services/charmlibs.py | 5 +- charmcraft/services/image.py | 4 +- charmcraft/services/lifecycle.py | 5 +- charmcraft/services/package.py | 38 ++- charmcraft/services/provider.py | 4 +- charmcraft/services/remotebuild.py | 1 + charmcraft/services/store.py | 20 +- charmcraft/store/client.py | 55 +++- charmcraft/store/store.py | 56 +++-- charmcraft/utils/__init__.py | 13 +- charmcraft/utils/charmlibs.py | 17 +- charmcraft/utils/cli.py | 9 +- charmcraft/utils/file.py | 1 + charmcraft/utils/package.py | 11 +- charmcraft/utils/parts.py | 12 +- charmcraft/utils/platform.py | 9 +- charmcraft/utils/project.py | 9 +- charmcraft/utils/skopeo.py | 11 +- charmcraft/utils/store.py | 11 +- charmcraft/utils/yaml.py | 4 +- tests/commands/test_store_api.py | 102 ++++++-- tests/commands/test_store_client.py | 65 +++-- tests/conftest.py | 20 +- tests/extensions/test_app.py | 4 +- tests/extensions/test_extensions.py | 24 +- tests/extensions/test_registry.py | 6 +- tests/integration/commands/test_analyse.py | 28 ++- tests/integration/commands/test_extensions.py | 4 +- tests/integration/commands/test_init.py | 17 +- tests/integration/commands/test_pack.py | 27 +- .../commands/test_resource_revisions.py | 32 ++- .../test_set_resource_architectures.py | 13 +- .../commands/test_store_commands.py | 49 +++- tests/integration/conftest.py | 9 +- tests/integration/parts/conftest.py | 4 +- .../integration/parts/plugins/test_poetry.py | 16 +- .../integration/parts/plugins/test_python.py | 17 +- tests/integration/services/test_image.py | 7 +- tests/integration/services/test_lifecycle.py | 1 - tests/integration/services/test_package.py | 25 +- tests/integration/services/test_provider.py | 11 +- tests/integration/services/test_store.py | 1 - tests/integration/test_charm_builder.py | 1 - tests/integration/utils/test_skopeo.py | 9 +- tests/test_charm_builder.py | 103 ++++++-- tests/test_infra.py | 4 +- tests/test_instrum.py | 13 +- tests/test_linters.py | 76 ++++-- tests/test_parts.py | 4 +- tests/test_snap.py | 8 +- tests/unit/commands/test_lifecycle.py | 17 +- tests/unit/commands/test_store.py | 30 ++- tests/unit/models/test_charmcraft.py | 16 +- tests/unit/models/test_config.py | 10 +- tests/unit/models/test_metadata.py | 9 +- tests/unit/models/test_project.py | 73 ++++-- tests/unit/parts/plugins/test_charm.py | 21 +- tests/unit/parts/plugins/test_poetry.py | 36 ++- tests/unit/parts/plugins/test_python.py | 28 ++- tests/unit/parts/plugins/test_reactive.py | 8 +- tests/unit/parts/test_lifecycle.py | 4 +- tests/unit/services/test_analysis.py | 42 +++- tests/unit/services/test_charmlibs.py | 24 +- tests/unit/services/test_image.py | 37 ++- tests/unit/services/test_lifecycle.py | 4 +- tests/unit/services/test_package.py | 46 +++- tests/unit/services/test_provider.py | 8 +- tests/unit/services/test_store.py | 77 ++++-- tests/unit/store/test_client.py | 16 +- tests/unit/test_application.py | 21 +- tests/unit/test_charm_builder.py | 5 +- tests/unit/test_dispatch.py | 9 +- tests/unit/test_parts.py | 23 +- tests/unit/test_preprocess.py | 27 +- tests/unit/utils/test_charmlibs.py | 29 ++- tests/unit/utils/test_cli.py | 5 +- tests/unit/utils/test_file.py | 1 + tests/unit/utils/test_package.py | 45 +++- tests/unit/utils/test_platform.py | 12 +- tests/unit/utils/test_project.py | 28 ++- tests/unit/utils/test_skopeo.py | 21 +- tests/unit/utils/test_store.py | 6 +- 120 files changed, 1766 insertions(+), 545 deletions(-) diff --git a/charmcraft/application/commands/__init__.py b/charmcraft/application/commands/__init__.py index 1ceff40b9..4364050af 100644 --- a/charmcraft/application/commands/__init__.py +++ b/charmcraft/application/commands/__init__.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Charmcraft commands.""" + import craft_application from charmcraft.application.commands.analyse import Analyse, Analyze @@ -109,7 +110,8 @@ def fill_command_groups(app: craft_application.Application) -> None: ], ) app.add_command_group( - "Extensions", [ExpandExtensionsCommand, ExtensionsCommand, ListExtensionsCommand] + "Extensions", + [ExpandExtensionsCommand, ExtensionsCommand, ListExtensionsCommand], ) app.add_command_group( "Other", diff --git a/charmcraft/application/commands/analyse.py b/charmcraft/application/commands/analyse.py index 8b160efe8..fefa706b7 100644 --- a/charmcraft/application/commands/analyse.py +++ b/charmcraft/application/commands/analyse.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Command for analysing a charm.""" + import argparse import json import pathlib diff --git a/charmcraft/application/commands/base.py b/charmcraft/application/commands/base.py index cb88c4fed..72fe8ae46 100644 --- a/charmcraft/application/commands/base.py +++ b/charmcraft/application/commands/base.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Base command for Charmcraft commands.""" + from __future__ import annotations import craft_application.commands diff --git a/charmcraft/application/commands/extensions.py b/charmcraft/application/commands/extensions.py index bb80e95df..5ec436565 100644 --- a/charmcraft/application/commands/extensions.py +++ b/charmcraft/application/commands/extensions.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Infrastructure for the 'extensions' command.""" + import argparse from textwrap import dedent diff --git a/charmcraft/application/commands/init.py b/charmcraft/application/commands/init.py index a0dcfe506..c6c39650c 100644 --- a/charmcraft/application/commands/init.py +++ b/charmcraft/application/commands/init.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Infrastructure for the 'init' command.""" + import argparse import os import pathlib @@ -130,7 +131,9 @@ class InitCommand(base.CharmcraftCommand): def fill_parser(self, parser): """Specify command's specific parameters.""" - parser.add_argument("--name", help="The name of the charm; defaults to the directory name") + parser.add_argument( + "--name", help="The name of the charm; defaults to the directory name" + ) parser.add_argument( "--author", help="The charm author; defaults to the current user name per GECOS", diff --git a/charmcraft/application/commands/lifecycle.py b/charmcraft/application/commands/lifecycle.py index f0bce4738..9e322bf81 100644 --- a/charmcraft/application/commands/lifecycle.py +++ b/charmcraft/application/commands/lifecycle.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """craft-application based lifecycle commands.""" + from __future__ import annotations import pathlib @@ -171,14 +172,20 @@ def run_managed(self, parsed_args: argparse.Namespace) -> bool: # Always use a runner on non-Linux platforms. # Craft-parts is not designed to work on non-posix platforms, and most # notably here, the bundle plugin doesn't work on Windows. - if sys.platform == "linux" and charmcraft_yaml and charmcraft_yaml.get("type") == "bundle": + if ( + sys.platform == "linux" + and charmcraft_yaml + and charmcraft_yaml.get("type") == "bundle" + ): return False return super().run_managed(parsed_args) def _update_charm_libs(self) -> None: """Update charm libs attached to the project.""" - craft_cli.emit.progress("Checking that charmlibs match 'charmcraft.yaml' values") + craft_cli.emit.progress( + "Checking that charmlibs match 'charmcraft.yaml' values" + ) project = cast(models.CharmcraftProject, self._services.project) libs_svc = cast(services.CharmLibsService, self._services.charm_libs) installable_libs: list[models.CharmLib] = [] diff --git a/charmcraft/application/commands/remote.py b/charmcraft/application/commands/remote.py index 5ed3318be..ac606dec2 100644 --- a/charmcraft/application/commands/remote.py +++ b/charmcraft/application/commands/remote.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Build a charm remotely on Launchpad.""" + import argparse import os import pathlib @@ -63,7 +64,9 @@ class RemoteBuild(ExtensibleCommand): @override def _fill_parser(self, parser: argparse.ArgumentParser) -> None: - parser.add_argument("--recover", action="store_true", help="recover an interrupted build") + parser.add_argument( + "--recover", action="store_true", help="recover an interrupted build" + ) parser.add_argument( "--launchpad-accept-public-upload", action="store_true", @@ -121,7 +124,9 @@ def _run(self, parsed_args: argparse.Namespace, **kwargs: Any) -> int | None: # emit.progress(f"Recovering build {build_id}") builds = builder.resume_builds(build_id) else: - emit.progress("Starting new build. It may take a while to upload large projects.") + emit.progress( + "Starting new build. It may take a while to upload large projects." + ) builds = builder.start_builds(project_dir) try: @@ -138,7 +143,9 @@ def _run(self, parsed_args: argparse.Namespace, **kwargs: Any) -> int | None: # builder.cleanup() return returncode - def _monitor_and_complete(self, build_id: str | None, builds: Collection[Build]) -> int: + def _monitor_and_complete( + self, build_id: str | None, builds: Collection[Build] + ) -> int: builder = self._services.remote_build emit.progress("Monitoring build") try: @@ -168,10 +175,14 @@ def _monitor_and_complete(self, build_id: str | None, builds: Collection[Build]) emit.progress("; ".join(progress_parts)) except TimeoutError: if build_id: - resume_command = f"{self._app.name} remote-build --recover --build-id={build_id}" + resume_command = ( + f"{self._app.name} remote-build --recover --build-id={build_id}" + ) else: resume_command = f"{self._app.name} remote-build --recover" - emit.message(f"Timed out waiting for build.\nTo resume, run {resume_command!r}") + emit.message( + f"Timed out waiting for build.\nTo resume, run {resume_command!r}" + ) return 75 # Temporary failure emit.progress(f"Fetching {len(builds)} build logs...") diff --git a/charmcraft/application/commands/store.py b/charmcraft/application/commands/store.py index f9c7b53c4..628ae0c49 100644 --- a/charmcraft/application/commands/store.py +++ b/charmcraft/application/commands/store.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Commands related to Charmhub.""" + import argparse import collections import dataclasses @@ -69,7 +70,9 @@ class _ResourceType(typing.NamedTuple): EntityType = _EntityType() ResourceType = _ResourceType() # the list of valid attenuations to restrict login credentials -VALID_ATTENUATIONS = {getattr(attenuations, x) for x in dir(attenuations) if x.isupper()} +VALID_ATTENUATIONS = { + getattr(attenuations, x) for x in dir(attenuations) if x.isupper() +} BUNDLE_REGISTRATION_REMOVAL_URL = "https://discourse.charmhub.io/t/15344" @@ -162,7 +165,9 @@ def run(self, parsed_args): """Run the command.""" # validate that restrictions are only used if credentials are exported restrictive_options = ["charm", "bundle", "channel", "permission", "ttl"] - if any(getattr(parsed_args, option) is not None for option in restrictive_options): + if any( + getattr(parsed_args, option) is not None for option in restrictive_options + ): if parsed_args.export is None: raise ArgumentParsingError( "The restrictive options 'bundle', 'channel', 'charm', 'permission' or 'ttl' " @@ -176,7 +181,9 @@ def run(self, parsed_args): "Explore the documentation to learn about valid permissions: " "https://juju.is/docs/sdk/remote-env-auth" ) - raise CraftError(f"Invalid permission: {invalid_text}.", details=details) + raise CraftError( + f"Invalid permission: {invalid_text}.", details=details + ) # restrictive options, mapping the names between what is used in Namespace (singular, # even if it ends up being a list) and the more natural ones used in the Store layer @@ -191,14 +198,20 @@ def run(self, parsed_args): kwargs[arg_name] = namespace_value packages = ( - utils.get_packages(charms=parsed_args.charm or [], bundles=parsed_args.bundle or []) + utils.get_packages( + charms=parsed_args.charm or [], bundles=parsed_args.bundle or [] + ) or None ) if parsed_args.export: - credentials = self._services.store.get_credentials(packages=packages, **kwargs) + credentials = self._services.store.get_credentials( + packages=packages, **kwargs + ) parsed_args.export.write_text(credentials) - emit.message(f"Login successful. Credentials exported to {str(parsed_args.export)!r}.") + emit.message( + f"Login successful. Credentials exported to {str(parsed_args.export)!r}." + ) else: self._services.store.login(packages=packages, **kwargs) username = self._services.store.get_account_info()["username"] @@ -341,7 +354,9 @@ def run(self, parsed_args): """Run the command.""" store = Store(env.get_store_config()) store.register_name(parsed_args.name, EntityType.charm) - emit.message(f"You are now the publisher of charm {parsed_args.name!r} in Charmhub.") + emit.message( + f"You are now the publisher of charm {parsed_args.name!r} in Charmhub." + ) class RegisterBundleNameCommand(CharmcraftCommand): @@ -395,7 +410,9 @@ def run(self, parsed_args: argparse.Namespace) -> int: ) store = Store(env.get_store_config()) store.register_name(parsed_args.name, EntityType.bundle) - emit.message(f"You are now the publisher of bundle {parsed_args.name!r} in Charmhub.") + emit.message( + f"You are now the publisher of bundle {parsed_args.name!r} in Charmhub." + ) # TODO(#1810): Replace this with os.EX_OK return 0 @@ -605,7 +622,9 @@ def run(self, parsed_args): if not result.ok: if parsed_args.format: - errors = [{"code": err.code, "message": err.message} for err in result.errors] + errors = [ + {"code": err.code, "message": err.message} for err in result.errors + ] info = {"errors": errors} emit.message(cli.format_content(info, parsed_args.format)) else: @@ -616,7 +635,9 @@ def run(self, parsed_args): if parsed_args.release: # also release! - store.release(name, result.revision, parsed_args.release, parsed_args.resource) + store.release( + name, result.revision, parsed_args.release, parsed_args.resource + ) if parsed_args.format: info = {"revision": result.revision} @@ -629,7 +650,9 @@ def run(self, parsed_args): if parsed_args.resource: msg += " (attaching resources: {})" args.append( - ", ".join(f"{r.name!r} r{r.revision}" for r in parsed_args.resource) + ", ".join( + f"{r.name!r} r{r.revision}" for r in parsed_args.resource + ) ) emit.message(msg.format(*args)) return 0 @@ -695,7 +718,9 @@ def run(self, parsed_args): "status": item.status, } if item.errors: - prog_info["errors"] = [{"message": e.message, "code": e.code} for e in item.errors] + prog_info["errors"] = [ + {"message": e.message, "code": e.code} for e in item.errors + ] prog_data.append(prog_info) if parsed_args.format: @@ -799,7 +824,9 @@ def run(self, parsed_args): args = [parsed_args.revision, parsed_args.name, ", ".join(parsed_args.channel)] if parsed_args.resource: msg += " (attaching resources: {})" - args.append(", ".join(f"{r.name!r} r{r.revision}" for r in parsed_args.resource)) + args.append( + ", ".join(f"{r.name!r} r{r.revision}" for r in parsed_args.resource) + ) emit.message(msg.format(*args)) @@ -850,8 +877,12 @@ def run(self, parsed_args: "Namespace") -> None: raise CraftError("promote-bundle must be run on a bundle.") # Check snapcraft for equiv logic - from_channel = charmcraft.store.models.ChannelData.from_str(parsed_args.from_channel) - to_channel = charmcraft.store.models.ChannelData.from_str(parsed_args.to_channel) + from_channel = charmcraft.store.models.ChannelData.from_str( + parsed_args.from_channel + ) + to_channel = charmcraft.store.models.ChannelData.from_str( + parsed_args.to_channel + ) if to_channel == from_channel: raise CraftError("Cannot promote from a channel to the same channel.") @@ -886,7 +917,9 @@ def run(self, parsed_args: "Namespace") -> None: emit.debug(f"Creating bundle file in {str(output_bundle)}") output_bundle /= "bundle.yaml" else: - raise CraftError(f"Not a valid bundle output path: {str(output_bundle)}") + raise CraftError( + f"Not a valid bundle output path: {str(output_bundle)}" + ) elif output_bundle is not None: if not output_bundle.suffix: output_bundle /= "bundle.yaml" @@ -894,14 +927,18 @@ def run(self, parsed_args: "Namespace") -> None: if parent.exists(): if os.access(parent, os.W_OK): break - raise CraftError(f"Bundle output directory not writable: {str(parent)}") + raise CraftError( + f"Bundle output directory not writable: {str(parent)}" + ) # Load bundle # TODO: When this goes into the StoreService, use the service's own project_path bundle_path = self._services.package.project_dir / "bundle.yaml" bundle_config = utils.load_yaml(bundle_path) if bundle_config is None: - raise CraftError(f"Missing or invalid main bundle file: {(str(bundle_path))}") + raise CraftError( + f"Missing or invalid main bundle file: {(str(bundle_path))}" + ) bundle_name = bundle_config.get("name") if not bundle_name: raise CraftError( @@ -923,7 +960,9 @@ def run(self, parsed_args: "Namespace") -> None: ) store = Store(env.get_store_config()) - registered_names: list[Entity] = store.list_registered_names(include_collaborations=True) + registered_names: list[Entity] = store.list_registered_names( + include_collaborations=True + ) name_map = {entity.name: entity for entity in registered_names} if bundle_name not in name_map: @@ -933,7 +972,9 @@ def run(self, parsed_args: "Namespace") -> None: ) elif name_map[bundle_name].entity_type != EntityType.bundle: entity_type = name_map[bundle_name].entity_type - raise CraftError(f"Store Entity {bundle_name} is a {entity_type}, not a bundle.") + raise CraftError( + f"Store Entity {bundle_name} is a {entity_type}, not a bundle." + ) invalid_charms = [] non_charms = [] @@ -950,7 +991,9 @@ def run(self, parsed_args: "Namespace") -> None: ) if non_charms: non_charm_list = utils.humanize_list(non_charms, "and") - raise CraftError(f"The following store entities are not charms: {non_charm_list}") + raise CraftError( + f"The following store entities are not charms: {non_charm_list}" + ) # Revision in the source channel channel_map, *_ = store.list_releases(bundle_name) @@ -960,7 +1003,9 @@ def run(self, parsed_args: "Namespace") -> None: bundle_revision = release.revision break if bundle_revision is None: - raise CraftError("Cannot find a bundle released to the given source channel.") + raise CraftError( + "Cannot find a bundle released to the given source channel." + ) # Get source channel charms charm_revisions: dict[str, int] = {} @@ -1025,7 +1070,9 @@ def run(self, parsed_args: "Namespace") -> None: # Upload the bundle and release it to the target channel. store.upload(bundle_name, zipname) - release_info = store.release(bundle_name, bundle_revision, [parsed_args.to_channel], []) + release_info = store.release( + bundle_name, bundle_revision, [parsed_args.to_channel], [] + ) # There should only be one revision. release_info = release_info["released"][0] @@ -1066,10 +1113,14 @@ def run(self, parsed_args): """Run the command.""" store = Store(env.get_store_config()) revision = None # revision None will actually close the channel - channels = [parsed_args.channel] # the API accepts multiple channels, we have only one + channels = [ + parsed_args.channel + ] # the API accepts multiple channels, we have only one resources = [] # not really used when closing channels store.release(parsed_args.name, revision, channels, resources) - emit.message(f"Closed {parsed_args.channel!r} channel for {parsed_args.name!r}.") + emit.message( + f"Closed {parsed_args.channel!r} channel for {parsed_args.name!r}." + ) class StatusCommand(CharmcraftCommand): @@ -1180,7 +1231,8 @@ def run(self, parsed_args): # bases are shown alphabetically ordered sorted_bases = sorted( - releases_by_base, key=lambda b: b and (b.name, b.channel, b.architecture) + releases_by_base, + key=lambda b: b and (b.name, b.channel, b.architecture), ) for base in sorted_bases: releases_by_channel = releases_by_base[base] @@ -1196,7 +1248,9 @@ def run(self, parsed_args): } prog_releases_info = [] - prog_channels_info.append({"base": prog_base, "releases": prog_releases_info}) + prog_channels_info.append( + {"base": prog_base, "releases": prog_releases_info} + ) release_shown_for_this_track_base = False @@ -1208,7 +1262,11 @@ def run(self, parsed_args): "↑" if release_shown_for_this_track_base else "-" ) prog_version = prog_revno = prog_resources = None - prog_status = "tracking" if release_shown_for_this_track_base else "closed" + prog_status = ( + "tracking" + if release_shown_for_this_track_base + else "closed" + ) else: release_shown_for_this_track_base = True revno = prog_revno = release.revision @@ -1266,7 +1324,9 @@ def run(self, parsed_args): if parsed_args.format: emit.message(cli.format_content(prog_data, parsed_args.format)) else: - table = tabulate(human_data, headers=headers, tablefmt="plain", numalign="left") + table = tabulate( + human_data, headers=headers, tablefmt="plain", numalign="left" + ) for line in table.splitlines(): emit.message(line) @@ -1311,7 +1371,11 @@ def run(self, parsed_args): lib_name = parsed_args.name valid_all_chars = set(string.ascii_lowercase + string.digits + "_") valid_first_char = string.ascii_lowercase - if set(lib_name) - valid_all_chars or not lib_name or lib_name[0] not in valid_first_char: + if ( + set(lib_name) - valid_all_chars + or not lib_name + or lib_name[0] not in valid_first_char + ): raise CraftError( "Invalid library name. Must only use lowercase alphanumeric " "characters and underscore, starting with alpha." @@ -1347,7 +1411,9 @@ def run(self, parsed_args): lib_path.parent.mkdir(parents=True, exist_ok=True) lib_path.write_text(template.render(context)) except OSError as exc: - raise CraftError(f"Error writing the library in {str(lib_path)!r}: {exc!r}.") + raise CraftError( + f"Error writing the library in {str(lib_path)!r}: {exc!r}." + ) if parsed_args.format: info = {"library_id": lib_id} @@ -1415,7 +1481,9 @@ def run(self, parsed_args): else: local_libs_data = utils.get_libs_from_tree(charm_name) found_libs = [lib_data.full_name for lib_data in local_libs_data] - (charmlib_path,) = {lib_data.path.parent.parent for lib_data in local_libs_data} + (charmlib_path,) = { + lib_data.path.parent.parent for lib_data in local_libs_data + } emit.debug(f"Libraries found under {str(charmlib_path)!r}: {found_libs}") # check if something needs to be done @@ -1444,7 +1512,9 @@ def run(self, parsed_args): elif tip.patch == lib_data.patch: # the store has same version numbers than local if tip.content_hash == lib_data.content_hash: - error_message = f"Library {lib_data.full_name} is already updated in Charmhub." + error_message = ( + f"Library {lib_data.full_name} is already updated in Charmhub." + ) else: # but shouldn't as hash is different! error_message = ( @@ -1559,7 +1629,11 @@ def run(self, parsed_args: argparse.Namespace) -> None: to_query = [] for lib in local_libs_data: if lib.lib_id is None: - item = {"charm_name": lib.charm_name, "lib_name": lib.lib_name, "api": lib.api} + item = { + "charm_name": lib.charm_name, + "lib_name": lib.lib_name, + "api": lib.api, + } else: item = {"lib_id": lib.lib_id, "api": lib.api} to_query.append(item) @@ -1572,7 +1646,10 @@ def run(self, parsed_args: argparse.Namespace) -> None: # fix any missing lib id using the Store info if lib_data.lib_id is None: for tip in libs_tips.values(): - if lib_data.charm_name == tip.charm_name and lib_data.lib_name == tip.lib_name: + if ( + lib_data.charm_name == tip.charm_name + and lib_data.lib_name == tip.lib_name + ): lib_data = dataclasses.replace(lib_data, lib_id=tip.lib_id) break @@ -1586,9 +1663,7 @@ def run(self, parsed_args: argparse.Namespace) -> None: pass elif tip.patch < lib_data.patch: # the store has a lower version numbers than local - error_message = ( - f"Library {lib_data.full_name} has local changes, cannot be updated." - ) + error_message = f"Library {lib_data.full_name} has local changes, cannot be updated." else: # same versions locally and in the store if tip.content_hash == lib_data.content_hash: @@ -1597,15 +1672,15 @@ def run(self, parsed_args: argparse.Namespace) -> None: f"version {tip.api:d}.{tip.patch:d}." ) else: - error_message = ( - f"Library {lib_data.full_name} has local changes, cannot be updated." - ) + error_message = f"Library {lib_data.full_name} has local changes, cannot be updated." analysis.append((lib_data, error_message)) full_lib_data = [] for lib_data, error_message in analysis: if error_message is None: - downloaded = store.get_library(lib_data.charm_name, lib_data.lib_id, lib_data.api) + downloaded = store.get_library( + lib_data.charm_name, lib_data.lib_id, lib_data.api + ) if lib_data.content is None: # locally new lib_data.path.parent.mkdir(parents=True, exist_ok=True) @@ -1714,7 +1789,8 @@ def run(self, parsed_args: argparse.Namespace) -> None: emit.trace(f"Library metadata retrieved: {libs_metadata}") local_libs = { - f"{lib.charm_name}.{lib.lib_name}": lib for lib in utils.get_libs_from_tree() + f"{lib.charm_name}.{lib.lib_name}": lib + for lib in utils.get_libs_from_tree() } emit.trace(f"Local libraries: {local_libs}") @@ -1730,7 +1806,9 @@ def run(self, parsed_args: argparse.Namespace) -> None: permanent=True, ) continue - lib_name = utils.get_lib_module_name(lib_md.charm_name, lib_md.lib_name, lib_md.api) + lib_name = utils.get_lib_module_name( + lib_md.charm_name, lib_md.lib_name, lib_md.api + ) emit.progress(f"Downloading {lib_name}") lib = store.get_library( charm_name=lib_md.charm_name, @@ -1743,7 +1821,9 @@ def run(self, parsed_args: argparse.Namespace) -> None: f"Store returned no content for '{lib.charm_name}.{lib.lib_name}'" ) downloaded_libs += 1 - lib_path = utils.get_lib_path(lib_md.charm_name, lib_md.lib_name, lib_md.api) + lib_path = utils.get_lib_path( + lib_md.charm_name, lib_md.lib_name, lib_md.api + ) lib_path.parent.mkdir(exist_ok=True, parents=True) lib_path.write_text(lib.content) emit.debug(f"Downloaded {lib_name}.") @@ -1807,7 +1887,9 @@ def run(self, parsed_args): libs_tips = store.get_libraries_tips(to_query) # order it - libs_data = sorted(libs_tips.values(), key=attrgetter("lib_name", "api", "patch")) + libs_data = sorted( + libs_tips.values(), key=attrgetter("lib_name", "api", "patch") + ) if parsed_args.format: info = [ @@ -1854,7 +1936,9 @@ class ListResourcesCommand(CharmcraftCommand): def fill_parser(self, parser): """Add own parameters to the general parser.""" super().fill_parser(parser) - parser.add_argument("charm_name", metavar="charm-name", help="The name of the charm") + parser.add_argument( + "charm_name", metavar="charm-name", help="The name of the charm" + ) def run(self, parsed_args): """Run the command.""" @@ -1885,8 +1969,12 @@ def run(self, parsed_args): data = [] for revision, items in sorted(by_revision.items(), reverse=True): initial, *rest = sorted(items, key=attrgetter("name")) - data.append((revision, initial.name, initial.resource_type, initial.optional)) - data.extend(("", item.name, item.resource_type, item.optional) for item in rest) + data.append( + (revision, initial.name, initial.resource_type, initial.optional) + ) + data.extend( + ("", item.name, item.resource_type, item.optional) for item in rest + ) table = tabulate(data, headers=headers, tablefmt="plain", numalign="left") for line in table.splitlines(): @@ -1926,7 +2014,9 @@ def fill_parser(self, parser): metavar="charm-name", help="The charm name to associate the resource", ) - parser.add_argument("resource_name", metavar="resource-name", help="The resource name") + parser.add_argument( + "resource_name", metavar="resource-name", help="The resource name" + ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "--filepath", @@ -1961,7 +2051,9 @@ def run(self, parsed_args: argparse.Namespace) -> int: architectures = ["all"] if parsed_args.filepath: - emit.progress(f"Uploading resource directly from file {str(parsed_args.filepath)!r}.") + emit.progress( + f"Uploading resource directly from file {str(parsed_args.filepath)!r}." + ) bases = [{"name": "all", "channel": "all", "architectures": architectures}] result = store.upload_resource( parsed_args.charm_name, @@ -2033,7 +2125,9 @@ def run(self, parsed_args: argparse.Namespace) -> int: image_service.convert_go_arch_to_charm_arch(arch).value for arch in image_metadata.architectures } - bases = [{"name": "all", "channel": "all", "architectures": sorted(image_arch)}] + bases = [ + {"name": "all", "channel": "all", "architectures": sorted(image_arch)} + ] # all is green, get the blob to upload to Charmhub content = store.get_oci_image_blob( @@ -2053,7 +2147,9 @@ def run(self, parsed_args: argparse.Namespace) -> int: bases=bases, ) else: - raise CraftError("Either a file path or an image descriptor must be passed.") + raise CraftError( + "Either a file path or an image descriptor must be passed." + ) if result.ok: if parsed_args.format: @@ -2069,7 +2165,8 @@ def run(self, parsed_args: argparse.Namespace) -> int: if parsed_args.format: info = { "errors": [ - {"code": error.code, "message": error.message} for error in result.errors + {"code": error.code, "message": error.message} + for error in result.errors ] } emit.message(cli.format_content(info, parsed_args.format)) @@ -2116,7 +2213,9 @@ def fill_parser(self, parser) -> None: metavar="charm-name", help="The name of the charm", ) - parser.add_argument("resource_name", metavar="resource-name", help="The resource name") + parser.add_argument( + "resource_name", metavar="resource-name", help="The resource name" + ) parser.add_argument( "--revision", dest="revisions", @@ -2165,16 +2264,22 @@ def write_output( if update.updated_at is not None else "--" ), - "Architectures": ",".join(_get_architectures_from_bases(update.bases)), + "Architectures": ",".join( + _get_architectures_from_bases(update.bases) + ), } - for update in sorted(updates, key=lambda rev: int(rev.revision), reverse=True) + for update in sorted( + updates, key=lambda rev: int(rev.revision), reverse=True + ) ] else: updates_dicts = [ { "revision": update.revision, "updated_at": ( - update.updated_at.isoformat() if update.updated_at is not None else None + update.updated_at.isoformat() + if update.updated_at is not None + else None ), "architectures": _get_architectures_from_bases(update.bases), } @@ -2212,12 +2317,16 @@ def fill_parser(self, parser): metavar="charm-name", help="The charm name to associate the resource", ) - parser.add_argument("resource_name", metavar="resource-name", help="The resource name") + parser.add_argument( + "resource_name", metavar="resource-name", help="The resource name" + ) def run(self, parsed_args): """Run the command.""" store = Store(env.get_store_config()) - result = store.list_resource_revisions(parsed_args.charm_name, parsed_args.resource_name) + result = store.list_resource_revisions( + parsed_args.charm_name, parsed_args.resource_name + ) if parsed_args.format: info = [ @@ -2249,12 +2358,16 @@ def run(self, parsed_args): for item in result ] - table = tabulate(data, headers=headers, tablefmt="plain", colalign=custom_alignment) + table = tabulate( + data, headers=headers, tablefmt="plain", colalign=custom_alignment + ) for line in table.splitlines(): emit.message(line) -def _get_architectures_from_bases(bases: typing.Iterable[ResponseCharmResourceBase]) -> list[str]: +def _get_architectures_from_bases( + bases: typing.Iterable[ResponseCharmResourceBase], +) -> list[str]: """Get a list of all architectures from an iterable of resource bases.""" architectures = set() for base in bases: diff --git a/charmcraft/application/commands/test.py b/charmcraft/application/commands/test.py index 5ab6bd7e2..c68b59211 100644 --- a/charmcraft/application/commands/test.py +++ b/charmcraft/application/commands/test.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Infrastructure for the 'test' command.""" + import argparse import os import subprocess diff --git a/charmcraft/application/commands/version.py b/charmcraft/application/commands/version.py index 7df00934a..d1aaff39b 100644 --- a/charmcraft/application/commands/version.py +++ b/charmcraft/application/commands/version.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Version command.""" + import argparse import json diff --git a/charmcraft/application/main.py b/charmcraft/application/main.py index 8bb6bb8d0..c6875c135 100644 --- a/charmcraft/application/main.py +++ b/charmcraft/application/main.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """New entrypoint for charmcraft.""" + from __future__ import annotations import pathlib @@ -97,7 +98,6 @@ def _check_deprecated(self, yaml_data: dict[str, Any]) -> None: def _extra_yaml_transform( self, yaml_data: dict[str, Any], *, build_on: str, build_for: str | None ) -> dict[str, Any]: - # Extensions get applied on as close as possible to what the user provided. yaml_data = extensions.apply_extensions(self.project_dir, yaml_data.copy()) @@ -141,7 +141,9 @@ def _get_app_plugins(self) -> dict[str, PluginType]: def _pre_run(self, dispatcher: craft_cli.Dispatcher) -> None: """Override to get project_dir early.""" super()._pre_run(dispatcher) - if not self.is_managed() and not getattr(dispatcher.parsed_args(), "project_dir", None): + if not self.is_managed() and not getattr( + dispatcher.parsed_args(), "project_dir", None + ): self.project_dir = pathlib.Path().expanduser().resolve() def run_managed(self, platform: str | None, build_for: str | None) -> None: @@ -162,10 +164,14 @@ def run_managed(self, platform: str | None, build_for: str | None) -> None: output_path.mkdir(parents=True, exist_ok=True) package_file_path = self._work_dir / ".charmcraft_output_packages.txt" if package_file_path.exists(): - package_files = package_file_path.read_text().splitlines(keepends=False) + package_files = package_file_path.read_text().splitlines( + keepends=False + ) package_file_path.unlink(missing_ok=True) for filename in package_files: - shutil.move(str(self._work_dir / filename), output_path / filename) + shutil.move( + str(self._work_dir / filename), output_path / filename + ) def _expand_environment(self, yaml_data: dict[str, Any], build_for: str) -> None: """Perform expansion of project environment variables. diff --git a/charmcraft/charm_builder.py b/charmcraft/charm_builder.py index 47bd9c943..de8ca1585 100644 --- a/charmcraft/charm_builder.py +++ b/charmcraft/charm_builder.py @@ -44,7 +44,9 @@ MINIMUM_PIP_VERSION = (24, 1) KNOWN_GOOD_PIP_URL = "https://files.pythonhosted.org/packages/c0/d0/9641dc7b05877874c6418f8034ddefc809495e65caa14d38c7551cd114bb/pip-24.1.1.tar.gz" -KNOWN_GOOD_PIP_HASH = "sha256:5aa64f65e1952733ee0a9a9b1f52496ebdb3f3077cc46f80a16d983b58d1180a" +KNOWN_GOOD_PIP_HASH = ( + "sha256:5aa64f65e1952733ee0a9a9b1f52496ebdb3f3077cc46f80a16d983b58d1180a" +) def relativise(src, dst): @@ -111,7 +113,9 @@ def create_symlink(self, src_path, dest_path): dest_path.symlink_to(relative_link) else: rel_path = src_path.relative_to(self.builddir) - print(f"Ignoring symlink because targets outside the project: {str(rel_path)!r}") + print( + f"Ignoring symlink because targets outside the project: {str(rel_path)!r}" + ) @instrum.Timer("Handling generic paths") def handle_generic_paths(self): @@ -125,7 +129,9 @@ def handle_generic_paths(self): """ print("Linking in generic paths") - for basedir, dirnames, filenames in os.walk(str(self.builddir), followlinks=False): + for basedir, dirnames, filenames in os.walk( + str(self.builddir), followlinks=False + ): abs_basedir = pathlib.Path(basedir) rel_basedir = abs_basedir.relative_to(self.builddir) @@ -204,10 +210,14 @@ def handle_dispatcher(self, linked_entrypoint): if node.resolve() == linked_entrypoint: current_hooks_to_replace.append(node) node.unlink() - print(f"Replacing existing hook {node.name!r} as it's a symlink to the entrypoint") + print( + f"Replacing existing hook {node.name!r} as it's a symlink to the entrypoint" + ) # include the mandatory ones and those we need to replace - hooknames = const.MANDATORY_HOOK_NAMES | {x.name for x in current_hooks_to_replace} + hooknames = const.MANDATORY_HOOK_NAMES | { + x.name for x in current_hooks_to_replace + } for hookname in hooknames: print(f"Creating the {hookname!r} hook script pointing to dispatch") dest_hook = dest_hookpath / hookname @@ -272,9 +282,13 @@ def _install_dependencies(self, staging_venv_dir: pathlib.Path): ) if self.python_packages: print("Installing Python pre-dependencies from source.") - _process_run([pip_cmd, "install", "--no-binary=:all:", *self.python_packages]) + _process_run( + [pip_cmd, "install", "--no-binary=:all:", *self.python_packages] + ) if self.requirement_paths or self.charmlib_deps: - print("Installing packages from requirements files and charm lib dependencies.") + print( + "Installing packages from requirements files and charm lib dependencies." + ) requirements_packages = get_requirements_file_package_names( *self.requirement_paths ) @@ -411,7 +425,9 @@ def _process_run(cmd: list[str]) -> None: retcode = proc.wait() if retcode: - raise RuntimeError(f"Subprocess command {cmd} execution failed with retcode {retcode}") + raise RuntimeError( + f"Subprocess command {cmd} execution failed with retcode {retcode}" + ) def _parse_arguments() -> argparse.Namespace: diff --git a/charmcraft/const.py b/charmcraft/const.py index 586e3a459..175748e77 100644 --- a/charmcraft/const.py +++ b/charmcraft/const.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Constants used in charmcraft.""" + import enum from typing import Literal diff --git a/charmcraft/dispatch.py b/charmcraft/dispatch.py index 8d2126c9b..6222bd0aa 100644 --- a/charmcraft/dispatch.py +++ b/charmcraft/dispatch.py @@ -41,7 +41,9 @@ """ -def create_dispatch(*, prime_dir: pathlib.Path, entrypoint: str = "src/charm.py") -> bool: +def create_dispatch( + *, prime_dir: pathlib.Path, entrypoint: str = "src/charm.py" +) -> bool: """If the charm has no hooks or dispatch, create a dispatch file. :param prime_dir: the prime directory to inspect and create the file in. diff --git a/charmcraft/env.py b/charmcraft/env.py index df3f112b9..922308677 100644 --- a/charmcraft/env.py +++ b/charmcraft/env.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Charmcraft environment utilities.""" + import dataclasses import os import pathlib @@ -63,7 +64,9 @@ def is_charmcraft_running_from_snap() -> bool: def is_charmcraft_running_in_managed_mode() -> bool: """Check if charmcraft is running in a managed environment.""" - managed_flag = os.getenv(const.MANAGED_MODE_ENV_VAR, os.getenv("CRAFT_MANAGED_MODE", "n")) + managed_flag = os.getenv( + const.MANAGED_MODE_ENV_VAR, os.getenv("CRAFT_MANAGED_MODE", "n") + ) return strtobool(managed_flag) @@ -82,6 +85,12 @@ class CharmhubConfig: def get_store_config() -> CharmhubConfig: """Get the appropriate configuration for the store.""" api_url = os.getenv(const.STORE_API_ENV_VAR, DEFAULT_CHARMHUB_CONFIG.api_url) - storage_url = os.getenv(const.STORE_STORAGE_ENV_VAR, DEFAULT_CHARMHUB_CONFIG.storage_url) - registry_url = os.getenv(const.STORE_REGISTRY_ENV_VAR, DEFAULT_CHARMHUB_CONFIG.registry_url) - return CharmhubConfig(api_url=api_url, storage_url=storage_url, registry_url=registry_url) + storage_url = os.getenv( + const.STORE_STORAGE_ENV_VAR, DEFAULT_CHARMHUB_CONFIG.storage_url + ) + registry_url = os.getenv( + const.STORE_REGISTRY_ENV_VAR, DEFAULT_CHARMHUB_CONFIG.registry_url + ) + return CharmhubConfig( + api_url=api_url, storage_url=storage_url, registry_url=registry_url + ) diff --git a/charmcraft/errors.py b/charmcraft/errors.py index 6764a50e3..be1d545d1 100644 --- a/charmcraft/errors.py +++ b/charmcraft/errors.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Charmcraft error classes.""" + import io import pathlib import shlex @@ -81,7 +82,9 @@ class DuplicateCharmsError(CraftError): "Files can be seen with --verbosity=debug" ) - def __init__(self, charms: Mapping[str, Iterable[pathlib.Path]], source: bool = True): + def __init__( + self, charms: Mapping[str, Iterable[pathlib.Path]], source: bool = True + ): import charmcraft.utils charm_names = charmcraft.utils.humanize_list(charms.keys(), "and") @@ -103,7 +106,10 @@ def _format_details(charms: Mapping[str, Iterable[pathlib.Path]]) -> str: print(path_tree_line_format.format(name="CHARM", path="PATHS"), file=details) for charm, paths in charms.items(): path_iter = iter(paths) - print(path_tree_line_format.format(name=charm, path=next(path_iter)), file=details) + print( + path_tree_line_format.format(name=charm, path=next(path_iter)), + file=details, + ) for path in path_iter: print(path_tree_line_format.format(name="", path=path), file=details) return details.getvalue() diff --git a/charmcraft/extensions/_utils.py b/charmcraft/extensions/_utils.py index 3a816ad2b..1090205fc 100644 --- a/charmcraft/extensions/_utils.py +++ b/charmcraft/extensions/_utils.py @@ -41,7 +41,9 @@ def apply_extensions(project_root: Path, yaml_data: dict[str, Any]) -> dict[str, # Process extensions in a consistent order for extension_name in sorted(declared_extensions): extension_class = get_extension_class(extension_name) - extension = extension_class(project_root=project_root, yaml_data=copy.deepcopy(yaml_data)) + extension = extension_class( + project_root=project_root, yaml_data=copy.deepcopy(yaml_data) + ) extension.validate(extension_name=extension_name) _apply_extension(yaml_data, extension) return yaml_data diff --git a/charmcraft/extensions/app.py b/charmcraft/extensions/app.py index 16167a634..f7a6eb46d 100644 --- a/charmcraft/extensions/app.py +++ b/charmcraft/extensions/app.py @@ -80,7 +80,11 @@ def _check_input(self) -> None: f"the '{self.framework}-framework' extension is incompatible with " f"customized charm part" ) - incompatible_fields = {"devices", "extra-bindings", "storage"} & self.yaml_data.keys() + incompatible_fields = { + "devices", + "extra-bindings", + "storage", + } & self.yaml_data.keys() if incompatible_fields: raise ExtensionError( f"the '{self.framework}-framework' extension is incompatible with the provided " @@ -100,7 +104,9 @@ def _check_input(self) -> None: user_provided: dict[str, Any] = self._get_nested(self.yaml_data, merging) if not user_provided: continue - overlap = user_provided.keys() & self._get_nested(root_snippet, merging).keys() + overlap = ( + user_provided.keys() & self._get_nested(root_snippet, merging).keys() + ) if overlap: raise ExtensionError( f"overlapping keys {overlap} in {merging} of charmcraft.yaml " diff --git a/charmcraft/extensions/extension.py b/charmcraft/extensions/extension.py index eabc570ee..ccb152b66 100644 --- a/charmcraft/extensions/extension.py +++ b/charmcraft/extensions/extension.py @@ -106,7 +106,9 @@ def validate(self, extension_name: str): ) invalid_parts = [ - p for p in self.get_parts_snippet() if not p.startswith(f"{extension_name}/") + p + for p in self.get_parts_snippet() + if not p.startswith(f"{extension_name}/") ] if invalid_parts: raise ValueError( @@ -134,7 +136,9 @@ def append_to_env(env_variable: str, paths: Sequence[str], separator: str = ":") return f"${{{env_variable}:+${env_variable}{separator}}}" + separator.join(paths) -def prepend_to_env(env_variable: str, paths: Sequence[str], separator: str = ":") -> str: +def prepend_to_env( + env_variable: str, paths: Sequence[str], separator: str = ":" +) -> str: """Return a string for env_variable with one of more paths prepended. :param env_variable: the variable to operate on. diff --git a/charmcraft/extensions/registry.py b/charmcraft/extensions/registry.py index 3afd564e9..e55016be5 100644 --- a/charmcraft/extensions/registry.py +++ b/charmcraft/extensions/registry.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Extension registry.""" + from typing import Any from charmcraft import errors diff --git a/charmcraft/jujuignore.py b/charmcraft/jujuignore.py index d715859b3..f2bed5823 100644 --- a/charmcraft/jujuignore.py +++ b/charmcraft/jujuignore.py @@ -232,6 +232,4 @@ def match(self, path: str, is_dir: bool) -> bool: /venv .jujuignore -""".split( - "\n" -) +""".split("\n") diff --git a/charmcraft/linters.py b/charmcraft/linters.py index edcccd2e6..c8d50392c 100644 --- a/charmcraft/linters.py +++ b/charmcraft/linters.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Analyze and lint charm structures and files.""" + import abc import ast import os @@ -245,7 +246,9 @@ def _check_operator(self, basedir: pathlib.Path) -> bool: def _check_reactive(self, basedir: pathlib.Path) -> bool: """Detect if the Reactive Framework is used.""" try: - metadata = CharmMetadataLegacy.from_yaml_file(basedir / const.METADATA_FILENAME) + metadata = CharmMetadataLegacy.from_yaml_file( + basedir / const.METADATA_FILENAME + ) except Exception: # file not found, corrupted, or mandatory "name" not present return False @@ -253,7 +256,9 @@ def _check_reactive(self, basedir: pathlib.Path) -> bool: wheelhouse_dir = basedir / "wheelhouse" if not wheelhouse_dir.exists(): return False - if not any(f.name.startswith("charms.reactive-") for f in wheelhouse_dir.iterdir()): + if not any( + f.name.startswith("charms.reactive-") for f in wheelhouse_dir.iterdir() + ): return False module_basename = metadata.name.replace("-", "_") @@ -433,9 +438,13 @@ def _config_options_check(config_file: pathlib.Path) -> list[str]: return warnings with config_file.open("rt", encoding="utf8") as fh: - options = content.get("options", {}) if (content := yaml.safe_load(fh)) else {} + options = ( + content.get("options", {}) if (content := yaml.safe_load(fh)) else {} + ) - if check := NamingConventions.check_naming_convention(options.keys(), "config-options"): + if check := NamingConventions.check_naming_convention( + options.keys(), "config-options" + ): warnings.append(check) return warnings @@ -465,7 +474,9 @@ def _actions_check(action_file: pathlib.Path) -> list[str]: for param in content.get(action_name, {}).get("params", []) ] - if check := NamingConventions.check_naming_convention(actions_params, "action params"): + if check := NamingConventions.check_naming_convention( + actions_params, "action params" + ): warnings.append(check) return warnings @@ -506,7 +517,9 @@ def run(self, basedir: pathlib.Path) -> str: """Run the proper verifications.""" entrypoint = get_entrypoint_from_dispatch(basedir) if entrypoint is None: - self.text = "Cannot find a proper 'dispatch' script pointing to an entrypoint." + self.text = ( + "Cannot find a proper 'dispatch' script pointing to an entrypoint." + ) return self.Result.NONAPPLICABLE if not entrypoint.exists(): @@ -539,7 +552,9 @@ def run(self, basedir: pathlib.Path) -> str: entrypoint = get_entrypoint_from_dispatch(basedir) if entrypoint is None: - self.text = "Cannot find a proper 'dispatch' script pointing to an entrypoint." + self.text = ( + "Cannot find a proper 'dispatch' script pointing to an entrypoint." + ) return self.Result.NONAPPLICABLE if not entrypoint.exists(): @@ -636,7 +651,9 @@ class AdditionalFiles(Linter): ) } - def _check_additional_files(self, stage_dir: pathlib.Path, prime_dir: pathlib.Path) -> str: + def _check_additional_files( + self, stage_dir: pathlib.Path, prime_dir: pathlib.Path + ) -> str: """Compare the staged files with the prime files.""" errors: list[str] = [] stage_dir = stage_dir.absolute() @@ -652,7 +669,9 @@ def _check_additional_files(self, stage_dir: pathlib.Path, prime_dir: pathlib.Pa errors.append(f"File '{prime_file}' is not staged but in the charm.") if errors: - self.text = "Error: Additional files found in the charm:\n" + "\n".join(errors) + self.text = "Error: Additional files found in the charm:\n" + "\n".join( + errors + ) return self.Result.ERROR return self.Result.OK @@ -662,7 +681,9 @@ def run(self, basedir: pathlib.Path) -> str: stage_dir = basedir.parent / "stage" if not stage_dir.exists() or not stage_dir.is_dir(): # Does not work without the build environment - self.text = "Additional files check not applicable without a build environment." + self.text = ( + "Additional files check not applicable without a build environment." + ) return self.Result.NONAPPLICABLE return self._check_additional_files(stage_dir, basedir) diff --git a/charmcraft/models/basic.py b/charmcraft/models/basic.py index bb49e23a1..4cef986e2 100644 --- a/charmcraft/models/basic.py +++ b/charmcraft/models/basic.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Charmcraft basic pydantic model.""" + from typing import Annotated import craft_parts.constraints diff --git a/charmcraft/models/charmcraft.py b/charmcraft/models/charmcraft.py index 991581fec..ade1309a8 100644 --- a/charmcraft/models/charmcraft.py +++ b/charmcraft/models/charmcraft.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Charmcraft configuration pydantic model.""" + from typing import TypedDict, cast import pydantic @@ -45,8 +46,12 @@ class Charmhub(CraftBaseModel): """Definition of Charmhub endpoint configuration.""" api_url: pydantic.HttpUrl = cast(pydantic.HttpUrl, "https://api.charmhub.io") - storage_url: pydantic.HttpUrl = cast(pydantic.HttpUrl, "https://storage.snapcraftcontent.com") - registry_url: pydantic.HttpUrl = cast(pydantic.HttpUrl, "https://registry.jujucharms.com") + storage_url: pydantic.HttpUrl = cast( + pydantic.HttpUrl, "https://storage.snapcraftcontent.com" + ) + registry_url: pydantic.HttpUrl = cast( + pydantic.HttpUrl, "https://registry.jujucharms.com" + ) class Base(CraftBaseModel): diff --git a/charmcraft/models/config.py b/charmcraft/models/config.py index 75b3664dc..b82ed3caa 100644 --- a/charmcraft/models/config.py +++ b/charmcraft/models/config.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Charmcraft Juju Config pydantic model.""" + from typing import Annotated, Literal import pydantic @@ -65,12 +66,17 @@ class JujuSecretOption(_BaseJujuOption): # the deployment in a model) is at the time that they are # writing the config, but included for completeness. default: ( - Annotated[str, pydantic.StringConstraints(pattern=r"^secret:[a-z0-9]{20}$")] | None + Annotated[str, pydantic.StringConstraints(pattern=r"^secret:[a-z0-9]{20}$")] + | None ) = None JujuOption = Annotated[ - JujuStringOption | JujuIntOption | JujuFloatOption | JujuBooleanOption | JujuSecretOption, + JujuStringOption + | JujuIntOption + | JujuFloatOption + | JujuBooleanOption + | JujuSecretOption, pydantic.Field(discriminator="type"), ] diff --git a/charmcraft/models/lint.py b/charmcraft/models/lint.py index 8352ad359..5b53399ae 100644 --- a/charmcraft/models/lint.py +++ b/charmcraft/models/lint.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Models for linters.""" + import enum from typing import final diff --git a/charmcraft/models/manifest.py b/charmcraft/models/manifest.py index 894e2d649..7fad49e34 100644 --- a/charmcraft/models/manifest.py +++ b/charmcraft/models/manifest.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Model for output charm's manifest.yaml file.""" + from typing import Any, Literal from craft_application import models diff --git a/charmcraft/models/project.py b/charmcraft/models/project.py index 9930b612a..171764166 100644 --- a/charmcraft/models/project.py +++ b/charmcraft/models/project.py @@ -121,7 +121,9 @@ def _validate_api_version(cls, value: str) -> str: try: int(api) except ValueError: - raise ValueError(f"API version not valid. Expected an integer, got {api!r}") from None + raise ValueError( + f"API version not valid. Expected an integer, got {api!r}" + ) from None return str(value) @pydantic.field_validator("version", mode="before") @@ -335,7 +337,9 @@ def get_build_plan(self) -> list[models.BuildInfo]: platform=current_arch, build_on=current_arch, build_for=current_arch, - base=bases.BaseName(name=current_base.system, version=current_base.release), + base=bases.BaseName( + name=current_base.system, version=current_base.release + ), ) ] if not self.base: @@ -507,7 +511,9 @@ def _preprocess_parts( ) -> dict[str, dict[str, Any]]: """Preprocess parts object for a charm or bundle, creating an implicit part if needed.""" if parts is not None and not isinstance(parts, dict): - raise TypeError("'parts' in charmcraft.yaml must conform to the charmcraft.yaml spec.") + raise TypeError( + "'parts' in charmcraft.yaml must conform to the charmcraft.yaml spec." + ) if not parts: if info.config and info.config.get("title") == "Bundle": parts = {"bundle": {"plugin": "bundle"}} @@ -1001,14 +1007,19 @@ def _check_base_is_legacy(base: charmcraft.BaseDict) -> bool: and base["channel"] < "24.04" # pyright: ignore[reportTypedDictNotRequiredAccess] ): return True - return base in ({"name": "centos", "channel": "7"}, {"name": "almalinux", "channel": "9"}) + return base in ( + {"name": "centos", "channel": "7"}, + {"name": "almalinux", "channel": "9"}, + ) def _validate_base( base: charmcraft.BaseDict | charmcraft.LongFormBasesDict, ) -> charmcraft.LongFormBasesDict: if "name" in base: # Convert short form to long form - base = cast(charmcraft.LongFormBasesDict, {"build-on": [base], "run-on": [base]}) + base = cast( + charmcraft.LongFormBasesDict, {"build-on": [base], "run-on": [base]} + ) else: # Cast to long form since we know it is one. base = cast(charmcraft.LongFormBasesDict, base) @@ -1038,9 +1049,9 @@ class BasesCharm(CharmProject): # This is defined this way because using conlist makes mypy sad and using # a ConstrainedList child class has pydantic issues. This appears to be # solved with Pydantic 2. - bases: list[Annotated[BasesConfiguration, pydantic.BeforeValidator(_validate_base)]] = ( - pydantic.Field(min_length=1) - ) + bases: list[ + Annotated[BasesConfiguration, pydantic.BeforeValidator(_validate_base)] + ] = pydantic.Field(min_length=1) base: None = None diff --git a/charmcraft/parts/__init__.py b/charmcraft/parts/__init__.py index 4530f5105..4d496aaad 100644 --- a/charmcraft/parts/__init__.py +++ b/charmcraft/parts/__init__.py @@ -72,7 +72,9 @@ def process_part_config(data: dict[str, Any]) -> dict[str, Any]: plugin_properties = plugin_class.properties_class.unmarshal(spec) # validate common part properties - part_spec = craft_parts.plugins.extract_part_properties(spec, plugin_name=plugin_name) + part_spec = craft_parts.plugins.extract_part_properties( + spec, plugin_name=plugin_name + ) PartSpec(**part_spec) # get plugin properties data if it's model based (otherwise it's empty), and diff --git a/charmcraft/parts/lifecycle.py b/charmcraft/parts/lifecycle.py index a12c5aadd..8875d4499 100644 --- a/charmcraft/parts/lifecycle.py +++ b/charmcraft/parts/lifecycle.py @@ -17,6 +17,7 @@ PENDING DEPRECATION: we're moving this to a craft-application LifecycleService """ + import os import pathlib import shlex @@ -88,7 +89,9 @@ def run(self, target_step: Step) -> None: charm_part = self._all_parts["charm"] if charm_part.get("plugin") == "charm": entrypoint = os.path.normpath(charm_part["charm-entrypoint"]) - dis_entrypoint = os.path.normpath(_get_dispatch_entrypoint(self.prime_dir)) + dis_entrypoint = os.path.normpath( + _get_dispatch_entrypoint(self.prime_dir) + ) if entrypoint != dis_entrypoint: self._lcm.clean(Step.BUILD, part_names=["charm"]) self._lcm.reload_state() @@ -100,8 +103,12 @@ def run(self, target_step: Step) -> None: with self._lcm.action_executor() as aex: executor_timer.mark("Context enter") for act in actions: - emit.progress(f"Running step {act.step.name} for part {act.part_name!r}") - with instrum.Timer("Running step", step=act.step.name, part=act.part_name): # type: ignore[arg-type] + emit.progress( + f"Running step {act.step.name} for part {act.part_name!r}" + ) + with instrum.Timer( + "Running step", step=act.step.name, part=act.part_name + ): # type: ignore[arg-type] with emit.open_stream("Execute action") as stream: aex.execute([act], stdout=stream, stderr=stream) executor_timer.mark("Context exit") diff --git a/charmcraft/parts/plugins/_bundle.py b/charmcraft/parts/plugins/_bundle.py index 057131997..bff693cff 100644 --- a/charmcraft/parts/plugins/_bundle.py +++ b/charmcraft/parts/plugins/_bundle.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Bundle plugin for craft-parts.""" + import sys from typing import Literal diff --git a/charmcraft/parts/plugins/_charm.py b/charmcraft/parts/plugins/_charm.py index fcfa4b249..a93995b5e 100644 --- a/charmcraft/parts/plugins/_charm.py +++ b/charmcraft/parts/plugins/_charm.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Charm plugin for craft-parts.""" + import os import pathlib import re @@ -55,7 +56,9 @@ class CharmPluginProperties(plugins.PluginProperties, frozen=True): """ @pydantic.field_validator("charm_entrypoint", mode="after") - def _validate_entrypoint(cls, charm_entrypoint: str, info: pydantic.ValidationInfo) -> str: + def _validate_entrypoint( + cls, charm_entrypoint: str, info: pydantic.ValidationInfo + ) -> str: """Validate the entry point.""" # the location of the project is needed if "source" not in info.data: @@ -67,11 +70,15 @@ def _validate_entrypoint(cls, charm_entrypoint: str, info: pydantic.ValidationIn # check that the entrypoint is inside the project filepath = (project_dirpath / charm_entrypoint).resolve() if project_dirpath not in filepath.parents: - raise ValueError(f"charm entry point must be inside the project: {str(filepath)!r}") + raise ValueError( + f"charm entry point must be inside the project: {str(filepath)!r}" + ) # store the entrypoint always relative to the project's path (no matter if the origin # was relative or absolute) - rel_entrypoint = (project_dirpath / charm_entrypoint).relative_to(project_dirpath) + rel_entrypoint = (project_dirpath / charm_entrypoint).relative_to( + project_dirpath + ) return rel_entrypoint.as_posix() @pydantic.model_validator(mode="after") @@ -90,7 +97,10 @@ def _validate_requirements(self) -> Self: # if nothing indicated, and default file is there, use it default_reqs_name = "requirements.txt" - if not self.charm_requirements and (project_dirpath / default_reqs_name).is_file(): + if ( + not self.charm_requirements + and (project_dirpath / default_reqs_name).is_file() + ): self.charm_requirements.append(default_reqs_name) return self @@ -188,7 +198,10 @@ def get_build_packages(self) -> set[str]: elif platform.is_yum_based(): try: os_release = os_utils.OsRelease() - if (os_release.id(), os_release.version_id()) in (("centos", "7"), ("rhel", "7")): + if (os_release.id(), os_release.version_id()) in ( + ("centos", "7"), + ("rhel", "7"), + ): # CentOS 7 Python 3.8 from SCL repo return { "autoconf", @@ -305,7 +318,10 @@ def _get_strict_dependencies_parameters(self) -> list[str]: options = cast(CharmPluginProperties, self._options) return [ "--strict-dependencies", - *(f"--binary-package={package}" for package in options.charm_binary_python_packages), + *( + f"--binary-package={package}" + for package in options.charm_binary_python_packages + ), *(f"--requirement={reqs}" for reqs in options.charm_requirements), ] @@ -324,7 +340,10 @@ def _get_legacy_dependencies_parameters(self) -> list[str]: base_tools.remove(pkg) os_release = os_utils.OsRelease() - if (os_release.id(), os_release.version_id()) in (("centos", "7"), ("rhel", "7")): + if (os_release.id(), os_release.version_id()) in ( + ("centos", "7"), + ("rhel", "7"), + ): # CentOS 7 compatibility, bootstrap base tools use binary packages for pkg in base_tools: parameters.extend(["-b", pkg]) @@ -354,7 +373,10 @@ def _get_os_special_priority_paths(self) -> str | None: """Return a str of PATH for special OS.""" with suppress(OsReleaseIdError, OsReleaseVersionIdError): os_release = os_utils.OsRelease() - if (os_release.id(), os_release.version_id()) in (("centos", "7"), ("rhel", "7")): + if (os_release.id(), os_release.version_id()) in ( + ("centos", "7"), + ("rhel", "7"), + ): # CentOS 7 Python 3.8 from SCL repo return "/opt/rh/rh-python38/root/usr/bin" diff --git a/charmcraft/parts/plugins/_poetry.py b/charmcraft/parts/plugins/_poetry.py index 102076f64..21415dd6b 100644 --- a/charmcraft/parts/plugins/_poetry.py +++ b/charmcraft/parts/plugins/_poetry.py @@ -25,7 +25,6 @@ class PoetryPluginProperties(poetry_plugin.PoetryPluginProperties, frozen=True): - poetry_keep_bins: bool = False """Keep the virtual environment's 'bin' directory.""" diff --git a/charmcraft/parts/plugins/_python.py b/charmcraft/parts/plugins/_python.py index cce0e58ba..f4dabd3f8 100644 --- a/charmcraft/parts/plugins/_python.py +++ b/charmcraft/parts/plugins/_python.py @@ -25,7 +25,6 @@ class PythonPluginProperties(python_plugin.PythonPluginProperties, frozen=True): - python_packages: list[str] = [] # No default packages. python_keep_bins: bool = False """Keep the virtual environment's 'bin' directory.""" @@ -63,7 +62,10 @@ def _get_package_install_commands(self) -> list[str]: pip = self._get_pip() install_params = shlex.join( ( - *(f"--constraint={constraint}" for constraint in self._options.python_constraints), + *( + f"--constraint={constraint}" + for constraint in self._options.python_constraints + ), *( f"--requirement={requirement}" for requirement in self._options.python_requirements diff --git a/charmcraft/parts/plugins/_reactive.py b/charmcraft/parts/plugins/_reactive.py index 275979740..8bca89bcd 100644 --- a/charmcraft/parts/plugins/_reactive.py +++ b/charmcraft/parts/plugins/_reactive.py @@ -147,7 +147,9 @@ def run_charm_tool(args: list[str]): result_classification = "ERROR" raise result_classification = "WARNING" - print(f"charm tool execution {result_classification}: returncode={exc.returncode}") + print( + f"charm tool execution {result_classification}: returncode={exc.returncode}" + ) else: print( f"charm tool execution {result_classification}: returncode={completed_process.returncode}" @@ -155,7 +157,11 @@ def run_charm_tool(args: list[str]): def build( - *, charm_name: str, build_dir: Path, install_dir: Path, charm_build_arguments: list[str] + *, + charm_name: str, + build_dir: Path, + install_dir: Path, + charm_build_arguments: list[str], ): """Build a charm using charm tool. diff --git a/charmcraft/preprocess.py b/charmcraft/preprocess.py index f114a87e3..c20a931a8 100644 --- a/charmcraft/preprocess.py +++ b/charmcraft/preprocess.py @@ -18,6 +18,7 @@ These functions are called from the Application class's `_extra_yaml_transform` to do pre-processing on a charmcraft.yaml file before applying extensions. """ + import pathlib from typing import Any diff --git a/charmcraft/services/__init__.py b/charmcraft/services/__init__.py index 04d8d2b90..c4ccb43f1 100644 --- a/charmcraft/services/__init__.py +++ b/charmcraft/services/__init__.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Service classes charmcraft.""" + from __future__ import annotations import dataclasses diff --git a/charmcraft/services/analysis.py b/charmcraft/services/analysis.py index 04911fefa..6565091cb 100644 --- a/charmcraft/services/analysis.py +++ b/charmcraft/services/analysis.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Service class for packing.""" + from __future__ import annotations import pathlib @@ -34,12 +35,18 @@ class AnalysisService(craft_application.AppService): _project: models.CharmcraftProject # type: ignore[assignment] def __init__( # (too many arguments) - self, app: craft_application.AppMetadata, services: craft_application.ServiceFactory + self, + app: craft_application.AppMetadata, + services: craft_application.ServiceFactory, ) -> None: super().__init__(app, services) def lint_directory( - self, path: pathlib.Path, *, ignore: Container[str] = (), include_ignored: bool = True + self, + path: pathlib.Path, + *, + ignore: Container[str] = (), + include_ignored: bool = True, ) -> Iterator[CheckResult]: """Lint an unpacked charm in the given directory.""" for checker, run in self._gen_checkers(ignore=ignore): @@ -49,7 +56,11 @@ def lint_directory( yield checker.get_ignore_result() def lint_file( - self, path: pathlib.Path, *, ignore: Container[str] = (), include_ignored: bool = True + self, + path: pathlib.Path, + *, + ignore: Container[str] = (), + include_ignored: bool = True, ) -> Iterator[CheckResult]: """Lint a packed charm. @@ -61,7 +72,9 @@ def lint_file( """ path = path.resolve(strict=True) - with tempfile.TemporaryDirectory(prefix=f"charmcraft_{path.name}_") as directory: + with tempfile.TemporaryDirectory( + prefix=f"charmcraft_{path.name}_" + ) as directory: directory_path = pathlib.Path(directory) try: with zipfile.ZipFile(path) as zip_file: @@ -85,7 +98,9 @@ def lint_file( ) @staticmethod - def _gen_checkers(ignore: Container[str]) -> Iterator[tuple[linters.BaseChecker, bool]]: + def _gen_checkers( + ignore: Container[str], + ) -> Iterator[tuple[linters.BaseChecker, bool]]: """Generate the checker classes to run, in their correct order.""" for cls in linters.CHECKERS: run_linter = cls.name not in ignore diff --git a/charmcraft/services/charmlibs.py b/charmcraft/services/charmlibs.py index a5e5a2ee9..74d71156d 100644 --- a/charmcraft/services/charmlibs.py +++ b/charmcraft/services/charmlibs.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Service class for interacting with charm libraries.""" + from __future__ import annotations import pathlib @@ -60,7 +61,9 @@ def is_downloaded( lib_info = utils.get_lib_info(lib_path=self._project_dir / lib_path) return lib_info.patch == patch - def get_local_version(self, *, charm_name: str, lib_name: str) -> tuple[int, int] | None: + def get_local_version( + self, *, charm_name: str, lib_name: str + ) -> tuple[int, int] | None: """Get the version of the library on the machine, or None. :param charm_name: The name of the charm where the lib is published diff --git a/charmcraft/services/image.py b/charmcraft/services/image.py index bfc2ec8a9..cc2827b07 100644 --- a/charmcraft/services/image.py +++ b/charmcraft/services/image.py @@ -133,7 +133,9 @@ def get_maybe_id_from_docker(self, url: str) -> str | None: @staticmethod def convert_go_arch_to_charm_arch(architecture: str) -> const.CharmArch: """Convert an OCI architecture to a charm architecture.""" - return const.CharmArch(const.GO_ARCH_TO_CHARM_ARCH.get(architecture, architecture)) + return const.CharmArch( + const.GO_ARCH_TO_CHARM_ARCH.get(architecture, architecture) + ) def inspect(self, image: str) -> OCIMetadata: """Inspect an image with Skopeo and return the relevant metadata. diff --git a/charmcraft/services/lifecycle.py b/charmcraft/services/lifecycle.py index b932e0753..b9710545b 100644 --- a/charmcraft/services/lifecycle.py +++ b/charmcraft/services/lifecycle.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Service class for running craft lifecycle commands.""" + from __future__ import annotations from typing import cast @@ -67,4 +68,6 @@ def post_prime(self, step_info: craft_parts.StepInfo) -> bool: project_info = cast(craft_parts.ProjectInfo, step_info.project_info) # TODO: include an entrypoint override. #1896 - return return_value | dispatch.create_dispatch(prime_dir=project_info.dirs.prime_dir) + return return_value | dispatch.create_dispatch( + prime_dir=project_info.dirs.prime_dir + ) diff --git a/charmcraft/services/package.py b/charmcraft/services/package.py index e06664adc..beb39e814 100644 --- a/charmcraft/services/package.py +++ b/charmcraft/services/package.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Service class for packing.""" + from __future__ import annotations import json @@ -63,7 +64,9 @@ def __init__( project_dir: pathlib.Path, build_plan: list[craft_application.models.BuildInfo], ) -> None: - super().__init__(app, services, project=cast(craft_application.models.Project, project)) + super().__init__( + app, services, project=cast(craft_application.models.Project, project) + ) self.project_dir = project_dir.resolve(strict=True) self._platform = build_plan[0].platform self._build_plan = build_plan @@ -95,7 +98,9 @@ def _write_package_paths(self, packages: Iterable[pathlib.Path]) -> None: with packages_file.open("at") as file: file.writelines(f"{package.name}\n" for package in packages) - def pack_bundle(self, prime_dir: pathlib.Path, dest_dir: pathlib.Path) -> pathlib.Path: + def pack_bundle( + self, prime_dir: pathlib.Path, dest_dir: pathlib.Path + ) -> pathlib.Path: """Pack a prime directory as a bundle.""" name = self._project.name or "bundle" bundle_path = dest_dir / f"{name}.zip" @@ -103,7 +108,9 @@ def pack_bundle(self, prime_dir: pathlib.Path, dest_dir: pathlib.Path) -> pathli utils.build_zip(bundle_path, prime_dir) return bundle_path - def pack_charm(self, prime_dir: pathlib.Path, dest_dir: pathlib.Path) -> pathlib.Path: + def pack_charm( + self, prime_dir: pathlib.Path, dest_dir: pathlib.Path + ) -> pathlib.Path: """Pack a prime directory as a charm for a given set of bases.""" charm_path = self.get_charm_path(dest_dir) emit.progress(f"Packing charm {charm_path.name}") @@ -214,11 +221,15 @@ def get_manifest_bases(self) -> list[models.Base]: if platform.build_for: architectures = [str(arch) for arch in platform.build_for] else: - raise ValueError(f"Platform {self._platform} contains unknown build-for.") + raise ValueError( + f"Platform {self._platform} contains unknown build-for." + ) else: architectures = [util.get_host_architecture()] return [models.Base.from_str_and_arch(self._project.base, architectures)] - raise TypeError(f"Unknown charm type {self._project.__class__}, cannot get bases.") + raise TypeError( + f"Unknown charm type {self._project.__class__}, cannot get bases." + ) def write_metadata(self, path: pathlib.Path) -> None: """Write additional charm metadata. @@ -248,7 +259,10 @@ def write_metadata(self, path: pathlib.Path) -> None: (path / "manifest.yaml").write_text( utils.dump_yaml( manifest.model_dump( - mode="json", by_alias=True, exclude_unset=False, exclude_none=True + mode="json", + by_alias=True, + exclude_unset=False, + exclude_none=True, ) ) ) @@ -256,7 +270,9 @@ def write_metadata(self, path: pathlib.Path) -> None: project_dict = self._project.marshal() # If there is a reactive part, defer to it for the existence of metadata.yaml. - plugins = {part.get("plugin") or name for name, part in self._project.parts.items()} + plugins = { + part.get("plugin") or name for name, part in self._project.parts.items() + } is_reactive = "reactive" in plugins stage_dir = self._services.lifecycle.project_info.dirs.stage_dir if is_reactive and (stage_dir / const.METADATA_FILENAME).exists(): @@ -264,9 +280,13 @@ def write_metadata(self, path: pathlib.Path) -> None: f"{const.METADATA_FILENAME!r} generated by charm. Not using original project metadata." ) else: - self._write_file_or_object(self.metadata.marshal(), const.METADATA_FILENAME, path) + self._write_file_or_object( + self.metadata.marshal(), const.METADATA_FILENAME, path + ) if is_reactive and (stage_dir / const.JUJU_ACTIONS_FILENAME).exists(): - emit.debug(f"{const.JUJU_ACTIONS_FILENAME!r} generated by charm. Skipping generation.") + emit.debug( + f"{const.JUJU_ACTIONS_FILENAME!r} generated by charm. Skipping generation." + ) elif actions := cast(dict | None, project_dict.get("actions")): self._write_file_or_object(actions, "actions.yaml", path) if config := cast(dict | None, project_dict.get("config")): diff --git a/charmcraft/services/provider.py b/charmcraft/services/provider.py index 443e1fb8b..ba3c03c85 100644 --- a/charmcraft/services/provider.py +++ b/charmcraft/services/provider.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Service class for creating providers.""" + from __future__ import annotations import contextlib @@ -140,7 +141,8 @@ def _maybe_lock_cache(path: pathlib.Path) -> io.TextIOBase | None: fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: emit.progress( - "Shared cache locked by another process; running without cache.", permanent=True + "Shared cache locked by another process; running without cache.", + permanent=True, ) return None else: diff --git a/charmcraft/services/remotebuild.py b/charmcraft/services/remotebuild.py index b537a72ca..9d45da75e 100644 --- a/charmcraft/services/remotebuild.py +++ b/charmcraft/services/remotebuild.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Charmcraft-specific overrides for the remote build service.""" + import datetime import pathlib from collections.abc import Mapping diff --git a/charmcraft/services/store.py b/charmcraft/services/store.py index 90334fb0f..f5c562504 100644 --- a/charmcraft/services/store.py +++ b/charmcraft/services/store.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Service class for store interaction.""" + from __future__ import annotations import platform @@ -207,17 +208,23 @@ def set_resource_revisions_architectures( *( models.CharmResourceRevisionUpdateRequest( revision=revision, - bases=[models.RequestCharmResourceBase(architectures=architectures)], + bases=[ + models.RequestCharmResourceBase(architectures=architectures) + ], ) for revision, architectures in updates.items() ), name=name, resource_name=resource_name, ) - new_revisions = self.client.list_resource_revisions(name=name, resource_name=resource_name) + new_revisions = self.client.list_resource_revisions( + name=name, resource_name=resource_name + ) return [rev for rev in new_revisions if int(rev.revision) in updates] - def get_libraries_metadata(self, libraries: Sequence[CharmLib]) -> Sequence[Library]: + def get_libraries_metadata( + self, libraries: Sequence[CharmLib] + ) -> Sequence[Library]: """Get the metadata for one or more charm libraries. :param libraries: A sequence of libraries to request. @@ -249,7 +256,12 @@ def get_libraries_metadata_by_name( } def get_library( - self, charm_name: str, *, library_id: str, api: int | None = None, patch: int | None = None + self, + charm_name: str, + *, + library_id: str, + api: int | None = None, + patch: int | None = None, ) -> Library: """Get a library by charm name and ID from charmhub.""" return self.anonymous_client.get_library( diff --git a/charmcraft/store/client.py b/charmcraft/store/client.py index 3bcbce428..006fdb5df 100644 --- a/charmcraft/store/client.py +++ b/charmcraft/store/client.py @@ -39,7 +39,9 @@ def build_user_agent(): """Build the charmcraft's user agent.""" - if any(key.startswith(prefix) for prefix in TESTING_ENV_PREFIXES for key in os.environ): + if any( + key.startswith(prefix) for prefix in TESTING_ENV_PREFIXES for key in os.environ + ): testing = " (testing) " else: testing = " " @@ -53,15 +55,23 @@ class AnonymousClient: def __init__(self, api_base_url: str, storage_base_url: str): self.api_base_url = api_base_url.rstrip("/") self.storage_base_url = storage_base_url.rstrip("/") - self._http_client = craft_store.http_client.HTTPClient(user_agent=build_user_agent()) + self._http_client = craft_store.http_client.HTTPClient( + user_agent=build_user_agent() + ) def request_urlpath_text(self, method: str, urlpath: str, *args, **kwargs) -> str: """Return a request.Response to a urlpath.""" - return self._http_client.request(method, self.api_base_url + urlpath, *args, **kwargs).text + return self._http_client.request( + method, self.api_base_url + urlpath, *args, **kwargs + ).text - def request_urlpath_json(self, method: str, urlpath: str, *args, **kwargs) -> dict[str, Any]: + def request_urlpath_json( + self, method: str, urlpath: str, *args, **kwargs + ) -> dict[str, Any]: """Return .json() from a request.Response to a urlpath.""" - response = self._http_client.request(method, self.api_base_url + urlpath, *args, **kwargs) + response = self._http_client.request( + method, self.api_base_url + urlpath, *args, **kwargs + ) try: return response.json() @@ -71,7 +81,12 @@ def request_urlpath_json(self, method: str, urlpath: str, *args, **kwargs) -> di ) from json_error def get_library( - self, *, charm_name: str, library_id: str, api: int | None = None, patch: int | None = None + self, + *, + charm_name: str, + library_id: str, + api: int | None = None, + patch: int | None = None, ) -> Library: """Fetch a library attached to a charm. @@ -100,10 +115,13 @@ def fetch_libraries_metadata( emit.trace( f"Fetching library metadata from charmhub: {libs}", ) - response = self.request_urlpath_json("POST", "/v1/charm/libraries/bulk", json=libs) + response = self.request_urlpath_json( + "POST", "/v1/charm/libraries/bulk", json=libs + ) if "libraries" not in response: raise CraftError( - "Server returned invalid response while querying libraries", details=str(response) + "Server returned invalid response while querying libraries", + details=str(response), ) converted_response = [Library.from_dict(lib) for lib in response["libraries"]] emit.trace(f"Store response: {converted_response}") @@ -130,7 +148,9 @@ def __init__( Supports both charmcraft 2.x style init and compatibility with upstream. """ if base_url and api_base_url or not base_url and not api_base_url: - raise ValueError("Either base_url or api_base_url must be set, but not both.") + raise ValueError( + "Either base_url or api_base_url must be set, but not both." + ) if base_url: api_base_url = base_url self.api_base_url = api_base_url.rstrip("/") @@ -166,9 +186,13 @@ def logout(self, *args, **kwargs): def request_urlpath_text(self, method: str, urlpath: str, *args, **kwargs) -> str: """Return a request.Response to a urlpath.""" - return super().request(method, self.api_base_url + urlpath, *args, **kwargs).text + return ( + super().request(method, self.api_base_url + urlpath, *args, **kwargs).text + ) - def request_urlpath_json(self, method: str, urlpath: str, *args, **kwargs) -> dict[str, Any]: + def request_urlpath_json( + self, method: str, urlpath: str, *args, **kwargs + ) -> dict[str, Any]: """Return .json() from a request.Response to a urlpath.""" response = super().request(method, self.api_base_url + urlpath, *args, **kwargs) @@ -190,7 +214,9 @@ def push_file(self, filepath) -> str: # create a monitor (so that progress can be displayed) as call the real pusher monitor = MultipartEncoderMonitor(encoder) - with emit.progress_bar("Uploading...", monitor.len, delta=False) as progress: + with emit.progress_bar( + "Uploading...", monitor.len, delta=False + ) as progress: monitor.callback = lambda mon: progress.advance(mon.bytes_read) response = self._storage_push(monitor) @@ -207,6 +233,9 @@ def _storage_push(self, monitor) -> requests.Response: return super().request( "POST", self.storage_base_url + "/unscanned-upload/", - headers={"Content-Type": monitor.content_type, "Accept": "application/json"}, + headers={ + "Content-Type": monitor.content_type, + "Accept": "application/json", + }, data=monitor, ) diff --git a/charmcraft/store/store.py b/charmcraft/store/store.py index 2e8517734..1f2e44fd0 100644 --- a/charmcraft/store/store.py +++ b/charmcraft/store/store.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """The Store API handling.""" + import os import pathlib import platform @@ -155,8 +156,12 @@ def error_decorator(self, *args, **kwargs): "Regenerate them and try again." ) if not auto_login: - raise CraftError("Existing credentials are no longer valid for Charmhub.") - emit.progress("Existing credentials no longer valid. Trying to log in...") + raise CraftError( + "Existing credentials are no longer valid for Charmhub." + ) + emit.progress( + "Existing credentials no longer valid. Trying to log in..." + ) # Clear credentials before trying to login again self.logout() else: @@ -178,14 +183,20 @@ def __init__(self, charmhub_config, ephemeral=False, needs_auth=True): if needs_auth: try: self._client = Client( - charmhub_config.api_url, charmhub_config.storage_url, ephemeral=ephemeral + charmhub_config.api_url, + charmhub_config.storage_url, + ephemeral=ephemeral, ) except craft_store.errors.NoKeyringError as error: raise CraftError(str(error)) from error else: - self._client = AnonymousClient(charmhub_config.api_url, charmhub_config.storage_url) + self._client = AnonymousClient( + charmhub_config.api_url, charmhub_config.storage_url + ) - def login(self, permissions=None, ttl=None, charms=None, bundles=None, channels=None): + def login( + self, permissions=None, ttl=None, charms=None, bundles=None, channels=None + ): """Login into the store.""" hostname = _get_hostname() # Used to identify the login on Ubuntu SSO to ease future revokations. @@ -201,11 +212,13 @@ def login(self, permissions=None, ttl=None, charms=None, bundles=None, channels= packages = [] if charms is not None: packages.extend( - endpoints.Package(package_type="charm", package_name=charm) for charm in charms + endpoints.Package(package_type="charm", package_name=charm) + for charm in charms ) if bundles is not None: packages.extend( - endpoints.Package(package_type="bundle", package_name=bundle) for bundle in bundles + endpoints.Package(package_type="bundle", package_name=bundle) + for bundle in bundles ) if packages: kwargs["packages"] = packages @@ -233,7 +246,9 @@ def whoami(self): response = self._client.whoami() acc = response["account"] - account = Account(name=acc["display-name"], username=acc["username"], id=acc["id"]) + account = Account( + name=acc["display-name"], username=acc["username"], id=acc["id"] + ) if response["packages"] is None: packages = None else: @@ -352,11 +367,15 @@ def upload_resource( @_store_client_wrapper() def list_revisions(self, name): """Return charm revisions for the indicated charm.""" - response = self._client.request_urlpath_json("GET", f"/v1/charm/{name}/revisions") + response = self._client.request_urlpath_json( + "GET", f"/v1/charm/{name}/revisions" + ) return [_build_revision(item) for item in response["revisions"]] @_store_client_wrapper() - def release(self, name: str, revision: int, channels: list[str], resources) -> dict[str, Any]: + def release( + self, name: str, revision: int, channels: list[str], resources + ) -> dict[str, Any]: """Release one or more revisions for a package.""" endpoint = f"/v1/charm/{name}/releases" resources = [{"name": res.name, "revision": res.revision} for res in resources] @@ -368,7 +387,9 @@ def release(self, name: str, revision: int, channels: list[str], resources) -> d return self._client.request_urlpath_json("POST", endpoint, json=items) @_store_client_wrapper() - def list_releases(self, name: str) -> tuple[list[Release], list[Channel], list[Revision]]: + def list_releases( + self, name: str + ) -> tuple[list[Release], list[Channel], list[Revision]]: """List current releases for a package.""" endpoint = f"/v1/charm/{name}/releases" response = self._client.request_urlpath_json("GET", endpoint) @@ -416,7 +437,9 @@ def create_library_id(self, charm_name, lib_name): return response["library-id"] @_store_client_wrapper() - def create_library_revision(self, charm_name, lib_id, api, patch, content, content_hash): + def create_library_revision( + self, charm_name, lib_id, api, patch, content, content_hash + ): """Create a new library revision.""" endpoint = f"/v1/charm/libraries/{charm_name}/{lib_id}" payload = { @@ -462,12 +485,17 @@ def get_libraries_tips(self, libraries): payload.append(item) response = self._client.request_urlpath_json("POST", endpoint, json=payload) libraries = response["libraries"] - return {(item["library-id"], item["api"]): _build_library(item) for item in libraries} + return { + (item["library-id"], item["api"]): _build_library(item) + for item in libraries + } @_store_client_wrapper() def list_resources(self, charm): """Return resources associated to the indicated charm.""" - response = self._client.request_urlpath_json("GET", f"/v1/charm/{charm}/resources") + response = self._client.request_urlpath_json( + "GET", f"/v1/charm/{charm}/resources" + ) return [_build_resource(item) for item in response["resources"]] @_store_client_wrapper() diff --git a/charmcraft/utils/__init__.py b/charmcraft/utils/__init__.py index d06faa241..48269b186 100644 --- a/charmcraft/utils/__init__.py +++ b/charmcraft/utils/__init__.py @@ -46,7 +46,13 @@ get_os_platform, validate_architectures, ) -from charmcraft.utils.file import S_IRALL, S_IXALL, make_executable, useful_filepath, build_zip +from charmcraft.utils.file import ( + S_IRALL, + S_IXALL, + make_executable, + useful_filepath, + build_zip, +) from charmcraft.utils.package import ( get_pypi_packages, PACKAGE_LINE_REGEX, @@ -57,7 +63,10 @@ get_requirements_file_package_names, validate_strict_dependencies, ) -from charmcraft.utils.parts import extend_python_build_environment, get_charm_copy_commands +from charmcraft.utils.parts import ( + extend_python_build_environment, + get_charm_copy_commands, +) from charmcraft.utils.project import ( find_charm_sources, get_charm_name_from_path, diff --git a/charmcraft/utils/charmlibs.py b/charmcraft/utils/charmlibs.py index 6985f2619..1c0d24554 100644 --- a/charmcraft/utils/charmlibs.py +++ b/charmcraft/utils/charmlibs.py @@ -117,15 +117,18 @@ def _api_patch_validator(value): simple_fields = { "LIBAPI": ( _api_patch_validator, - _msg_prefix + "LIBAPI must be a constant assignment of zero or a positive integer.", + _msg_prefix + + "LIBAPI must be a constant assignment of zero or a positive integer.", ), "LIBPATCH": ( _api_patch_validator, - _msg_prefix + "LIBPATCH must be a constant assignment of zero or a positive integer.", + _msg_prefix + + "LIBPATCH must be a constant assignment of zero or a positive integer.", ), "LIBID": ( lambda value: isinstance(value, str) and value and value.isascii(), - _msg_prefix + "LIBID must be a constant assignment of a non-empty ASCII string.", + _msg_prefix + + "LIBID must be a constant assignment of a non-empty ASCII string.", ), } pydeps_error = _msg_prefix + "PYDEPS must be a constant list of non-empty strings" @@ -224,7 +227,9 @@ def get_lib_module_name(charm: str, lib_name: str, api: int) -> str: def get_lib_info(*, full_name: str) -> LibData: ... @overload def get_lib_info(*, lib_path: pathlib.Path) -> LibData: ... -def get_lib_info(*, full_name: str | None = None, lib_path: pathlib.Path | None = None) -> LibData: +def get_lib_info( + *, full_name: str | None = None, lib_path: pathlib.Path | None = None +) -> LibData: """Get the whole lib info from the path/file. This will perform mutation of the charm name to create importable paths. @@ -267,7 +272,9 @@ def get_lib_info(*, full_name: str | None = None, lib_path: pathlib.Path | None charm_name = create_charm_name_from_importable(importable_charm_name) if v_api[0] != "v" or not v_api[1:].isdigit(): - raise CraftError("The API version in the library path must be 'vN' where N is an integer.") + raise CraftError( + "The API version in the library path must be 'vN' where N is an integer." + ) api_from_path = int(v_api[1:]) lib_name = lib_path.stem diff --git a/charmcraft/utils/cli.py b/charmcraft/utils/cli.py index 1f1044ec3..5165a4e73 100644 --- a/charmcraft/utils/cli.py +++ b/charmcraft/utils/cli.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """CLI-related utilities for Charmcraft.""" + import datetime import enum import json @@ -80,9 +81,7 @@ def __call__(self, value): else: if revision >= 0: return ResourceOption(name, revision) - msg = ( - "the resource format must be : (revision being a non-negative integer)" - ) + msg = "the resource format must be : (revision being a non-negative integer)" raise ValueError(msg) @@ -182,7 +181,9 @@ class OutputFormat(enum.Enum): @overload -def format_content(content: dict[str, str], fmt: Literal[OutputFormat.TABLE, "table"]) -> str: ... +def format_content( + content: dict[str, str], fmt: Literal[OutputFormat.TABLE, "table"] +) -> str: ... @overload diff --git a/charmcraft/utils/file.py b/charmcraft/utils/file.py index a88c73936..e517012ec 100644 --- a/charmcraft/utils/file.py +++ b/charmcraft/utils/file.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """File-related utilities.""" + import io import os import pathlib diff --git a/charmcraft/utils/package.py b/charmcraft/utils/package.py index 80d231606..e90946c62 100644 --- a/charmcraft/utils/package.py +++ b/charmcraft/utils/package.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Utilities related to Python packages.""" + import pathlib import re import string @@ -121,7 +122,11 @@ def get_pip_command( source_only_packages = sorted( get_package_names(all_packages) - get_package_names(binary_packages) ) - no_binary = [f"--no-binary={','.join(source_only_packages)}"] if source_only_packages else () + no_binary = ( + [f"--no-binary={','.join(source_only_packages)}"] + if source_only_packages + else () + ) return [ *prefix, @@ -133,7 +138,9 @@ def get_pip_command( def get_pip_version(pip_cmd: str) -> tuple[int, ...]: """Get the version of pip available from a specific pip command.""" - result = subprocess.run([pip_cmd, "--version"], text=True, capture_output=True, check=True) + result = subprocess.run( + [pip_cmd, "--version"], text=True, capture_output=True, check=True + ) version_data = result.stdout.split(" ") if len(version_data) < 2: raise ValueError("Unknown pip version") diff --git a/charmcraft/utils/parts.py b/charmcraft/utils/parts.py index 5facd433c..11583aa3e 100644 --- a/charmcraft/utils/parts.py +++ b/charmcraft/utils/parts.py @@ -32,7 +32,9 @@ def extend_python_build_environment(environment: dict[str, str]) -> dict[str, st } -def get_charm_copy_commands(build_dir: pathlib.Path, install_dir: pathlib.Path) -> Collection[str]: +def get_charm_copy_commands( + build_dir: pathlib.Path, install_dir: pathlib.Path +) -> Collection[str]: """Get the commands to copy charm source and charmlibs into the install directory. The commands will only be included if the relevant directories exist. @@ -43,8 +45,12 @@ def get_charm_copy_commands(build_dir: pathlib.Path, install_dir: pathlib.Path) commands = [] if src_dir.exists(): - commands.append(shlex.join([*copy_command_base, str(src_dir), str(install_dir)])) + commands.append( + shlex.join([*copy_command_base, str(src_dir), str(install_dir)]) + ) if libs_dir.exists(): - commands.append(shlex.join([*copy_command_base, str(libs_dir), str(install_dir)])) + commands.append( + shlex.join([*copy_command_base, str(libs_dir), str(install_dir)]) + ) return commands diff --git a/charmcraft/utils/platform.py b/charmcraft/utils/platform.py index b5cd6da33..ab0836438 100644 --- a/charmcraft/utils/platform.py +++ b/charmcraft/utils/platform.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Platform-related Charmcraft utilities.""" + import dataclasses import pathlib import platform @@ -35,7 +36,9 @@ class OSPlatform: machine: str -def get_os_platform(filepath: pathlib.Path = pathlib.Path("/etc/os-release")) -> OSPlatform: +def get_os_platform( + filepath: pathlib.Path = pathlib.Path("/etc/os-release"), +) -> OSPlatform: """Determine a system/release combo for an OS using /etc/os-release if available.""" system = platform.system() release = platform.release() @@ -52,7 +55,9 @@ def get_os_platform(filepath: pathlib.Path = pathlib.Path("/etc/os-release")) -> return OSPlatform(system=system, release=release, machine=machine) -def validate_architectures(architectures: Iterable[str], *, allow_all: bool = False) -> None: +def validate_architectures( + architectures: Iterable[str], *, allow_all: bool = False +) -> None: """Validate that all architectures provided are valid architecture names.""" architectures = set(architectures) if allow_all and "all" in architectures: diff --git a/charmcraft/utils/project.py b/charmcraft/utils/project.py index 4c39591ec..68292b65d 100644 --- a/charmcraft/utils/project.py +++ b/charmcraft/utils/project.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Charm project related utilities.""" + import itertools import os import pathlib @@ -50,13 +51,17 @@ def find_charm_sources( lambda p: (p / const.CHARMCRAFT_FILENAME).exists(), outer_potential_paths ) for path in potential_paths: - if path in charm_paths.values(): # Symlinks can cause ignorable duplicate paths. + if ( + path in charm_paths.values() + ): # Symlinks can cause ignorable duplicate paths. continue try: charm_name = get_charm_name_from_path(path) except InvalidCharmPathError: continue - if charm_name not in charm_names: # We only care if the charm is listed for finding + if ( + charm_name not in charm_names + ): # We only care if the charm is listed for finding continue if charm_name != path.name: emit.verbose(f"Charm {charm_name!r} found in non-matching path {path}") diff --git a/charmcraft/utils/skopeo.py b/charmcraft/utils/skopeo.py index 3bdaebb06..bb0d15c68 100644 --- a/charmcraft/utils/skopeo.py +++ b/charmcraft/utils/skopeo.py @@ -70,7 +70,9 @@ def get_global_command(self) -> list[str]: command.append("--debug") return command - def _run_skopeo(self, command: Sequence[str], **kwargs) -> subprocess.CompletedProcess: + def _run_skopeo( + self, command: Sequence[str], **kwargs + ) -> subprocess.CompletedProcess: """Run skopeo, converting the error message if necessary.""" try: return subprocess.run(command, check=True, **kwargs) @@ -121,7 +123,12 @@ def copy( @overload def inspect( - self, image: str, *, format_template: None = None, raw: bool = False, tags: bool = True + self, + image: str, + *, + format_template: None = None, + raw: bool = False, + tags: bool = True, ) -> dict[str, Any]: ... @overload def inspect( diff --git a/charmcraft/utils/store.py b/charmcraft/utils/store.py index d1c69d7cb..bbe286a1f 100644 --- a/charmcraft/utils/store.py +++ b/charmcraft/utils/store.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Store helper utilities.""" + from collections.abc import Iterable from craft_store import endpoints @@ -24,6 +25,12 @@ def get_packages( ) -> list[endpoints.Package]: """Get a list of packages from charms and bundles.""" return [ - *(endpoints.Package(package_type="charm", package_name=charm) for charm in charms), - *(endpoints.Package(package_type="bundle", package_name=bundle) for bundle in bundles), + *( + endpoints.Package(package_type="charm", package_name=charm) + for charm in charms + ), + *( + endpoints.Package(package_type="bundle", package_name=bundle) + for bundle in bundles + ), ] diff --git a/charmcraft/utils/yaml.py b/charmcraft/utils/yaml.py index 13ac9b255..fe1ffa684 100644 --- a/charmcraft/utils/yaml.py +++ b/charmcraft/utils/yaml.py @@ -49,7 +49,9 @@ def dump_yaml(data: Any) -> str: # noqa: ANN401: yaml.dump takes anything, so w """Dump a craft model to a YAML string.""" yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) yaml.add_representer( - pydantic.AnyHttpUrl, _repr_str, Dumper=yaml.SafeDumper # type: ignore[arg-type] + pydantic.AnyHttpUrl, + _repr_str, + Dumper=yaml.SafeDumper, # type: ignore[arg-type] ) yaml.add_representer( const.CharmArch, diff --git a/tests/commands/test_store_api.py b/tests/commands/test_store_api.py index 11a22d9a4..4c9cc722f 100644 --- a/tests/commands/test_store_api.py +++ b/tests/commands/test_store_api.py @@ -52,7 +52,10 @@ def client_mock(monkeypatch): """Fixture to provide a mocked client.""" monkeypatch.setattr(platform, "node", lambda: "fake-host") client_mock = MagicMock(spec=Client) - with patch("charmcraft.store.store.Client", lambda api, storage, ephemeral=True: client_mock): + with patch( + "charmcraft.store.store.Client", + lambda api, storage, ephemeral=True: client_mock, + ): yield client_mock @@ -287,14 +290,16 @@ def test_auth_bad_credentials(charmhub_config, monkeypatch): Store(charmhub_config) assert ( - str(error.value) == "Credentials could not be parsed. Expected base64 encoded credentials." + str(error.value) + == "Credentials could not be parsed. Expected base64 encoded credentials." ) def test_no_keyring(charmhub_config): """Verify CraftStore is raised from Store when no keyring is available.""" with patch( - "craft_store.StoreClient.__init__", side_effect=craft_store.errors.NoKeyringError() + "craft_store.StoreClient.__init__", + side_effect=craft_store.errors.NoKeyringError(), ): with pytest.raises(CraftError) as error: Store(charmhub_config) @@ -497,7 +502,9 @@ def test_register_name(client_mock, charmhub_config): result = store.register_name("testname", "stuff") assert client_mock.mock_calls == [ - call.request_urlpath_json("POST", "/v1/charm", json={"name": "testname", "type": "stuff"}), + call.request_urlpath_json( + "POST", "/v1/charm", json={"name": "testname", "type": "stuff"} + ), ] assert result is None @@ -512,7 +519,9 @@ def test_register_name_unauthorized_logs_in(client_mock, charmhub_config): store.register_name("testname", "stuff") assert client_mock.mock_calls == [ - call.request_urlpath_json("POST", "/v1/charm", json={"name": "testname", "type": "stuff"}), + call.request_urlpath_json( + "POST", "/v1/charm", json={"name": "testname", "type": "stuff"} + ), call.logout(), call.login( ttl=108000, @@ -524,7 +533,9 @@ def test_register_name_unauthorized_logs_in(client_mock, charmhub_config): "package-view", ], ), - call.request_urlpath_json("POST", "/v1/charm", json={"name": "testname", "type": "stuff"}), + call.request_urlpath_json( + "POST", "/v1/charm", json={"name": "testname", "type": "stuff"} + ), ] @@ -546,9 +557,13 @@ def test_unregister_name_success(client_mock, charmhub_config): id="unknown_name", ), pytest.param( - FakeResponse("discharge required", 401), StoreServerError, id="discharge_required" + FakeResponse("discharge required", 401), + StoreServerError, + id="discharge_required", + ), + pytest.param( + FakeResponse("Unauthorized", 401), StoreServerError, id="Unauthorized" ), - pytest.param(FakeResponse("Unauthorized", 401), StoreServerError, id="Unauthorized"), pytest.param( FakeResponse("Cannot unregister a package with existing revisions", 403), CraftError, @@ -588,7 +603,9 @@ def test_unregister_name_errors( ), ], ) -def test_unregister_name_login(client_mock, charmhub_config, http_response: FakeResponse): +def test_unregister_name_login( + client_mock, charmhub_config, http_response: FakeResponse +): """Retry login when registering a name.""" client_mock.unregister_name.side_effect = [StoreServerError(http_response), None] @@ -713,7 +730,9 @@ def test_upload_straightforward(client_mock, emitter, charmhub_config): test_revision = 123 test_status_ok = "test-status" status_response = { - "revisions": [{"status": test_status_ok, "revision": test_revision, "errors": None}] + "revisions": [ + {"status": test_status_ok, "revision": test_revision, "errors": None} + ] } client_mock.request_urlpath_json.side_effect = [ @@ -732,7 +751,9 @@ def test_upload_straightforward(client_mock, emitter, charmhub_config): assert client_mock.mock_calls == [ call.whoami(), call.push_file(test_filepath), - call.request_urlpath_json("POST", test_endpoint, json={"upload-id": test_upload_id}), + call.request_urlpath_json( + "POST", test_endpoint, json={"upload-id": test_upload_id} + ), call.request_urlpath_json("GET", test_status_url), ] @@ -772,7 +793,9 @@ def test_upload_polls_status_ok(client_mock, emitter, charmhub_config): "revisions": [{"status": "more-revisions", "revision": None, "errors": None}] } status_response_3 = { - "revisions": [{"status": test_status_ok, "revision": test_revision, "errors": None}] + "revisions": [ + {"status": test_status_ok, "revision": test_revision, "errors": None} + ] } client_mock.request_urlpath_json.side_effect = [ {"status-url": test_status_url}, @@ -917,7 +940,9 @@ def test_upload_resources_endpoint(charmhub_config): with patch.object(store, "_upload") as mock: mock.return_value = test_results - result = store.upload_resource("test-charm", "test-resource", "test-type", "test-filepath") + result = store.upload_resource( + "test-charm", "test-resource", "test-type", "test-filepath" + ) expected_endpoint = "/v1/charm/test-charm/resources/test-resource/revisions" mock.assert_called_once_with( expected_endpoint, @@ -942,7 +967,9 @@ def test_upload_including_extra_parameters(client_mock, emitter, charmhub_config test_revision = 123 test_status_ok = "test-status" status_response = { - "revisions": [{"status": test_status_ok, "revision": test_revision, "errors": None}] + "revisions": [ + {"status": test_status_ok, "revision": test_revision, "errors": None} + ] } client_mock.request_urlpath_json.side_effect = [ @@ -985,7 +1012,9 @@ def test_list_revisions_ok(client_mock, charmhub_config): "created-at": "2020-06-29T22:11:00.123", "status": "approved", "errors": None, - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], } ] } @@ -1032,7 +1061,9 @@ def test_list_revisions_errors(client_mock, charmhub_config): {"message": "error text 1", "code": "error-code-1"}, {"message": "error text 2", "code": "error-code-2"}, ], - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], } ] } @@ -1063,7 +1094,9 @@ def test_list_revisions_several_mixed(client_mock, charmhub_config): "errors": [ {"message": "error", "code": "code"}, ], - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], }, { "revision": 2, @@ -1071,7 +1104,9 @@ def test_list_revisions_several_mixed(client_mock, charmhub_config): "created-at": "2020-06-29T22:11:02", "status": "approved", "errors": None, - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], }, ] } @@ -1126,7 +1161,9 @@ def test_release_simple(client_mock, charmhub_config): expected_body = [{"revision": 123, "channel": "somechannel", "resources": []}] assert client_mock.mock_calls == [ - call.request_urlpath_json("POST", "/v1/charm/testname/releases", json=expected_body), + call.request_urlpath_json( + "POST", "/v1/charm/testname/releases", json=expected_body + ), ] @@ -1141,7 +1178,9 @@ def test_release_multiple_channels(client_mock, charmhub_config): {"revision": 123, "channel": "channel3", "resources": []}, ] assert client_mock.mock_calls == [ - call.request_urlpath_json("POST", "/v1/charm/testname/releases", json=expected_body), + call.request_urlpath_json( + "POST", "/v1/charm/testname/releases", json=expected_body + ), ] @@ -1171,7 +1210,9 @@ def test_release_with_resources(client_mock, charmhub_config): }, ] assert client_mock.mock_calls == [ - call.request_urlpath_json("POST", "/v1/charm/testname/releases", json=expected_body), + call.request_urlpath_json( + "POST", "/v1/charm/testname/releases", json=expected_body + ), ] @@ -1226,7 +1267,9 @@ def test_status_ok(client_mock, charmhub_config): "created-at": "2020-06-29T22:11:05", "status": "approved", "errors": None, - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], }, { "revision": 10, @@ -1234,7 +1277,9 @@ def test_status_ok(client_mock, charmhub_config): "created-at": "2020-06-29T22:11:10", "status": "approved", "errors": None, - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], }, ], } @@ -1360,7 +1405,9 @@ def test_status_with_resources(client_mock, charmhub_config): "created-at": "2020-06-29T22:11:05", "status": "approved", "errors": None, - "bases": [{"architecture": "amd64", "channel": "20.04", "name": "ubuntu"}], + "bases": [ + {"architecture": "amd64", "channel": "20.04", "name": "ubuntu"} + ], }, ], } @@ -1814,7 +1861,8 @@ def test_get_oci_registry_credentials(client_mock, charmhub_config): assert client_mock.mock_calls == [ call.request_urlpath_json( - "GET", "/v1/charm/charm-name/resources/resource-name/oci-image/upload-credentials" + "GET", + "/v1/charm/charm-name/resources/resource-name/oci-image/upload-credentials", ) ] assert result.image_name == "test-image-name" @@ -1826,7 +1874,9 @@ def test_get_oci_image_blob(client_mock, charmhub_config): """Get the blob generated by Charmhub to refer to the OCI image.""" store = Store(charmhub_config) client_mock.request_urlpath_text.return_value = "some opaque stuff" - result = store.get_oci_image_blob("charm-name", "resource-name", "a-very-specific-digest") + result = store.get_oci_image_blob( + "charm-name", "resource-name", "a-very-specific-digest" + ) assert client_mock.mock_calls == [ call.request_urlpath_text( diff --git a/tests/commands/test_store_client.py b/tests/commands/test_store_client.py index 392e23c4b..4722254e0 100644 --- a/tests/commands/test_store_client.py +++ b/tests/commands/test_store_client.py @@ -41,7 +41,9 @@ def test_useragent_linux(monkeypatch): """Construct a user-agent as a patched Linux machine""" monkeypatch.setenv("TRAVIS_TESTING", "1") - os_platform = OSPlatform(system="Arch Linux", release="5.10.10-arch1-1", machine="x86_64") + os_platform = OSPlatform( + system="Arch Linux", release="5.10.10-arch1-1", machine="x86_64" + ) with ( patch("charmcraft.store.client.__version__", "1.2.3"), patch("charmcraft.utils.get_os_platform", return_value=os_platform), @@ -50,7 +52,10 @@ def test_useragent_linux(monkeypatch): patch("platform.python_version", return_value="3.9.1"), ): ua = build_user_agent() - assert ua == "charmcraft/1.2.3 (testing) Arch Linux/5.10.10-arch1-1 (x86_64) python/3.9.1" + assert ( + ua + == "charmcraft/1.2.3 (testing) Arch Linux/5.10.10-arch1-1 (x86_64) python/3.9.1" + ) def test_useragent_windows(monkeypatch): @@ -182,7 +187,9 @@ def test_client_request_text_error(client_class): """Hits the server in text mode, getting an error.""" client = client_class("http://api.test", "http://storage.test") original_error_text = "bad bad server" - client.request_mock.side_effect = craft_store.errors.CraftStoreError(original_error_text) + client.request_mock.side_effect = craft_store.errors.CraftStoreError( + original_error_text + ) with pytest.raises(craft_store.errors.CraftStoreError) as cm: client.request_urlpath_text("GET", "/somepath") @@ -193,7 +200,9 @@ def test_client_request_json_error(client_class): """Hits the server in json mode, getting an error.""" client = client_class("http://api.test", "http://storage.test") original_error_text = "bad bad server" - client.request_mock.side_effect = craft_store.errors.CraftStoreError(original_error_text) + client.request_mock.side_effect = craft_store.errors.CraftStoreError( + original_error_text + ) with pytest.raises(craft_store.errors.CraftStoreError) as cm: client.request_urlpath_json("GET", "/somepath") @@ -209,7 +218,9 @@ def test_client_hit_success_withbody(client_class): result = client.request_urlpath_text("GET", "/somepath", "somebody") - assert client.request_mock.mock_calls == [call("GET", "http://api.test/somepath", "somebody")] + assert client.request_mock.mock_calls == [ + call("GET", "http://api.test/somepath", "somebody") + ] assert result == response_value @@ -303,16 +314,16 @@ def test_client_push_response_unsuccessful(tmp_path, client_class): with patch.object(client, "_storage_push", return_value=fake_response): with pytest.raises(CraftError) as error: client.push_file(test_filepath) - expected_error = ( - "Server error while pushing file: {'successful': False, 'upload_id': None}" - ) + expected_error = "Server error while pushing file: {'successful': False, 'upload_id': None}" assert str(error.value) == expected_error def test_storage_push_succesful(client_class): """Bytes are properly pushed to the Storage.""" test_monitor = MultipartEncoderMonitor( - MultipartEncoder(fields={"binary": ("filename", "somefile", "application/octet-stream")}) + MultipartEncoder( + fields={"binary": ("filename", "somefile", "application/octet-stream")} + ) ) client = client_class("http://api.test", "http://test.url:0000") @@ -335,9 +346,7 @@ def test_alternate_auth_login_forbidden(client_class, monkeypatch): client = client_class("http://api.test", "http://storage.test") with pytest.raises(CraftError) as cm: client.login() - expected_error = ( - "Cannot login when using alternative auth through CHARMCRAFT_AUTH environment variable." - ) + expected_error = "Cannot login when using alternative auth through CHARMCRAFT_AUTH environment variable." assert str(cm.value) == expected_error @@ -347,9 +356,7 @@ def test_alternate_auth_logout_forbidden(client_class, monkeypatch): client = client_class("http://api.test", "http://storage.test") with pytest.raises(CraftError) as cm: client.logout() - expected_error = ( - "Cannot logout when using alternative auth through CHARMCRAFT_AUTH environment variable." - ) + expected_error = "Cannot logout when using alternative auth through CHARMCRAFT_AUTH environment variable." assert str(cm.value) == expected_error @@ -373,12 +380,16 @@ def test_anonymous_client_request_success_simple(): """Hits the server, all ok.""" response_value = {"foo": "bar"} fake_response = FakeResponse(content=json.dumps(response_value), status_code=200) - with patch("craft_store.http_client.HTTPClient.request") as mock_http_client_request: + with patch( + "craft_store.http_client.HTTPClient.request" + ) as mock_http_client_request: mock_http_client_request.return_value = fake_response client = AnonymousClient("http://api.test", "http://storage.test") result = client.request_urlpath_json("GET", "/somepath") - assert mock_http_client_request.mock_calls == [call("GET", "http://api.test/somepath")] + assert mock_http_client_request.mock_calls == [ + call("GET", "http://api.test/somepath") + ] assert result == response_value @@ -386,18 +397,24 @@ def test_anonymous_client_request_success_without_json_parsing(): """Hits the server, all ok, return the raw response without parsing the json.""" response_value = "whatever test response" fake_response = FakeResponse(content=response_value, status_code=200) - with patch("craft_store.http_client.HTTPClient.request") as mock_http_client_request: + with patch( + "craft_store.http_client.HTTPClient.request" + ) as mock_http_client_request: client = AnonymousClient("http://api.test", "http://storage.test") mock_http_client_request.return_value = fake_response result = client.request_urlpath_text("GET", "/somepath") - assert mock_http_client_request.mock_calls == [call("GET", "http://api.test/somepath")] + assert mock_http_client_request.mock_calls == [ + call("GET", "http://api.test/somepath") + ] assert result == response_value def test_anonymous_client_request_text_error(): """Hits the server in text mode, getting an error.""" - with patch("craft_store.http_client.HTTPClient.request") as mock_http_client_request: + with patch( + "craft_store.http_client.HTTPClient.request" + ) as mock_http_client_request: original_error_text = "bad bad server" mock_http_client_request.side_effect = craft_store.errors.CraftStoreError( original_error_text @@ -412,7 +429,9 @@ def test_anonymous_client_request_text_error(): def test_anonymous_client_request_json_error(): """Hits the server in json mode, getting an error.""" - with patch("craft_store.http_client.HTTPClient.request") as mock_http_client_request: + with patch( + "craft_store.http_client.HTTPClient.request" + ) as mock_http_client_request: original_error_text = "bad bad server" mock_http_client_request.side_effect = craft_store.errors.CraftStoreError( original_error_text @@ -429,7 +448,9 @@ def test_anonymous_client_hit_success_withbody(): """Hits the server including a body, all ok.""" response_value = {"foo": "bar"} fake_response = FakeResponse(content=response_value, status_code=200) - with patch("craft_store.http_client.HTTPClient.request") as mock_http_client_request: + with patch( + "craft_store.http_client.HTTPClient.request" + ) as mock_http_client_request: mock_http_client_request.return_value = fake_response client = AnonymousClient("http://api.test", "http://storage.test") diff --git a/tests/conftest.py b/tests/conftest.py index 133a89cc9..455b71709 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,7 +63,13 @@ def simple_charm(basic_charm_dict: dict[str, Any]): "architectures": [util.get_host_architecture()], } ], - "run-on": [{"name": "ubuntu", "channel": "22.04", "architectures": ["arm64"]}], + "run-on": [ + { + "name": "ubuntu", + "channel": "22.04", + "architectures": ["arm64"], + } + ], } ], } @@ -317,7 +323,9 @@ def helper(*match_lines): for match_line in match_lines: if match_line not in printed_lines: printed_repr = "\n".join(map(repr, printed_lines)) - pytest.fail(f"Line {match_line!r} not found in the output found:\n{printed_repr}") + pytest.fail( + f"Line {match_line!r} not found in the output found:\n{printed_repr}" + ) return helper @@ -377,7 +385,9 @@ def charm_plugin(tmp_path): ) part_info = craft_parts.PartInfo(project_info=project_info, part=part) - return plugins.get_plugin(part=part, part_info=part_info, properties=plugin_properties) + return plugins.get_plugin( + part=part, part_info=part_info, properties=plugin_properties + ) @pytest.fixture @@ -399,7 +409,9 @@ def bundle_plugin(tmp_path): ) part_info = craft_parts.PartInfo(project_info=project_info, part=part) - return plugins.get_plugin(part=part, part_info=part_info, properties=plugin_properties) + return plugins.get_plugin( + part=part, part_info=part_info, properties=plugin_properties + ) @pytest.fixture diff --git a/tests/extensions/test_app.py b/tests/extensions/test_app.py index 2ddd26429..39de39c60 100644 --- a/tests/extensions/test_app.py +++ b/tests/extensions/test_app.py @@ -307,7 +307,9 @@ def flask_input_yaml_fixture(): ), ], ) -def test_apply_extensions_correct(monkeypatch, experimental, tmp_path, input_yaml, expected): +def test_apply_extensions_correct( + monkeypatch, experimental, tmp_path, input_yaml, expected +): if experimental: monkeypatch.setenv("CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS", "1") diff --git a/tests/extensions/test_extensions.py b/tests/extensions/test_extensions.py index 794b09d98..8a6770cbd 100644 --- a/tests/extensions/test_extensions.py +++ b/tests/extensions/test_extensions.py @@ -128,8 +128,12 @@ def test_experimental_no_env(fake_extensions, tmp_path): "description": "test description", "bases": [ { - "build-on": [{"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]}], - "run-on": [{"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]}], + "build-on": [ + {"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]} + ], + "run-on": [ + {"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]} + ], } ], "extensions": [ExperimentalExtension.name], @@ -149,8 +153,12 @@ def test_wrong_base(fake_extensions, tmp_path): "description": "test description", "bases": [ { - "build-on": [{"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]}], - "run-on": [{"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]}], + "build-on": [ + {"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]} + ], + "run-on": [ + {"name": "ubuntu", "channel": "20.04", "architectures": ["amd64"]} + ], } ], "extensions": [FakeExtension.name], @@ -188,7 +196,13 @@ def test_apply_extensions(fake_extensions, tmp_path): "description": "test description", "bases": [{"name": "ubuntu", "channel": "22.04"}], "extensions": [FullExtension.name], - "parts": {"my-part": {"plugin": "nil", "source": None, "stage-packages": ["old-package"]}}, + "parts": { + "my-part": { + "plugin": "nil", + "source": None, + "stage-packages": ["old-package"], + } + }, } applied = extensions.apply_extensions(tmp_path, charmcraft_config) diff --git a/tests/extensions/test_registry.py b/tests/extensions/test_registry.py index b4cf9f303..50647b4ac 100644 --- a/tests/extensions/test_registry.py +++ b/tests/extensions/test_registry.py @@ -85,7 +85,11 @@ def test_get_extension_class_error(fake_extensions): def test_get_extensions(fake_extensions): assert extensions.get_extensions() == [ - {"name": "fake-extension-1", "bases": [("ubuntu@22.04")], "experimental_bases": []}, + { + "name": "fake-extension-1", + "bases": [("ubuntu@22.04")], + "experimental_bases": [], + }, { "name": "fake-extension-2", "bases": [("ubuntu@22.04")], diff --git a/tests/integration/commands/test_analyse.py b/tests/integration/commands/test_analyse.py index 69a4bf9ef..6c4474c6f 100644 --- a/tests/integration/commands/test_analyse.py +++ b/tests/integration/commands/test_analyse.py @@ -63,7 +63,9 @@ def test_corrupt_charm(new_path, config): args = Namespace(filepath=charm_file, force=None, format=None, ignore=None) with pytest.raises(CraftError) as cm: Analyse(config).run(args) - assert str(cm.value) == (f"Cannot open charm file '{charm_file}': File is not a zip file") + assert str(cm.value) == ( + f"Cannot open charm file '{charm_file}': File is not a zip file" + ) def create_a_valid_zip(tmp_path): @@ -88,7 +90,13 @@ def test_integration_linters(new_path, emitter, config, monkeypatch): @pytest.mark.parametrize("indicated_format", [None, "json"]) def test_complete_set_of_results( - check, emitter, service_factory, config, monkeypatch, fake_project_dir, indicated_format + check, + emitter, + service_factory, + config, + monkeypatch, + fake_project_dir, + indicated_format, ): """Show a complete basic case of results.""" # fake results from the analyzer @@ -145,7 +153,9 @@ def test_complete_set_of_results( ] fake_charm = create_a_valid_zip(fake_project_dir) - args = Namespace(filepath=fake_charm, force=None, format=indicated_format, ignore=None) + args = Namespace( + filepath=fake_charm, force=None, format=indicated_format, ignore=None + ) monkeypatch.setattr( service_factory.analysis, "lint_directory", lambda *a, **k: linting_results ) @@ -220,7 +230,9 @@ def test_complete_set_of_results( assert expected == json.loads(text) -def test_only_attributes(emitter, service_factory, config, monkeypatch, fake_project_dir): +def test_only_attributes( + emitter, service_factory, config, monkeypatch, fake_project_dir +): """Show only attribute results (the rest may be ignored).""" # fake results from the analyzer linting_results = [ @@ -240,7 +252,9 @@ def test_only_attributes(emitter, service_factory, config, monkeypatch, fake_pro ) retcode = Analyse(config).run(args) - emitter.assert_progress("check-attribute: [CHECK-RESULT] text (url)", permanent=True) + emitter.assert_progress( + "check-attribute: [CHECK-RESULT] text (url)", permanent=True + ) assert retcode == 0 @@ -292,7 +306,9 @@ def test_only_errors(emitter, service_factory, config, monkeypatch, fake_project assert retcode == 2 -def test_both_errors_and_warnings(emitter, service_factory, config, monkeypatch, fake_project_dir): +def test_both_errors_and_warnings( + emitter, service_factory, config, monkeypatch, fake_project_dir +): """Show error and warnings results.""" # fake results from the analyzer linting_results = [ diff --git a/tests/integration/commands/test_extensions.py b/tests/integration/commands/test_extensions.py index 3569f991a..c0aa1e321 100644 --- a/tests/integration/commands/test_extensions.py +++ b/tests/integration/commands/test_extensions.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for extension commands.""" + import argparse import textwrap @@ -41,7 +42,8 @@ def is_experimental(base: tuple[str, str] | None) -> bool: @pytest.fixture(autouse=True, scope="module") def registered_extensions(): default_extensions = { - name: extensions.get_extension_class(name) for name in extensions.get_extension_names() + name: extensions.get_extension_class(name) + for name in extensions.get_extension_names() } for ext in default_extensions: extensions.unregister(ext) diff --git a/tests/integration/commands/test_init.py b/tests/integration/commands/test_init.py index a4f632809..5bee96aef 100644 --- a/tests/integration/commands/test_init.py +++ b/tests/integration/commands/test_init.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for init command.""" + import argparse import contextlib import os @@ -97,7 +98,9 @@ @pytest.fixture def init_command(): - return commands.InitCommand({"app": charmcraft.application.APP_METADATA, "services": None}) + return commands.InitCommand( + {"app": charmcraft.application.APP_METADATA, "services": None} + ) def create_namespace( @@ -148,7 +151,9 @@ def test_files_created_correct( tox_ini = (new_path / "tox.ini").read_text(encoding="utf-8") pytest_check.equal(actual_files, expected_files) - pytest_check.is_true(re.search(rf"^name: {charm_name}$", charmcraft_yaml, re.MULTILINE)) + pytest_check.is_true( + re.search(rf"^name: {charm_name}$", charmcraft_yaml, re.MULTILINE) + ) pytest_check.is_true(re.search(rf"^# Copyright \d+ {author}", tox_ini)) @@ -204,7 +209,9 @@ def test_gecos_valid_author(monkeypatch, new_path, init_command, author): ), ], ) -def test_gecos_user_not_found(monkeypatch, new_path, init_command, mock_getpwuid, error_msg): +def test_gecos_user_not_found( + monkeypatch, new_path, init_command, mock_getpwuid, error_msg +): monkeypatch.setattr(pwd, "getpwuid", mock_getpwuid) with pytest.raises(errors.CraftError, match=error_msg): @@ -311,7 +318,9 @@ def test_pep257(new_path, init_command, profile): errors = list(pydocstyle.check(python_paths, select=to_include)) if errors: - report = [f"Please fix files as suggested by pydocstyle ({len(errors):d} issues):"] + report = [ + f"Please fix files as suggested by pydocstyle ({len(errors):d} issues):" + ] report.extend(str(e) for e in errors) msg = "\n".join(report) pytest.fail(msg, pytrace=False) diff --git a/tests/integration/commands/test_pack.py b/tests/integration/commands/test_pack.py index 635bd9a76..fd9d64c51 100644 --- a/tests/integration/commands/test_pack.py +++ b/tests/integration/commands/test_pack.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Integration tests for packing.""" + import sys import zipfile @@ -27,7 +28,8 @@ @pytest.mark.xfail( - sys.platform != "linux", reason="https://github.com/canonical/charmcraft/issues/1552" + sys.platform != "linux", + reason="https://github.com/canonical/charmcraft/issues/1552", ) @pytest.mark.parametrize( ("bundle_yaml", "filename"), @@ -66,14 +68,19 @@ def test_build_basic_bundle(monkeypatch, capsys, app, new_path, bundle_yaml, fil { "build-on": [{"name": "ubuntu", "channel": "22.04"}], "run-on": [ - {"name": "ubuntu", "channel": "22.04", "architectures": ["amd64"]} + { + "name": "ubuntu", + "channel": "22.04", + "architectures": ["amd64"], + } ], } ], }, "ubuntu-22.04-amd64", marks=pytest.mark.skipif( - CURRENT_PLATFORM.release != "22.04", reason="Bases charm only tested on jammy." + CURRENT_PLATFORM.release != "22.04", + reason="Bases charm only tested on jammy.", ), id="bases-charm", ), @@ -85,13 +92,17 @@ def test_build_basic_bundle(monkeypatch, capsys, app, new_path, bundle_yaml, fil "description": "A charm for testing", "base": "ubuntu@22.04", "platforms": { - "ubuntu-22.04-amd64": {"build-on": ["amd64"], "build-for": ["amd64"]} + "ubuntu-22.04-amd64": { + "build-on": ["amd64"], + "build-for": ["amd64"], + } }, "parts": {}, }, "ubuntu-22.04-amd64", marks=pytest.mark.skipif( - CURRENT_PLATFORM.release != "22.04", reason="Jammy charms only tested on jammy" + CURRENT_PLATFORM.release != "22.04", + reason="Jammy charms only tested on jammy", ), id="platforms-jammy-charm", ), @@ -107,7 +118,8 @@ def test_build_basic_bundle(monkeypatch, capsys, app, new_path, bundle_yaml, fil }, util.get_host_architecture(), marks=pytest.mark.skipif( - CURRENT_PLATFORM.release != "22.04", reason="Jammy charms only tested on jammy" + CURRENT_PLATFORM.release != "22.04", + reason="Jammy charms only tested on jammy", ), id="platforms-jammy-basic", ), @@ -142,7 +154,8 @@ def test_build_basic_charm( monkeypatch.setenv("CRAFT_DEBUG", "1") monkeypatch.setattr( - "sys.argv", ["charmcraft", "pack", "--destructive-mode", f"--platform={platform}"] + "sys.argv", + ["charmcraft", "pack", "--destructive-mode", f"--platform={platform}"], ) app.configure({}) diff --git a/tests/integration/commands/test_resource_revisions.py b/tests/integration/commands/test_resource_revisions.py index 3948e3209..812a34aca 100644 --- a/tests/integration/commands/test_resource_revisions.py +++ b/tests/integration/commands/test_resource_revisions.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for resource-revisions command.""" + import datetime from argparse import Namespace from unittest import mock @@ -53,7 +54,9 @@ def test_resourcerevisions_simple(emitter, store_mock, config, formatted): CharmResourceRevision( revision=1, size=pydantic.ByteSize(50), - created_at=datetime.datetime(2020, 7, 3, 2, 30, 40, tzinfo=datetime.timezone.utc), + created_at=datetime.datetime( + 2020, 7, 3, 2, 30, 40, tzinfo=datetime.timezone.utc + ), bases=[ResponseCharmResourceBase()], name="testresource", sha256="", @@ -65,7 +68,9 @@ def test_resourcerevisions_simple(emitter, store_mock, config, formatted): ] store_mock.list_resource_revisions.return_value = store_response - args = Namespace(charm_name="testcharm", resource_name="testresource", format=formatted) + args = Namespace( + charm_name="testcharm", resource_name="testresource", format=formatted + ) ListResourceRevisionsCommand(config).run(args) assert store_mock.mock_calls == [ @@ -95,7 +100,9 @@ def test_resourcerevisions_empty(emitter, store_mock, config, formatted): store_response = [] store_mock.list_resource_revisions.return_value = store_response - args = Namespace(charm_name="testcharm", resource_name="testresource", format=formatted) + args = Namespace( + charm_name="testcharm", resource_name="testresource", format=formatted + ) ListResourceRevisionsCommand(config).run(args) if formatted: @@ -162,7 +169,9 @@ def test_resourcerevisions_ordered_by_revision(emitter, store_mock, config, form ] store_mock.list_resource_revisions.return_value = store_response - args = Namespace(charm_name="testcharm", resource_name="testresource", format=formatted) + args = Namespace( + charm_name="testcharm", resource_name="testresource", format=formatted + ) ListResourceRevisionsCommand(config).run(args) if formatted: @@ -183,9 +192,20 @@ def test_resourcerevisions_ordered_by_revision(emitter, store_mock, config, form "revision": 4, "created at": "2020-07-03T20:30:40+00:00", "size": 876543, - "bases": [{"name": "all", "channel": "all", "architectures": ["amd64", "arm64"]}], + "bases": [ + { + "name": "all", + "channel": "all", + "architectures": ["amd64", "arm64"], + } + ], + }, + { + "revision": 2, + "created at": "2020-07-03T20:30:40+00:00", + "size": 50, + "bases": [], }, - {"revision": 2, "created at": "2020-07-03T20:30:40+00:00", "size": 50, "bases": []}, ] emitter.assert_json_output(expected) else: diff --git a/tests/integration/commands/test_set_resource_architectures.py b/tests/integration/commands/test_set_resource_architectures.py index ae40e0502..0e6ba6974 100644 --- a/tests/integration/commands/test_set_resource_architectures.py +++ b/tests/integration/commands/test_set_resource_architectures.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Integration tests for set-resource-architectures command.""" + import argparse import textwrap @@ -50,7 +51,11 @@ def cmd(service_factory): [ get_fake_revision( revision=1, - bases=[models.ResponseCharmResourceBase(architectures=["amd64", "arm64"])], + bases=[ + models.ResponseCharmResourceBase( + architectures=["amd64", "arm64"] + ) + ], ), get_fake_revision( revision=2, @@ -69,7 +74,11 @@ def cmd(service_factory): [ get_fake_revision( revision=1, - bases=[models.ResponseCharmResourceBase(architectures=["amd64", "arm64"])], + bases=[ + models.ResponseCharmResourceBase( + architectures=["amd64", "arm64"] + ) + ], ), get_fake_revision( revision=2, diff --git a/tests/integration/commands/test_store_commands.py b/tests/integration/commands/test_store_commands.py index fd0af34db..3b1f12e59 100644 --- a/tests/integration/commands/test_store_commands.py +++ b/tests/integration/commands/test_store_commands.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Integration tests for store commands.""" + import argparse import sys from unittest import mock @@ -44,7 +45,9 @@ def validate_params(config, ephemeral=False, needs_auth=True): # region fetch-lib tests @pytest.mark.parametrize("formatted", [None, "json"]) -def test_fetchlib_simple_downloaded(emitter, store_mock, tmp_path, monkeypatch, config, formatted): +def test_fetchlib_simple_downloaded( + emitter, store_mock, tmp_path, monkeypatch, config, formatted +): """Happy path fetching the lib for the first time (downloading it).""" monkeypatch.chdir(tmp_path) @@ -101,7 +104,9 @@ def test_fetchlib_simple_downloaded(emitter, store_mock, tmp_path, monkeypatch, assert saved_file.read_text() == lib_content -def test_fetchlib_simple_dash_in_name(emitter, store_mock, tmp_path, monkeypatch, config): +def test_fetchlib_simple_dash_in_name( + emitter, store_mock, tmp_path, monkeypatch, config +): """Happy path fetching the lib for the first time (downloading it).""" monkeypatch.chdir(tmp_path) @@ -143,7 +148,9 @@ def test_fetchlib_simple_dash_in_name(emitter, store_mock, tmp_path, monkeypatch assert saved_file.read_text() == lib_content -def test_fetchlib_simple_dash_in_name_on_disk(emitter, store_mock, tmp_path, monkeypatch, config): +def test_fetchlib_simple_dash_in_name_on_disk( + emitter, store_mock, tmp_path, monkeypatch, config +): """Happy path fetching the lib for the first time (downloading it).""" monkeypatch.chdir(tmp_path) @@ -344,9 +351,11 @@ def test_fetchlib_store_not_found(emitter, store_mock, config, formatted): args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) FetchLibCommand(config).run(args) - store_mock.get_libraries_tips.assert_called_once_with( - [{"charm_name": "testcharm", "lib_name": "testlib", "api": 0}] - ), + ( + store_mock.get_libraries_tips.assert_called_once_with( + [{"charm_name": "testcharm", "lib_name": "testlib", "api": 0}] + ), + ) error_message = "Library charms.testcharm.v0.testlib not found in Charmhub." if formatted: expected = [ @@ -364,7 +373,9 @@ def test_fetchlib_store_not_found(emitter, store_mock, config, formatted): @pytest.mark.parametrize("formatted", [None, "json"]) -def test_fetchlib_store_is_old(emitter, store_mock, tmp_path, monkeypatch, config, formatted): +def test_fetchlib_store_is_old( + emitter, store_mock, tmp_path, monkeypatch, config, formatted +): """The store has an older version that what is found locally.""" monkeypatch.chdir(tmp_path) @@ -385,8 +396,12 @@ def test_fetchlib_store_is_old(emitter, store_mock, tmp_path, monkeypatch, confi args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) FetchLibCommand(config).run(args) - store_mock.get_libraries_tips.assert_called_once_with([{"lib_id": lib_id, "api": 0}]) - error_message = "Library charms.testcharm.v0.testlib has local changes, cannot be updated." + store_mock.get_libraries_tips.assert_called_once_with( + [{"lib_id": lib_id, "api": 0}] + ) + error_message = ( + "Library charms.testcharm.v0.testlib has local changes, cannot be updated." + ) if formatted: expected = [ { @@ -410,7 +425,9 @@ def test_fetchlib_store_same_versions_same_hash( monkeypatch.chdir(tmp_path) lib_id = "test-example-lib-id" - _, c_hash = factory.create_lib_filepath("testcharm", "testlib", api=0, patch=7, lib_id=lib_id) + _, c_hash = factory.create_lib_filepath( + "testcharm", "testlib", api=0, patch=7, lib_id=lib_id + ) store_mock.get_libraries_tips.return_value = { (lib_id, 0): Library( @@ -426,8 +443,12 @@ def test_fetchlib_store_same_versions_same_hash( args = argparse.Namespace(library="charms.testcharm.v0.testlib", format=formatted) FetchLibCommand(config).run(args) - store_mock.get_libraries_tips.assert_called_once_with([{"lib_id": lib_id, "api": 0}]) - error_message = "Library charms.testcharm.v0.testlib was already up to date in version 0.7." + store_mock.get_libraries_tips.assert_called_once_with( + [{"lib_id": lib_id, "api": 0}] + ) + error_message = ( + "Library charms.testcharm.v0.testlib was already up to date in version 0.7." + ) if formatted: expected = [ { @@ -470,7 +491,9 @@ def test_fetchlib_store_same_versions_different_hash( assert store_mock.mock_calls == [ mock.call.get_libraries_tips([{"lib_id": lib_id, "api": 0}]), ] - error_message = "Library charms.testcharm.v0.testlib has local changes, cannot be updated." + error_message = ( + "Library charms.testcharm.v0.testlib has local changes, cannot be updated." + ) if formatted: expected = [ { diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index bac94917d..f31f60df8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """General fixtures for integration tests.""" + import pathlib from typing import Any from unittest import mock @@ -37,7 +38,9 @@ def project_path(tmp_path: pathlib.Path): @pytest.fixture -def charm_project(basic_charm_dict: dict[str, Any], project_path: pathlib.Path, request): +def charm_project( + basic_charm_dict: dict[str, Any], project_path: pathlib.Path, request +): # Workaround for testing across systems. If we're not on Ubuntu, make an Ubuntu 24.04 charm. # If we are on Ubuntu, use the current version. distro_id = "ubuntu" @@ -53,7 +56,9 @@ def charm_project(basic_charm_dict: dict[str, Any], project_path: pathlib.Path, @pytest.fixture -def service_factory(new_path: pathlib.Path, charm_project, default_build_plan, project_path): +def service_factory( + new_path: pathlib.Path, charm_project, default_build_plan, project_path +): factory = services.CharmcraftServiceFactory(app=application.APP_METADATA) factory.store.client = mock.Mock(spec_set=craft_store.StoreClient) factory.project = charm_project diff --git a/tests/integration/parts/conftest.py b/tests/integration/parts/conftest.py index c45821932..b6b5b1757 100644 --- a/tests/integration/parts/conftest.py +++ b/tests/integration/parts/conftest.py @@ -22,7 +22,9 @@ from craft_application import models from craft_providers import bases -pytestmark = [pytest.mark.skipif(sys.platform != "linux", reason="craft-parts is linux-only")] +pytestmark = [ + pytest.mark.skipif(sys.platform != "linux", reason="craft-parts is linux-only") +] @pytest.fixture diff --git a/tests/integration/parts/plugins/test_poetry.py b/tests/integration/parts/plugins/test_poetry.py index a77b7e2c1..f210e2d6d 100644 --- a/tests/integration/parts/plugins/test_poetry.py +++ b/tests/integration/parts/plugins/test_poetry.py @@ -27,11 +27,15 @@ from charmcraft import services from charmcraft.models import project -pytestmark = [pytest.mark.skipif(sys.platform != "linux", reason="craft-parts is linux-only")] +pytestmark = [ + pytest.mark.skipif(sys.platform != "linux", reason="craft-parts is linux-only") +] @pytest.fixture -def charm_project(basic_charm_dict: dict[str, Any], project_path: pathlib.Path, request): +def charm_project( + basic_charm_dict: dict[str, Any], project_path: pathlib.Path, request +): return project.PlatformCharm.unmarshal( basic_charm_dict | { @@ -51,7 +55,13 @@ def charm_project(basic_charm_dict: dict[str, Any], project_path: pathlib.Path, @pytest.fixture def poetry_project(project_path: pathlib.Path) -> None: subprocess.run( - ["poetry", "init", "--name=test-charm", f"--directory={project_path}", "--no-interaction"], + [ + "poetry", + "init", + "--name=test-charm", + f"--directory={project_path}", + "--no-interaction", + ], check=False, ) source_dir = project_path / "src" diff --git a/tests/integration/parts/plugins/test_python.py b/tests/integration/parts/plugins/test_python.py index 825f984ba..d03e7193c 100644 --- a/tests/integration/parts/plugins/test_python.py +++ b/tests/integration/parts/plugins/test_python.py @@ -26,11 +26,15 @@ from charmcraft import services from charmcraft.models import project -pytestmark = [pytest.mark.skipif(sys.platform != "linux", reason="craft-parts is linux-only")] +pytestmark = [ + pytest.mark.skipif(sys.platform != "linux", reason="craft-parts is linux-only") +] @pytest.fixture -def charm_project(basic_charm_dict: dict[str, Any], project_path: pathlib.Path, request): +def charm_project( + basic_charm_dict: dict[str, Any], project_path: pathlib.Path, request +): return project.PlatformCharm.unmarshal( basic_charm_dict | { @@ -71,7 +75,14 @@ def test_python_plugin( # Check that the part install directory looks correct. assert (install_path / "src" / "charm.py").read_text() == "# Charm file" assert (install_path / "venv" / "lib").is_dir() - assert len(list((install_path / "venv" / "lib").glob("python*/site-packages/distro.py"))) == 1 + assert ( + len( + list( + (install_path / "venv" / "lib").glob("python*/site-packages/distro.py") + ) + ) + == 1 + ) # Check that the stage directory looks correct. assert (stage_path / "src" / "charm.py").read_text() == "# Charm file" diff --git a/tests/integration/services/test_image.py b/tests/integration/services/test_image.py index 13435d0ea..dc19d0d27 100644 --- a/tests/integration/services/test_image.py +++ b/tests/integration/services/test_image.py @@ -25,7 +25,8 @@ @pytest.fixture def image_service() -> services.ImageService: service = services.ImageService( - app=application.APP_METADATA, services=None # pyright: ignore[reportArgumentType] + app=application.APP_METADATA, + services=None, # pyright: ignore[reportArgumentType] ) service.setup() return service @@ -42,5 +43,7 @@ def image_service() -> services.ImageService: ], ) @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_get_maybe_id_from_docker_no_exceptions(image_service: services.ImageService, url): +def test_get_maybe_id_from_docker_no_exceptions( + image_service: services.ImageService, url +): image_service.get_maybe_id_from_docker(url) diff --git a/tests/integration/services/test_lifecycle.py b/tests/integration/services/test_lifecycle.py index ff473a44b..f0c199858 100644 --- a/tests/integration/services/test_lifecycle.py +++ b/tests/integration/services/test_lifecycle.py @@ -15,7 +15,6 @@ # For further info, check https://github.com/canonical/charmcraft """Integration tests for the lifecycle service.""" - import distro import pytest from craft_application import errors, models, util diff --git a/tests/integration/services/test_package.py b/tests/integration/services/test_package.py index 7f21f98b5..4dee96074 100644 --- a/tests/integration/services/test_package.py +++ b/tests/integration/services/test_package.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for package service.""" + import datetime import pathlib @@ -47,14 +48,18 @@ def package_service(new_path: pathlib.Path, service_factory, default_build_plan) for path in (pathlib.Path(__file__).parent / "sample_projects").iterdir() ], ) -@freezegun.freeze_time(datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc)) +@freezegun.freeze_time( + datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc) +) def test_write_metadata(monkeypatch, new_path, package_service, project_path): monkeypatch.setattr(charmcraft, "__version__", "3.0-test-version") test_prime_dir = new_path / "prime" test_prime_dir.mkdir() expected_prime_dir = project_path / "prime" - project = models.CharmcraftProject.from_yaml_file(project_path / "project" / "charmcraft.yaml") + project = models.CharmcraftProject.from_yaml_file( + project_path / "project" / "charmcraft.yaml" + ) project._started_at = datetime.datetime.now(tz=datetime.timezone.utc) package_service._project = project @@ -71,7 +76,9 @@ def test_write_metadata(monkeypatch, new_path, package_service, project_path): for path in (pathlib.Path(__file__).parent / "sample_projects").iterdir() ], ) -@freezegun.freeze_time(datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc)) +@freezegun.freeze_time( + datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc) +) def test_overwrite_metadata(monkeypatch, new_path, package_service, project_path): """Test that the metadata file gets rewritten for a charm. @@ -82,7 +89,9 @@ def test_overwrite_metadata(monkeypatch, new_path, package_service, project_path test_prime_dir.mkdir() expected_prime_dir = project_path / "prime" - project = models.CharmcraftProject.from_yaml_file(project_path / "project" / "charmcraft.yaml") + project = models.CharmcraftProject.from_yaml_file( + project_path / "project" / "charmcraft.yaml" + ) project._started_at = datetime.datetime.now(tz=datetime.timezone.utc) package_service._project = project @@ -94,7 +103,9 @@ def test_overwrite_metadata(monkeypatch, new_path, package_service, project_path pytest_check.equal((test_prime_dir / file.name).read_text(), file.read_text()) -@freezegun.freeze_time(datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc)) +@freezegun.freeze_time( + datetime.datetime(2020, 3, 14, 0, 0, 0, tzinfo=datetime.timezone.utc) +) def test_no_overwrite_reactive_metadata(monkeypatch, new_path, package_service): """Test that the metadata file doesn't get overwritten for a reactive charm.. @@ -108,7 +119,9 @@ def test_no_overwrite_reactive_metadata(monkeypatch, new_path, package_service): test_stage_dir.mkdir() (test_stage_dir / const.METADATA_FILENAME).write_text("INVALID!!") - project = models.CharmcraftProject.from_yaml_file(project_path / "project" / "charmcraft.yaml") + project = models.CharmcraftProject.from_yaml_file( + project_path / "project" / "charmcraft.yaml" + ) project._started_at = datetime.datetime.now(tz=datetime.timezone.utc) package_service._project = project diff --git a/tests/integration/services/test_provider.py b/tests/integration/services/test_provider.py index a6b149f99..c640c3fcf 100644 --- a/tests/integration/services/test_provider.py +++ b/tests/integration/services/test_provider.py @@ -38,7 +38,9 @@ def test_lock_cache( cache_path = tmp_path / "cache" cache_path.mkdir() lock_file = cache_path / "charmcraft.lock" - bash_lock_cmd = ["bash", "-c", f"flock -n {lock_file} true"] if shutil.which("flock") else None + bash_lock_cmd = ( + ["bash", "-c", f"flock -n {lock_file} true"] if shutil.which("flock") else None + ) provider = service_factory.provider provider_kwargs = { "build_info": default_build_info, @@ -71,7 +73,9 @@ def test_locked_cache_no_cache( cache_path.mkdir() lock_file = cache_path / "charmcraft.lock" - bash_lock_cmd = ["bash", "-c", f"flock -n {lock_file} true"] if shutil.which("flock") else None + bash_lock_cmd = ( + ["bash", "-c", f"flock -n {lock_file} true"] if shutil.which("flock") else None + ) # Check that we can lock the file from another process. if bash_lock_cmd: subprocess.run(bash_lock_cmd, check=True) @@ -97,7 +101,8 @@ def test_locked_cache_no_cache( # instance cache and not the shared cache. assert list(cache_path.iterdir()) == [cache_path / "charmcraft.lock"] emitter.assert_progress( - "Shared cache locked by another process; running without cache.", permanent=True + "Shared cache locked by another process; running without cache.", + permanent=True, ) assert not (tmp_path / "cache_cached").exists() diff --git a/tests/integration/services/test_store.py b/tests/integration/services/test_store.py index f54fe91a1..5ca489063 100644 --- a/tests/integration/services/test_store.py +++ b/tests/integration/services/test_store.py @@ -15,7 +15,6 @@ # For further info, check https://github.com/canonical/charmcraft """Integration tests for the store service.""" - import pytest from charmcraft import models, services diff --git a/tests/integration/test_charm_builder.py b/tests/integration/test_charm_builder.py index 16c84e877..7859cd4ac 100644 --- a/tests/integration/test_charm_builder.py +++ b/tests/integration/test_charm_builder.py @@ -15,7 +15,6 @@ # For further info, check https://github.com/canonical/charmcraft """Integration tests for CharmBuilder.""" - import pathlib import sys diff --git a/tests/integration/utils/test_skopeo.py b/tests/integration/utils/test_skopeo.py index 281827a5b..d2cbfb0d9 100644 --- a/tests/integration/utils/test_skopeo.py +++ b/tests/integration/utils/test_skopeo.py @@ -28,7 +28,8 @@ pytestmark = [ pytest.mark.skipif( - "CI" not in os.environ and not shutil.which("skopeo"), reason="skopeo not found in PATH" + "CI" not in os.environ and not shutil.which("skopeo"), + reason="skopeo not found in PATH", ), pytest.mark.xfail( platform.system().lower() not in ("linux", "darwin"), @@ -43,7 +44,11 @@ [ ("alpine", "docker://ghcr.io/containerd/alpine", "3.14.0"), ("debian12", "docker://gcr.io/distroless/base-debian12", "nonroot"), - ("mock-rock", "docker://ghcr.io/canonical/oci-factory/mock-rock", "1.2-22.04_279"), + ( + "mock-rock", + "docker://ghcr.io/canonical/oci-factory/mock-rock", + "1.2-22.04_279", + ), ("nanoserver", "docker://ghcr.io/containerd/nanoserver", "1809"), ], ) diff --git a/tests/test_charm_builder.py b/tests/test_charm_builder.py index b8a3ebecd..2c7436f89 100644 --- a/tests/test_charm_builder.py +++ b/tests/test_charm_builder.py @@ -583,7 +583,13 @@ def test_build_dispatcher_classic_hooks_linking_charm_replaced(tmp_path, assert_ @pytest.mark.parametrize( - ("python_packages", "binary_packages", "reqs_contents", "charmlibs", "expected_call_params"), + ( + "python_packages", + "binary_packages", + "reqs_contents", + "charmlibs", + "expected_call_params", + ), [ pytest.param( [], @@ -654,7 +660,14 @@ def test_build_dispatcher_classic_hooks_linking_charm_replaced(tmp_path, assert_ [], [], ["charmlib-dep"], - [["install", "--no-binary=:all:", "--requirement={reqs_file}", "charmlib-dep"]], + [ + [ + "install", + "--no-binary=:all:", + "--requirement={reqs_file}", + "charmlib-dep", + ] + ], id="charmlib-dep-only", ), pytest.param( @@ -685,7 +698,12 @@ def test_build_dispatcher_classic_hooks_linking_charm_replaced(tmp_path, assert_ [ ["install", "bin-pkg1", "duplicate"], ["install", "--no-binary=:all:", "duplicate", "pkg1"], - ["install", "--no-binary=:all:", "--requirement={reqs_file}", "lib-dep"], + [ + "install", + "--no-binary=:all:", + "--requirement={reqs_file}", + "lib-dep", + ], ], id="all-overlap", ), @@ -723,10 +741,13 @@ def test_build_dependencies_virtualenv( with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() - pip_cmd = str(charm_builder._find_venv_bin(tmp_path / const.STAGING_VENV_DIRNAME, "pip")) + pip_cmd = str( + charm_builder._find_venv_bin(tmp_path / const.STAGING_VENV_DIRNAME, "pip") + ) formatted_calls = [ - [param.format(reqs_file=str(reqs_file)) for param in call] for call in expected_call_params + [param.format(reqs_file=str(reqs_file)) for param in call] + for call in expected_call_params ] extra_pip_calls = [call([pip_cmd, *params]) for params in formatted_calls] @@ -739,7 +760,9 @@ def test_build_dependencies_virtualenv( site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] assert_output("Handling dependencies", "Installing dependencies") @@ -768,7 +791,9 @@ def test_build_dependencies_virtualenv_multiple(tmp_path, assert_output): with patch("shutil.copytree") as mock_copytree: builder.handle_dependencies() - pip_cmd = str(charm_builder._find_venv_bin(tmp_path / const.STAGING_VENV_DIRNAME, "pip")) + pip_cmd = str( + charm_builder._find_venv_bin(tmp_path / const.STAGING_VENV_DIRNAME, "pip") + ) assert mock.mock_calls == [ call(["python3", "-m", "venv", str(tmp_path / const.STAGING_VENV_DIRNAME)]), call([pip_cmd, "install", "--force-reinstall", f"pip@{KNOWN_GOOD_PIP_URL}"]), @@ -786,7 +811,9 @@ def test_build_dependencies_virtualenv_multiple(tmp_path, assert_output): site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] assert_output("Handling dependencies", "Installing dependencies") @@ -846,7 +873,9 @@ def test_build_dependencies_no_reused_missing_venv(tmp_path, assert_output): site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] # remove the site venv directory staging_venv_dir.rmdir() @@ -867,7 +896,9 @@ def test_build_dependencies_no_reused_missing_venv(tmp_path, assert_output): site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] def test_build_dependencies_no_reused_missing_hash_file(tmp_path, assert_output): @@ -905,7 +936,9 @@ def test_build_dependencies_no_reused_missing_hash_file(tmp_path, assert_output) site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] # remove the hash file (tmp_path / const.DEPENDENCIES_HASH_FILENAME).unlink() @@ -926,7 +959,9 @@ def test_build_dependencies_no_reused_missing_hash_file(tmp_path, assert_output) site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] def test_build_dependencies_no_reused_problematic_hash_file(tmp_path, assert_output): @@ -964,10 +999,14 @@ def test_build_dependencies_no_reused_problematic_hash_file(tmp_path, assert_out site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] # avoid the file to be read successfully - (tmp_path / const.DEPENDENCIES_HASH_FILENAME).write_bytes(b"\xc3\x28") # invalid UTF8 + (tmp_path / const.DEPENDENCIES_HASH_FILENAME).write_bytes( + b"\xc3\x28" + ) # invalid UTF8 # second run! with patch("shutil.copytree") as mock_copytree: @@ -986,7 +1025,9 @@ def test_build_dependencies_no_reused_problematic_hash_file(tmp_path, assert_out site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] @pytest.mark.parametrize( @@ -1049,7 +1090,9 @@ def test_build_dependencies_no_reused_different_dependencies( site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] # for the second call, default new dependencies to first ones so only one is changed at a time if new_reqs_content is not None: @@ -1076,7 +1119,9 @@ def test_build_dependencies_no_reused_different_dependencies( site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] def test_build_dependencies_reused(tmp_path, assert_output): @@ -1118,7 +1163,9 @@ def test_build_dependencies_reused(tmp_path, assert_output): site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] # second run! with patch("shutil.copytree") as mock_copytree: @@ -1133,7 +1180,9 @@ def test_build_dependencies_reused(tmp_path, assert_output): site_packages_dir = charm_builder._find_venv_site_packages( pathlib.Path(const.STAGING_VENV_DIRNAME) ) - assert mock_copytree.mock_calls == [call(site_packages_dir, build_dir / const.VENV_DIRNAME)] + assert mock_copytree.mock_calls == [ + call(site_packages_dir, build_dir / const.VENV_DIRNAME) + ] # -- tests about juju ignore @@ -1192,8 +1241,12 @@ def mock_build_charm(self): fake_argv = ["cmd", "--builddir", "builddir", "--installdir", "installdir"] with patch.object(sys, "argv", fake_argv): - with patch("charmcraft.charm_builder.CharmBuilder.build_charm", new=mock_build_charm): - with patch("charmcraft.charm_builder.collect_charmlib_pydeps") as mock_collect_pydeps: + with patch( + "charmcraft.charm_builder.CharmBuilder.build_charm", new=mock_build_charm + ): + with patch( + "charmcraft.charm_builder.collect_charmlib_pydeps" + ) as mock_collect_pydeps: with pytest.raises(SystemExit) as raised: charm_builder.main() assert raised.value.code == 42 @@ -1216,8 +1269,12 @@ def mock_build_charm(self): fake_argv = ["cmd", "--builddir", "builddir", "--installdir", "installdir"] fake_argv += ["-rreqs1.txt", "--requirement", "reqs2.txt"] with patch.object(sys, "argv", fake_argv): - with patch("charmcraft.charm_builder.CharmBuilder.build_charm", new=mock_build_charm): - with patch("charmcraft.charm_builder.collect_charmlib_pydeps") as mock_collect_pydeps: + with patch( + "charmcraft.charm_builder.CharmBuilder.build_charm", new=mock_build_charm + ): + with patch( + "charmcraft.charm_builder.collect_charmlib_pydeps" + ) as mock_collect_pydeps: with pytest.raises(SystemExit) as raised: charm_builder.main() assert raised.value.code == 42 diff --git a/tests/test_infra.py b/tests/test_infra.py index 0a1264c6e..56f26eca4 100644 --- a/tests/test_infra.py +++ b/tests/test_infra.py @@ -55,5 +55,7 @@ def test_ensure_copyright(): else: issues.append(filepath) if issues: - msg = "Please add copyright headers to the following files:\n" + "\n".join(issues) + msg = "Please add copyright headers to the following files:\n" + "\n".join( + issues + ) pytest.fail(msg, pytrace=False) diff --git a/tests/test_instrum.py b/tests/test_instrum.py index b1602857b..571414168 100644 --- a/tests/test_instrum.py +++ b/tests/test_instrum.py @@ -97,7 +97,10 @@ def test_measurement_extra_info_complex(): weird_object = object() mid = measurements.start("test msg", {"foo": 42, "bar": weird_object}) - assert measurements.measurements[mid]["extra"] == {"foo": "42", "bar": str(weird_object)} + assert measurements.measurements[mid]["extra"] == { + "foo": "42", + "bar": str(weird_object), + } def test_measurement_overlapped_measurements(): @@ -224,7 +227,9 @@ def test_measurement_merge_complex(tmp_path, fake_times): # merge from it and check merged structure measurements_outer.merge_from(measures_filepath) merged_1 = measurements_outer.measurements[mid_inner_1] - assert merged_1["parent"] == mid_outer_2 # the parent is the "current" outer measure + assert ( + merged_1["parent"] == mid_outer_2 + ) # the parent is the "current" outer measure assert merged_1["tstart"] == 25 # back to absolute assert merged_1["tend"] == 55 # back to absolute merged_2 = measurements_outer.measurements[mid_inner_2] @@ -232,7 +237,9 @@ def test_measurement_merge_complex(tmp_path, fake_times): assert merged_2["tstart"] == 35 # back to absolute assert merged_2["tend"] == 45 # back to absolute merged_3 = measurements_outer.measurements[mid_inner_3] - assert merged_3["parent"] == mid_outer_2 # the parent is the "current" outer measure + assert ( + merged_3["parent"] == mid_outer_2 + ) # the parent is the "current" outer measure assert merged_3["tstart"] == 65 # back to absolute assert merged_3["tend"] == 75 # back to absolute diff --git a/tests/test_linters.py b/tests/test_linters.py index 6aafe7bb5..3fb5296d0 100644 --- a/tests/test_linters.py +++ b/tests/test_linters.py @@ -76,7 +76,7 @@ def test_epfromdispatch_inaccessible_dispatch(tmp_path): def test_epfromdispatch_broken_dispatch(tmp_path): """The charm has a dispatch which we can't decode.""" dispatch = tmp_path / const.DISPATCH_FILENAME - dispatch.write_bytes(b"\xC0\xC0") + dispatch.write_bytes(b"\xc0\xc0") result = get_entrypoint_from_dispatch(tmp_path) assert result is None @@ -93,7 +93,9 @@ def test_checkdispatchpython_python_ok(tmp_path): """The charm is written in Python.""" entrypoint = tmp_path / "charm.py" entrypoint.touch(mode=0o700) - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = check_dispatch_with_python_entrypoint(tmp_path) assert result == entrypoint @@ -101,7 +103,9 @@ def test_checkdispatchpython_python_ok(tmp_path): def test_checkdispatchpython_no_entrypoint(tmp_path): """Cannot find the entrypoint used in dispatch.""" entrypoint = tmp_path / "charm.py" - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = check_dispatch_with_python_entrypoint(tmp_path) assert result is None @@ -124,7 +128,9 @@ def test_checkdispatchpython_entrypoint_is_not_python(tmp_path): ) entrypoint = tmp_path / "charm" entrypoint.touch(mode=0o700) - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = check_dispatch_with_python_entrypoint(tmp_path) assert result is None @@ -136,7 +142,9 @@ def test_checkdispatchpython_entrypoint_no_exec(tmp_path): dispatch.write_text(EXAMPLE_DISPATCH) entrypoint = tmp_path / "charm.py" entrypoint.touch() - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = check_dispatch_with_python_entrypoint(tmp_path) assert result is None @@ -146,7 +154,9 @@ def test_checkdispatchpython_entrypoint_no_exec(tmp_path): def test_language_python(): """The charm is written in Python.""" - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path("entrypoint") result = Language().run(pathlib.Path("somedir")) assert result == Language.Result.PYTHON @@ -155,7 +165,9 @@ def test_language_python(): def test_language_no_dispatch(tmp_path): """The charm has no dispatch at all.""" - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = None result = Language().run(pathlib.Path("somedir")) assert result == Language.Result.UNKNOWN @@ -214,7 +226,9 @@ def test_framework_operator_used_ok(tmp_path, import_line): opsdir.mkdir(parents=True) # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path(entrypoint) result = Framework()._check_operator(tmp_path) assert result is True @@ -232,7 +246,9 @@ def test_framework_operator_language_not_python(tmp_path): opsdir.mkdir(parents=True) # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = None result = Framework()._check_operator(tmp_path) assert result is False @@ -245,7 +261,9 @@ def test_framework_operator_venv_directory_missing(tmp_path): entrypoint.write_text("import ops") # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path(entrypoint) result = Framework()._check_operator(tmp_path) assert result is False @@ -262,7 +280,9 @@ def test_framework_operator_no_venv_ops_directory(tmp_path): venvdir.mkdir() # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path(entrypoint) result = Framework()._check_operator(tmp_path) assert result is False @@ -280,7 +300,9 @@ def test_framework_operator_venv_ops_directory_is_not_a_dir(tmp_path): opsfile.touch() # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path(entrypoint) result = Framework()._check_operator(tmp_path) assert result is False @@ -297,7 +319,9 @@ def test_framework_operator_corrupted_entrypoint(tmp_path): opsdir.mkdir(parents=True) # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path(entrypoint) result = Framework()._check_operator(tmp_path) assert result is False @@ -323,7 +347,9 @@ def test_framework_operator_no_ops_imported(tmp_path, monkeypatch, import_line): opsdir.mkdir(parents=True) # check - with patch("charmcraft.linters.check_dispatch_with_python_entrypoint") as mock_check: + with patch( + "charmcraft.linters.check_dispatch_with_python_entrypoint" + ) as mock_check: mock_check.return_value = pathlib.Path(entrypoint) result = Framework()._check_operator(tmp_path) assert result is False @@ -844,7 +870,10 @@ def test_jujuconfig_no_type_in_options_items(tmp_path): linter = JujuConfig() result = linter.run(tmp_path) assert result == JujuConfig.Result.ERROR - assert linter.text == "Error in config.yaml: items under 'options' must have a 'type' key." + assert ( + linter.text + == "Error in config.yaml: items under 'options' must have a 'type' key." + ) @pytest.mark.parametrize( @@ -946,7 +975,9 @@ def test_entrypoint_missing(tmp_path): """The file does not exist.""" entrypoint = tmp_path / "charm" linter = Entrypoint() - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = linter.run(tmp_path) assert result == Entrypoint.Result.ERROR assert linter.text == f"Cannot find the entrypoint file: {str(entrypoint)!r}" @@ -957,7 +988,9 @@ def test_entrypoint_directory(tmp_path): entrypoint = tmp_path / "charm" entrypoint.mkdir() linter = Entrypoint() - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = linter.run(tmp_path) assert result == Entrypoint.Result.ERROR assert linter.text == f"The entrypoint is not a file: {str(entrypoint)!r}" @@ -969,7 +1002,9 @@ def test_entrypoint_non_exec(tmp_path): entrypoint = tmp_path / "charm" entrypoint.touch() linter = Entrypoint() - with patch("charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint): + with patch( + "charmcraft.linters.get_entrypoint_from_dispatch", return_value=entrypoint + ): result = linter.run(tmp_path) assert result == Entrypoint.Result.ERROR assert linter.text == f"The entrypoint file is not executable: {str(entrypoint)!r}" @@ -1030,7 +1065,10 @@ def test_additional_files_checker_not_applicable(tmp_path): result = linter.run(prime_dir) assert result == LintResult.NONAPPLICABLE - assert linter.text == "Additional files check not applicable without a build environment." + assert ( + linter.text + == "Additional files check not applicable without a build environment." + ) @pytest.mark.parametrize( diff --git a/tests/test_parts.py b/tests/test_parts.py index 55fe754fe..c6ba2a318 100644 --- a/tests/test_parts.py +++ b/tests/test_parts.py @@ -21,7 +21,9 @@ from charmcraft import parts -pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Windows not [yet] supported") +pytestmark = pytest.mark.skipif( + sys.platform == "win32", reason="Windows not [yet] supported" +) # -- tests for part config processing diff --git a/tests/test_snap.py b/tests/test_snap.py index 8ed617a19..7cdbe9703 100644 --- a/tests/test_snap.py +++ b/tests/test_snap.py @@ -29,7 +29,9 @@ @pytest.fixture def mock_snap_config(): - with mock.patch("charmcraft.snap.snaphelpers.SnapConfig", autospec=True) as mock_snap_config: + with mock.patch( + "charmcraft.snap.snaphelpers.SnapConfig", autospec=True + ) as mock_snap_config: yield mock_snap_config @@ -72,5 +74,7 @@ def fake_get(key: str): assert snap_config == CharmcraftSnapConfiguration(provider=provider) assert snap_config.provider == provider - with pytest.raises(ValueError, match=re.escape(f"provider {provider!r} is not supported")): + with pytest.raises( + ValueError, match=re.escape(f"provider {provider!r} is not supported") + ): validate_snap_configuration(snap_config) diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py index a3243766a..2fc1975f7 100644 --- a/tests/unit/commands/test_lifecycle.py +++ b/tests/unit/commands/test_lifecycle.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for lifecycle commands.""" + import argparse import pathlib @@ -63,7 +64,9 @@ def get_namespace( @pytest.fixture def pack(service_factory: services.ServiceFactory) -> lifecycle.PackCommand: - return lifecycle.PackCommand({"app": application.APP_METADATA, "services": service_factory}) + return lifecycle.PackCommand( + {"app": application.APP_METADATA, "services": service_factory} + ) @pytest.mark.parametrize( @@ -133,7 +136,9 @@ def test_pack_update_charm_libs_empty( ): simple_charm.charm_libs = [models.CharmLib(lib="my_charm.my_lib", version="0.1")] store_lib = Library("lib_id", "my_lib", "my_charm", 0, 1, "Lib contents", "hash") - service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = [store_lib] + service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = [ + store_lib + ] service_factory.store.anonymous_client.get_library.return_value = store_lib pack._update_charm_libs() @@ -157,7 +162,9 @@ def test_pack_update_charm_libs_no_update( path = fake_project_dir / utils.get_lib_path("my_charm", "my_lib", 0) path.parent.mkdir(parents=True) path.write_text("LIBID='id'\nLIBAPI=0\nLIBPATCH=1") - service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = [store_lib] + service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = [ + store_lib + ] service_factory.store.anonymous_client.get_library.return_value = store_lib pack._update_charm_libs() @@ -180,7 +187,9 @@ def test_pack_update_charm_libs_needs_update( path = fake_project_dir / utils.get_lib_path("my_charm", "my_lib", 0) path.parent.mkdir(parents=True) path.write_text("LIBID='id'\nLIBAPI=0\nLIBPATCH=1") - service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = [store_lib] + service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = [ + store_lib + ] service_factory.store.anonymous_client.get_library.return_value = store_lib pack._update_charm_libs() diff --git a/tests/unit/commands/test_store.py b/tests/unit/commands/test_store.py index 1d2164aa9..9d68d4bfd 100644 --- a/tests/unit/commands/test_store.py +++ b/tests/unit/commands/test_store.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for store commands.""" + import argparse import datetime import pathlib @@ -61,7 +62,14 @@ def test_login_basic_no_export(service_factory, mock_store_client): @pytest.mark.parametrize("permission", [None, [], ["package-manage"]]) @pytest.mark.parametrize("ttl", [None, 0, 2**65]) def test_login_export( - monkeypatch, service_factory, mock_store_client, charm, bundle, channel, permission, ttl + monkeypatch, + service_factory, + mock_store_client, + charm, + bundle, + channel, + permission, + ttl, ): mock_client_cls = mock.Mock(return_value=mock_store_client) monkeypatch.setattr(craft_store, "StoreClient", mock_client_cls) @@ -95,7 +103,13 @@ def test_login_export( bases=[models.ResponseCharmResourceBase()], ) ], - [{"revision": 123, "updated_at": "1900-01-01T00:00:00", "architectures": ["all"]}], + [ + { + "revision": 123, + "updated_at": "1900-01-01T00:00:00", + "architectures": ["all"], + } + ], ), ], ) @@ -213,9 +227,13 @@ def test_fetch_libs_missing_from_store(service_factory, libs, expected): ), ], ) -def test_fetch_libs_no_content(new_path, service_factory, libs, store_libs, dl_lib, expected): +def test_fetch_libs_no_content( + new_path, service_factory, libs, store_libs, dl_lib, expected +): service_factory.project.charm_libs = libs - service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = store_libs + service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = ( + store_libs + ) service_factory.store.anonymous_client.get_library.return_value = dl_lib fetch_libs = FetchLibs({"app": APP_METADATA, "services": service_factory}) @@ -258,7 +276,9 @@ def test_fetch_libs_success( new_path, emitter, service_factory, libs, store_libs, dl_lib, expected ) -> None: service_factory.project.charm_libs = libs - service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = store_libs + service_factory.store.anonymous_client.fetch_libraries_metadata.return_value = ( + store_libs + ) service_factory.store.anonymous_client.get_library.return_value = dl_lib fetch_libs = FetchLibs({"app": APP_METADATA, "services": service_factory}) diff --git a/tests/unit/models/test_charmcraft.py b/tests/unit/models/test_charmcraft.py index 719067198..a0a40b17b 100644 --- a/tests/unit/models/test_charmcraft.py +++ b/tests/unit/models/test_charmcraft.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for Charmcraft models.""" + import pytest from charmcraft.models import charmcraft @@ -22,9 +23,18 @@ @pytest.mark.parametrize( ("base_str", "expected"), [ - ("ubuntu@24.04", charmcraft.Base(name="ubuntu", channel="24.04", architectures=[])), - ("ubuntu@22.04", charmcraft.Base(name="ubuntu", channel="22.04", architectures=[])), - ("almalinux@9", charmcraft.Base(name="almalinux", channel="9", architectures=[])), + ( + "ubuntu@24.04", + charmcraft.Base(name="ubuntu", channel="24.04", architectures=[]), + ), + ( + "ubuntu@22.04", + charmcraft.Base(name="ubuntu", channel="22.04", architectures=[]), + ), + ( + "almalinux@9", + charmcraft.Base(name="almalinux", channel="9", architectures=[]), + ), ], ) def test_get_base_from_str_and_arch(base_str, expected): diff --git a/tests/unit/models/test_config.py b/tests/unit/models/test_config.py index e929d848e..689aac06c 100644 --- a/tests/unit/models/test_config.py +++ b/tests/unit/models/test_config.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for the config model.""" + import math import pydantic @@ -36,7 +37,10 @@ { "favourite integer": {"type": "int"}, "favourite number": {"type": "float", "default": math.pi}, - "catchphrase": {"type": "string", "description": "What's your catchphrase?"}, + "catchphrase": { + "type": "string", + "description": "What's your catchphrase?", + }, "default_answer": { "type": "boolean", "description": "Yes/no true or false", @@ -46,7 +50,9 @@ ], ) def test_valid_config(options): - assert JujuConfig.model_validate({"options": options}) == JujuConfig(options=options) + assert JujuConfig.model_validate({"options": options}) == JujuConfig( + options=options + ) def test_empty_config(): diff --git a/tests/unit/models/test_metadata.py b/tests/unit/models/test_metadata.py index 63fdeaa50..1a2fbe608 100644 --- a/tests/unit/models/test_metadata.py +++ b/tests/unit/models/test_metadata.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for metadata models.""" + import json import pytest @@ -82,7 +83,10 @@ def test_charm_metadata_from_charm_success(charm_dict, expected): charm = project.CharmcraftProject.unmarshal(charm_dict) - assert json.loads(json.dumps(metadata.CharmMetadata.from_charm(charm).marshal())) == expected + assert ( + json.loads(json.dumps(metadata.CharmMetadata.from_charm(charm).marshal())) + == expected + ) @pytest.mark.parametrize( @@ -95,5 +99,6 @@ def test_bundle_metadata_from_bundle(bundle_dict, expected): bundle = project.Bundle.unmarshal(BASIC_BUNDLE_DICT) assert ( - json.loads(json.dumps(metadata.BundleMetadata.from_bundle(bundle).marshal())) == expected + json.loads(json.dumps(metadata.BundleMetadata.from_bundle(bundle).marshal())) + == expected ) diff --git a/tests/unit/models/test_project.py b/tests/unit/models/test_project.py index 166979025..045111cf0 100644 --- a/tests/unit/models/test_project.py +++ b/tests/unit/models/test_project.py @@ -41,7 +41,9 @@ SIMPLE_BASE = Base(name="simple", channel="0.0") BASE_WITH_ONE_ARCH = Base(name="arch", channel="1.0", architectures=["amd64"]) -BASE_WITH_MULTIARCH = Base(name="multiarch", channel="2.0", architectures=["arm64", "riscv64"]) +BASE_WITH_MULTIARCH = Base( + name="multiarch", channel="2.0", architectures=["arm64", "riscv64"] +) SIMPLE_BASENAME = bases.BaseName("simple", "0.0") ONE_ARCH_BASENAME = bases.BaseName("arch", "1.0") MULTIARCH_BASENAME = bases.BaseName("multiarch", "2.0") @@ -110,7 +112,9 @@ } ], } -SIMPLE_METADATA_YAML = "{name: charmy-mccharmface, summary: Charmy!, description: Very charming!}" +SIMPLE_METADATA_YAML = ( + "{name: charmy-mccharmface, summary: Charmy!, description: Very charming!}" +) SIMPLE_CHARMCRAFT_YAML = f"""\ type: charm name: charmy-mccharmface @@ -131,9 +135,13 @@ "summary": "Charmy!", "description": "Very charming!", } -SIMPLE_CONFIG_YAML = "options: {admin: {default: root, description: Admin user, type: string}}" +SIMPLE_CONFIG_YAML = ( + "options: {admin: {default: root, description: Admin user, type: string}}" +) SIMPLE_CONFIG_DICT = { - "options": {"admin": {"type": "string", "default": "root", "description": "Admin user"}} + "options": { + "admin": {"type": "string", "default": "root", "description": "Admin user"} + } } SIMPLE_ACTIONS_YAML = "snooze: {description: Take a little nap.}" SIMPLE_ACTIONS_DICT = {"snooze": {"description": "Take a little nap."}} @@ -248,13 +256,17 @@ def test_platform_from_multiple_bases(bases, expected): *( list(x) for x in itertools.combinations(const.CharmArch, 1) ), # A single architecture in a list - *(list(x) for x in itertools.combinations(const.CharmArch, 2)), # Two architectures in a list + *( + list(x) for x in itertools.combinations(const.CharmArch, 2) + ), # Two architectures in a list ] # endregion # region CharmBuildInfo tests -@pytest.mark.parametrize("build_on_base", [SIMPLE_BASE, BASE_WITH_ONE_ARCH, BASE_WITH_MULTIARCH]) +@pytest.mark.parametrize( + "build_on_base", [SIMPLE_BASE, BASE_WITH_ONE_ARCH, BASE_WITH_MULTIARCH] +) @pytest.mark.parametrize("build_on_arch", ["amd64", "arm64", "riscv64", "s390x"]) @pytest.mark.parametrize("run_on", [SIMPLE_BASE, BASE_WITH_ONE_ARCH]) def test_build_info_from_build_on_run_on_basic( @@ -284,7 +296,9 @@ def test_build_info_from_build_on_run_on_basic( ], ) @pytest.mark.parametrize("lib_version", ["0", "1", "2.0", "2.1", "3.14"]) -def test_create_valid_charm_lib(lib_name: str, expected_lib_name: str, lib_version: str): +def test_create_valid_charm_lib( + lib_name: str, expected_lib_name: str, lib_version: str +): lib = project.CharmLib.unmarshal({"lib": lib_name, "version": lib_version}) assert lib.lib == expected_lib_name @@ -396,7 +410,9 @@ def test_build_info_from_build_on_run_on_multi_arch(run_on, expected): ], ) def test_build_info_generator(given, expected): - assert list(project.CharmBuildInfo.gen_from_bases_configurations(*given)) == expected + assert ( + list(project.CharmBuildInfo.gen_from_bases_configurations(*given)) == expected + ) # endregion @@ -514,7 +530,9 @@ def test_build_info_generator(given, expected): platform=f"ubuntu-22.04-{util.get_host_architecture()}", build_on=util.get_host_architecture(), build_for=util.get_host_architecture(), - build_for_bases=[project.charmcraft.Base(name="ubuntu", channel="22.04")], + build_for_bases=[ + project.charmcraft.Base(name="ubuntu", channel="22.04") + ], build_on_index=0, base=bases.BaseName("ubuntu", "22.04"), bases_index=0, @@ -523,7 +541,11 @@ def test_build_info_generator(given, expected): id="basic-bases", ), pytest.param( - {"bases": [{"build-on": [BASE_WITH_ONE_ARCH], "run-on": [BASE_WITH_ONE_ARCH]}]}, + { + "bases": [ + {"build-on": [BASE_WITH_ONE_ARCH], "run-on": [BASE_WITH_ONE_ARCH]} + ] + }, [ project.CharmBuildInfo( platform="arch-1.0-amd64", @@ -565,7 +587,9 @@ def test_build_planner_correct(data, expected): }, ], ) -def test_build_planner_platforms_combinations(base, build_base, build_plan_basename, platforms): +def test_build_planner_platforms_combinations( + base, build_base, build_plan_basename, platforms +): """Test that we're able to create a valid platform for each of these combinations.""" planner = project.CharmcraftBuildPlanner( base=base, @@ -581,12 +605,18 @@ def test_build_planner_platforms_combinations(base, build_base, build_plan_basen @pytest.mark.parametrize("architecture", sorted(const.SUPPORTED_ARCHITECTURES)) @pytest.mark.parametrize("system", ["ubuntu", "linux", "macos", "windows", "plan9"]) -@pytest.mark.parametrize("release", ["22.04", "2.6.32", "10.5", "vista", "from bell labs"]) +@pytest.mark.parametrize( + "release", ["22.04", "2.6.32", "10.5", "vista", "from bell labs"] +) def test_get_bundle_plan(mocker, architecture, release, system): - mocker.patch("craft_application.util.get_host_architecture", return_value=architecture) + mocker.patch( + "craft_application.util.get_host_architecture", return_value=architecture + ) mocker.patch( "charmcraft.utils.get_os_platform", - return_value=utils.OSPlatform(machine=architecture, system=system, release=release), + return_value=utils.OSPlatform( + machine=architecture, system=system, release=release + ), ) planner = project.CharmcraftBuildPlanner(type="bundle") @@ -659,14 +689,16 @@ def test_unmarshal_invalid_type(type_): None, SIMPLE_CONFIG_YAML, None, - SIMPLE_CHARMCRAFT_DICT | {"config": SIMPLE_CONFIG_DICT, "parts": BASIC_CHARM_PARTS}, + SIMPLE_CHARMCRAFT_DICT + | {"config": SIMPLE_CONFIG_DICT, "parts": BASIC_CHARM_PARTS}, ), ( SIMPLE_CHARMCRAFT_YAML, None, None, SIMPLE_ACTIONS_YAML, - SIMPLE_CHARMCRAFT_DICT | {"actions": SIMPLE_ACTIONS_DICT, "parts": BASIC_CHARM_PARTS}, + SIMPLE_CHARMCRAFT_DICT + | {"actions": SIMPLE_ACTIONS_DICT, "parts": BASIC_CHARM_PARTS}, ), ( MINIMAL_CHARMCRAFT_YAML, @@ -869,7 +901,10 @@ def test_from_yaml_file_exception( "charmhub": {"api_url": "http://charmhub.io"}, }, ), - (project.Bundle, {"type": "bundle", "charmhub": {"api_url": "http://charmhub.io"}}), + ( + project.Bundle, + {"type": "bundle", "charmhub": {"api_url": "http://charmhub.io"}}, + ), ], ) def test_warn_on_deprecated_charmhub( @@ -897,7 +932,9 @@ def test_warn_on_deprecated_charmhub( ), ], ) -def test_instantiate_bases_charm_success(values: dict[str, Any], expected_changes: dict[str, Any]): +def test_instantiate_bases_charm_success( + values: dict[str, Any], expected_changes: dict[str, Any] +): """Various successful instantiations of a charm project.""" values.update( { diff --git a/tests/unit/parts/plugins/test_charm.py b/tests/unit/parts/plugins/test_charm.py index 40fcb396f..77c47e8f0 100644 --- a/tests/unit/parts/plugins/test_charm.py +++ b/tests/unit/parts/plugins/test_charm.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for charm plugin.""" + import pathlib import sys from unittest.mock import patch @@ -90,7 +91,9 @@ def test_charmplugin_get_build_environment_ubuntu(charm_plugin, mocker): mock_version = mocker.patch("craft_parts.utils.os_utils.OsRelease.version_id") mock_id.return_value = "ubuntu" mock_version.return_value = "22.04" - assert charm_plugin.get_build_environment() == {"CRYPTOGRAPHY_OPENSSL_NO_LEGACY": "true"} + assert charm_plugin.get_build_environment() == { + "CRYPTOGRAPHY_OPENSSL_NO_LEGACY": "true" + } def test_charmplugin_get_build_environment_centos_7(charm_plugin, mocker, monkeypatch): @@ -105,7 +108,9 @@ def test_charmplugin_get_build_environment_centos_7(charm_plugin, mocker, monkey } -def test_charmplugin_get_build_commands_ubuntu(charm_plugin, tmp_path, mocker, monkeypatch): +def test_charmplugin_get_build_commands_ubuntu( + charm_plugin, tmp_path, mocker, monkeypatch +): monkeypatch.setenv("PATH", "/some/path") monkeypatch.setenv("SNAP", "snap_value") monkeypatch.setenv("SNAP_ARCH", "snap_arch_value") @@ -146,10 +151,14 @@ def test_charmplugin_get_build_commands_ubuntu(charm_plugin, tmp_path, mocker, m ] # check the callback is properly registered for running own method after build - mock_register.assert_called_with(charm_plugin.post_build_callback, step_list=[Step.BUILD]) + mock_register.assert_called_with( + charm_plugin.post_build_callback, step_list=[Step.BUILD] + ) -def test_charmplugin_get_build_commands_centos_7(charm_plugin, tmp_path, mocker, monkeypatch): +def test_charmplugin_get_build_commands_centos_7( + charm_plugin, tmp_path, mocker, monkeypatch +): monkeypatch.setenv("PATH", "/some/path") monkeypatch.setenv("SNAP", "snap_value") monkeypatch.setenv("SNAP_ARCH", "snap_arch_value") @@ -193,7 +202,9 @@ def test_charmplugin_get_build_commands_centos_7(charm_plugin, tmp_path, mocker, ] # check the callback is properly registered for running own method after build - mock_register.assert_called_with(charm_plugin.post_build_callback, step_list=[Step.BUILD]) + mock_register.assert_called_with( + charm_plugin.post_build_callback, step_list=[Step.BUILD] + ) def test_charmplugin_post_build_metric_collection(charm_plugin): diff --git a/tests/unit/parts/plugins/test_poetry.py b/tests/unit/parts/plugins/test_poetry.py index 31880c4d7..d9ac2c820 100644 --- a/tests/unit/parts/plugins/test_poetry.py +++ b/tests/unit/parts/plugins/test_poetry.py @@ -23,33 +23,47 @@ from charmcraft.parts import plugins -pytestmark = [pytest.mark.skipif(sys.platform == "win32", reason="Windows not supported")] +pytestmark = [ + pytest.mark.skipif(sys.platform == "win32", reason="Windows not supported") +] -def test_get_build_environment(poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path): +def test_get_build_environment( + poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path +): env = poetry_plugin.get_build_environment() assert env["PIP_NO_BINARY"] == ":all:" -def test_get_venv_directory(poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path): +def test_get_venv_directory( + poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path +): assert poetry_plugin._get_venv_directory() == install_path / "venv" def test_get_pip_install_commands(poetry_plugin: plugins.PoetryPlugin): poetry_plugin._get_pip = lambda: "/python -m pip" - assert poetry_plugin._get_pip_install_commands(pathlib.Path("/my dir/reqs.txt")) == [ + assert poetry_plugin._get_pip_install_commands( + pathlib.Path("/my dir/reqs.txt") + ) == [ "/python -m pip install --no-deps '--requirement=/my dir/reqs.txt'", "/python -m pip check", ] def test_get_package_install_commands( - poetry_plugin: plugins.PoetryPlugin, build_path: pathlib.Path, install_path: pathlib.Path + poetry_plugin: plugins.PoetryPlugin, + build_path: pathlib.Path, + install_path: pathlib.Path, ): - copy_src_cmd = f"cp --archive --recursive --reflink=auto {build_path}/src {install_path}" - copy_lib_cmd = f"cp --archive --recursive --reflink=auto {build_path}/lib {install_path}" + copy_src_cmd = ( + f"cp --archive --recursive --reflink=auto {build_path}/src {install_path}" + ) + copy_lib_cmd = ( + f"cp --archive --recursive --reflink=auto {build_path}/lib {install_path}" + ) # Check if no src or libs exist default_commands = poetry_plugin._get_package_install_commands() @@ -80,7 +94,9 @@ def test_get_package_install_commands( ) -def test_get_rm_command(poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path): +def test_get_rm_command( + poetry_plugin: plugins.PoetryPlugin, install_path: pathlib.Path +): assert f"rm -rf {install_path / 'venv/bin'}" in poetry_plugin.get_build_commands() @@ -93,4 +109,6 @@ def test_no_get_rm_command( "poetry-keep-bins": True, } poetry_plugin._options = plugins.PoetryPluginProperties.unmarshal(spec) - assert f"rm -rf {install_path / 'venv/bin'}" not in poetry_plugin.get_build_commands() + assert ( + f"rm -rf {install_path / 'venv/bin'}" not in poetry_plugin.get_build_commands() + ) diff --git a/tests/unit/parts/plugins/test_python.py b/tests/unit/parts/plugins/test_python.py index 7943a537b..5010e4ddf 100644 --- a/tests/unit/parts/plugins/test_python.py +++ b/tests/unit/parts/plugins/test_python.py @@ -24,16 +24,22 @@ from charmcraft.parts import plugins -pytestmark = [pytest.mark.skipif(sys.platform == "win32", reason="Windows not supported")] +pytestmark = [ + pytest.mark.skipif(sys.platform == "win32", reason="Windows not supported") +] -def test_get_build_environment(python_plugin: plugins.PythonPlugin, install_path: pathlib.Path): +def test_get_build_environment( + python_plugin: plugins.PythonPlugin, install_path: pathlib.Path +): env = python_plugin.get_build_environment() assert env["PIP_NO_BINARY"] == ":all:" -def test_get_venv_directory(python_plugin: plugins.PythonPlugin, install_path: pathlib.Path): +def test_get_venv_directory( + python_plugin: plugins.PythonPlugin, install_path: pathlib.Path +): assert python_plugin._get_venv_directory() == install_path / "venv" @@ -58,8 +64,12 @@ def test_get_package_install_commands( } python_plugin._options = plugins.PythonPluginProperties.unmarshal(spec) python_plugin._get_pip = lambda: "/python -m pip" - copy_src_cmd = f"cp --archive --recursive --reflink=auto {build_path}/src {install_path}" - copy_lib_cmd = f"cp --archive --recursive --reflink=auto {build_path}/lib {install_path}" + copy_src_cmd = ( + f"cp --archive --recursive --reflink=auto {build_path}/src {install_path}" + ) + copy_lib_cmd = ( + f"cp --archive --recursive --reflink=auto {build_path}/lib {install_path}" + ) actual = python_plugin._get_package_install_commands() @@ -93,7 +103,9 @@ def test_get_package_install_commands( pytest_check.is_in(copy_lib_cmd, python_plugin._get_package_install_commands()) -def test_get_rm_command(python_plugin: plugins.PythonPlugin, install_path: pathlib.Path): +def test_get_rm_command( + python_plugin: plugins.PythonPlugin, install_path: pathlib.Path +): assert f"rm -rf {install_path / 'venv/bin'}" in python_plugin.get_build_commands() @@ -106,4 +118,6 @@ def test_no_get_rm_command( "python-keep-bins": True, } python_plugin._options = plugins.PythonPluginProperties.unmarshal(spec) - assert f"rm -rf {install_path / 'venv/bin'}" not in python_plugin.get_build_commands() + assert ( + f"rm -rf {install_path / 'venv/bin'}" not in python_plugin.get_build_commands() + ) diff --git a/tests/unit/parts/plugins/test_reactive.py b/tests/unit/parts/plugins/test_reactive.py index 9cd68b3b3..942883f0e 100644 --- a/tests/unit/parts/plugins/test_reactive.py +++ b/tests/unit/parts/plugins/test_reactive.py @@ -28,7 +28,9 @@ from charmcraft import const from charmcraft.parts.plugins import _reactive -pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Windows not [yet] supported") +pytestmark = pytest.mark.skipif( + sys.platform == "win32", reason="Windows not [yet] supported" +) @pytest.fixture @@ -87,7 +89,9 @@ def plugin(tmp_path, plugin_properties, spec): ) part_info = craft_parts.PartInfo(project_info=project_info, part=part) - return plugins.get_plugin(part=part, part_info=part_info, properties=plugin_properties) + return plugins.get_plugin( + part=part, part_info=part_info, properties=plugin_properties + ) def test_get_build_package(plugin): diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py index 00037ec4e..7784ca038 100644 --- a/tests/unit/parts/test_lifecycle.py +++ b/tests/unit/parts/test_lifecycle.py @@ -178,7 +178,9 @@ def test_partslifecycle_run_actions_progress(tmp_path, monkeypatch, emitter): with patch("craft_parts.LifecycleManager.plan") as mock_plan: mock_plan.return_value = [action1, action2] - with patch("craft_parts.executor.executor.ExecutionContext.execute") as mock_exec: + with patch( + "craft_parts.executor.executor.ExecutionContext.execute" + ) as mock_exec: lc.run(Step.PRIME) emitter.assert_progress("Running step STAGE for part 'testpart'") diff --git a/tests/unit/services/test_analysis.py b/tests/unit/services/test_analysis.py index d3c3d9eb4..a2ff9520e 100644 --- a/tests/unit/services/test_analysis.py +++ b/tests/unit/services/test_analysis.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for analysis service.""" + import pathlib import tempfile import zipfile @@ -57,12 +58,19 @@ def run(self, basedir: pathlib.Path) -> str: STUB_ATTRIBUTE_CHECKERS = [ StubAttributeChecker( - "unknown_attribute", "https://example.com/unknown", "returns unknown", LintResult.UNKNOWN + "unknown_attribute", + "https://example.com/unknown", + "returns unknown", + LintResult.UNKNOWN, + ), + StubAttributeChecker( + "says_python", "https://python.org", "returns python", "python" ), - StubAttributeChecker("says_python", "https://python.org", "returns python", "python"), ] STUB_CHECKER_RESULTS = [ - CheckResult(linter.name, linter.result, linter.url, CheckType.ATTRIBUTE, linter.text) + CheckResult( + linter.name, linter.result, linter.url, CheckType.ATTRIBUTE, linter.text + ) for linter in STUB_ATTRIBUTE_CHECKERS ] ATTRIBUTE_CHECKER_NAMES = frozenset(checker.name for checker in STUB_ATTRIBUTE_CHECKERS) @@ -86,7 +94,9 @@ def run(self, basedir: pathlib.Path) -> str: @pytest.fixture def mock_temp_dir(monkeypatch): mock_obj = mock.MagicMock(spec=tempfile.TemporaryDirectory) - monkeypatch.setattr(tempfile, "TemporaryDirectory", mock.Mock(return_value=mock_obj)) + monkeypatch.setattr( + tempfile, "TemporaryDirectory", mock.Mock(return_value=mock_obj) + ) return mock_obj @@ -104,7 +114,10 @@ def analysis_service(): @pytest.mark.parametrize( ("checkers", "expected"), - [(STUB_ATTRIBUTE_CHECKERS, STUB_CHECKER_RESULTS), (STUB_LINTERS, STUB_LINTER_RESULTS)], + [ + (STUB_ATTRIBUTE_CHECKERS, STUB_CHECKER_RESULTS), + (STUB_LINTERS, STUB_LINTER_RESULTS), + ], ) def test_lint_directory_results(monkeypatch, analysis_service, checkers, expected): monkeypatch.setattr(linters, "CHECKERS", checkers) @@ -114,22 +127,29 @@ def test_lint_directory_results(monkeypatch, analysis_service, checkers, expecte @pytest.mark.parametrize("checkers", [STUB_ATTRIBUTE_CHECKERS + STUB_LINTERS]) @pytest.mark.parametrize( - "ignore", [set(), {"success"}, ATTRIBUTE_CHECKER_NAMES, LINTER_NAMES, ALL_CHECKER_NAMES] + "ignore", + [set(), {"success"}, ATTRIBUTE_CHECKER_NAMES, LINTER_NAMES, ALL_CHECKER_NAMES], ) def test_lint_directory_ignores(monkeypatch, analysis_service, checkers, ignore): monkeypatch.setattr(linters, "CHECKERS", checkers) checker_names = {checker.name for checker in checkers} results = list( - analysis_service.lint_directory(pathlib.Path(), ignore=ignore, include_ignored=False) + analysis_service.lint_directory( + pathlib.Path(), ignore=ignore, include_ignored=False + ) ) checkers_run = {r.name for r in results} pytest_check.is_true(checkers_run.isdisjoint(ignore), f"{checkers_run & ignore}") - pytest_check.is_true(checkers_run.issubset(checker_names), str(checkers_run - checker_names)) + pytest_check.is_true( + checkers_run.issubset(checker_names), str(checkers_run - checker_names) + ) -def test_lint_file_results(fs, mock_temp_dir, mock_zip_file, monkeypatch, analysis_service): +def test_lint_file_results( + fs, mock_temp_dir, mock_zip_file, monkeypatch, analysis_service +): fake_charm = pathlib.Path("/fake/charm.charm") fs.create_file(fake_charm) mock_checker = mock.Mock() @@ -139,7 +159,9 @@ def test_lint_file_results(fs, mock_temp_dir, mock_zip_file, monkeypatch, analys results = list(analysis_service.lint_file(fake_charm)) with pytest_check.check: - mock_zip_file.__enter__.return_value.extractall.assert_called_once_with(fake_temp_path) + mock_zip_file.__enter__.return_value.extractall.assert_called_once_with( + fake_temp_path + ) with pytest_check.check: mock_checker.get_result.assert_called_once_with(fake_temp_path) pytest_check.equal(results, [mock_checker.get_result.return_value]) diff --git a/tests/unit/services/test_charmlibs.py b/tests/unit/services/test_charmlibs.py index b4d4b9120..3fb4764ab 100644 --- a/tests/unit/services/test_charmlibs.py +++ b/tests/unit/services/test_charmlibs.py @@ -75,7 +75,9 @@ def test_is_downloaded_with_file( lib_path.write_text("LIBID='abc'\nLIBAPI=0\nLIBPATCH=1\n") assert ( - service.is_downloaded(charm_name=charm_name, lib_name=lib_name, api=0, patch=patch) + service.is_downloaded( + charm_name=charm_name, lib_name=lib_name, api=0, patch=patch + ) == expected ) @@ -84,10 +86,18 @@ def test_is_downloaded_with_file( ("charm_name", "lib_name", "lib_contents", "expected"), [ pytest.param( - "my-charm", "my_lib", "LIBID='abc'\nLIBAPI=0\nLIBPATCH=1\n", (0, 1), id="0.1" + "my-charm", + "my_lib", + "LIBID='abc'\nLIBAPI=0\nLIBPATCH=1\n", + (0, 1), + id="0.1", ), pytest.param( - "my-charm", "my_lib", "LIBID='abc'\nLIBAPI=16\nLIBPATCH=19\n", (16, 19), id="16.19" + "my-charm", + "my_lib", + "LIBID='abc'\nLIBAPI=16\nLIBPATCH=19\n", + (16, 19), + id="16.19", ), pytest.param( "my-charm", @@ -108,11 +118,15 @@ def test_get_local_version( expected: tuple[int, int] | None, ): if expected is not None: - lib_path = fake_project_dir / utils.get_lib_path(charm_name, lib_name, expected[0]) + lib_path = fake_project_dir / utils.get_lib_path( + charm_name, lib_name, expected[0] + ) (fake_project_dir / lib_path).parent.mkdir(parents=True) (fake_project_dir / lib_path).write_text(lib_contents) - assert service.get_local_version(charm_name=charm_name, lib_name=lib_name) == expected + assert ( + service.get_local_version(charm_name=charm_name, lib_name=lib_name) == expected + ) @pytest.mark.parametrize( diff --git a/tests/unit/services/test_image.py b/tests/unit/services/test_image.py index f685a3f35..231635f82 100644 --- a/tests/unit/services/test_image.py +++ b/tests/unit/services/test_image.py @@ -15,7 +15,6 @@ # For further info, check https://github.com/canonical/charmcraft """Unit tests for the Image service.""" - import itertools import json from unittest import mock @@ -41,7 +40,9 @@ def mock_skopeo(fake_process) -> mock.Mock: @pytest.fixture def image_service(service_factory, mock_skopeo, mock_docker) -> services.ImageService: - service = services.ImageService(app=application.APP_METADATA, services=service_factory) + service = services.ImageService( + app=application.APP_METADATA, services=service_factory + ) service._skopeo = mock_skopeo service._docker = mock_docker return service @@ -79,7 +80,10 @@ def test_get_name_from_url(url: str, name: str): @pytest.mark.parametrize( ("go_arch", "charm_arch"), [ - *((key, const.CharmArch(value)) for key, value in const.GO_ARCH_TO_CHARM_ARCH.items()), + *( + (key, const.CharmArch(value)) + for key, value in const.GO_ARCH_TO_CHARM_ARCH.items() + ), ("amd64", "amd64"), ("arm64", "arm64"), ("riscv64", "riscv64"), @@ -90,9 +94,13 @@ def test_convert_go_acrh_to_charm_arch(go_arch: str, charm_arch: const.CharmArch assert services.ImageService.convert_go_arch_to_charm_arch(go_arch) == charm_arch -def test_get_maybe_id_from_docker_success(image_service: services.ImageService, mock_docker): +def test_get_maybe_id_from_docker_success( + image_service: services.ImageService, mock_docker +): expected = "sha256:some-sha-hash" - mock_docker.images.get.return_value = docker.models.images.Image(attrs={"Id": expected}) + mock_docker.images.get.return_value = docker.models.images.Image( + attrs={"Id": expected} + ) result = image_service.get_maybe_id_from_docker("some-image") @@ -100,7 +108,9 @@ def test_get_maybe_id_from_docker_success(image_service: services.ImageService, assert result == expected -def test_get_maybe_id_from_docker_failure(image_service: services.ImageService, mock_docker): +def test_get_maybe_id_from_docker_failure( + image_service: services.ImageService, mock_docker +): mock_docker.images.get.side_effect = docker.errors.ImageNotFound("womp womp") assert image_service.get_maybe_id_from_docker("some-image") is None @@ -115,10 +125,15 @@ def test_get_maybe_id_from_docker_no_docker(image_service: services.ImageService @pytest.mark.parametrize("image", ["my-image"]) @pytest.mark.parametrize("architecture", const.CharmArch) def test_inspect_single_arch( - fake_process, image_service: services.ImageService, mock_skopeo, image: str, architecture + fake_process, + image_service: services.ImageService, + mock_skopeo, + image: str, + architecture, ): fake_process.register( - ["/skopeo", "inspect", "--raw", image], stdout=json.dumps({"raw_manifest": True}) + ["/skopeo", "inspect", "--raw", image], + stdout=json.dumps({"raw_manifest": True}), ) fake_process.register( ["/skopeo", "inspect", image], @@ -135,7 +150,11 @@ def test_inspect_single_arch( @pytest.mark.parametrize("image", ["my-image"]) @pytest.mark.parametrize("architectures", itertools.product(const.CharmArch, repeat=2)) def test_inspect_two_arch( - fake_process, image_service: services.ImageService, mock_skopeo, image: str, architectures + fake_process, + image_service: services.ImageService, + mock_skopeo, + image: str, + architectures, ): fake_process.register( ["/skopeo", "inspect", "--raw", image], diff --git a/tests/unit/services/test_lifecycle.py b/tests/unit/services/test_lifecycle.py index cb0e0964e..a5ad0b0ac 100644 --- a/tests/unit/services/test_lifecycle.py +++ b/tests/unit/services/test_lifecycle.py @@ -42,7 +42,9 @@ def service(service_factory) -> LifecycleService: (f"foreign-{HOST_ARCH}", "foreign"), ], ) -def test_get_build_for_values(service: LifecycleService, plan_build_for: str, expected: str): +def test_get_build_for_values( + service: LifecycleService, plan_build_for: str, expected: str +): service._build_plan = [ models.BuildInfo( base=bases.BaseName("ubuntu", "22.04"), diff --git a/tests/unit/services/test_package.py b/tests/unit/services/test_package.py index 9ac506243..c11437b1f 100644 --- a/tests/unit/services/test_package.py +++ b/tests/unit/services/test_package.py @@ -32,7 +32,9 @@ from charmcraft.application.main import APP_METADATA from charmcraft.models.project import BasesCharm -SIMPLE_BUILD_BASE = models.charmcraft.Base(name="ubuntu", channel="22.04", architectures=["arm64"]) +SIMPLE_BUILD_BASE = models.charmcraft.Base( + name="ubuntu", channel="22.04", architectures=["arm64"] +) SIMPLE_MANIFEST = models.Manifest( charmcraft_started_at="1970-01-01T00:00:00+00:00", bases=[SIMPLE_BUILD_BASE], @@ -112,7 +114,9 @@ def test_get_charm_path(fake_path, package_service, bases, expected_name): ], ) def test_get_manifest(package_service, simple_charm, lint, expected): - simple_charm._started_at = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc) + simple_charm._started_at = datetime.datetime( + 1970, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc + ) assert package_service.get_manifest(lint) == expected @@ -181,10 +185,18 @@ def test_do_not_overwrite_actions_yaml( [ { "build-on": [ - {"name": "ubuntu", "channel": "22.04", "architectures": ["riscv64"]} + { + "name": "ubuntu", + "channel": "22.04", + "architectures": ["riscv64"], + } ], "run-on": [ - {"name": "ubuntu", "channel": "22.04", "architectures": ["all"]}, + { + "name": "ubuntu", + "channel": "22.04", + "architectures": ["all"], + }, ], }, ], @@ -206,20 +218,38 @@ def test_do_not_overwrite_actions_yaml( build_for=util.get_host_architecture(), base=BaseName("centos", "7"), ), - [{"name": "centos", "channel": "7", "architectures": [util.get_host_architecture()]}], + [ + { + "name": "centos", + "channel": "7", + "architectures": [util.get_host_architecture()], + } + ], ), pytest.param( [ {"name": "centos", "channel": "7"}, { "build-on": [{"name": "ubuntu", "channel": "20.04"}], - "run-on": [{"name": "ubuntu", "channel": "20.04", "architectures": ["all"]}], + "run-on": [ + {"name": "ubuntu", "channel": "20.04", "architectures": ["all"]} + ], }, { "build-on": [ - {"name": "ubuntu", "channel": "22.04", "architectures": ["amd64", "arm64"]} + { + "name": "ubuntu", + "channel": "22.04", + "architectures": ["amd64", "arm64"], + } + ], + "run-on": [ + { + "name": "ubuntu", + "channel": "22.04", + "architectures": ["arm64"], + } ], - "run-on": [{"name": "ubuntu", "channel": "22.04", "architectures": ["arm64"]}], }, ], BuildInfo( diff --git a/tests/unit/services/test_provider.py b/tests/unit/services/test_provider.py index be6bc72de..d0cd16475 100644 --- a/tests/unit/services/test_provider.py +++ b/tests/unit/services/test_provider.py @@ -91,7 +91,9 @@ def test_get_base_forwards_cache( fake_path: pathlib.Path, base_name: bases.BaseName, ): - monkeypatch.setattr("charmcraft.env.get_host_shared_cache_path", lambda: fake_path / "cache") + monkeypatch.setattr( + "charmcraft.env.get_host_shared_cache_path", lambda: fake_path / "cache" + ) base = provider_service.get_base( base_name=base_name, @@ -127,7 +129,9 @@ def test_get_base_no_cache_if_locked( locked = _maybe_lock_cache(cache_path) assert locked new_cache_path = pathlib.Path(str(cache_path)) - monkeypatch.setattr("charmcraft.env.get_host_shared_cache_path", lambda: new_cache_path) + monkeypatch.setattr( + "charmcraft.env.get_host_shared_cache_path", lambda: new_cache_path + ) # Can't use the fixture as pyfakefs doesn't handle locks. provider_service = services.ProviderService( diff --git a/tests/unit/services/test_store.py b/tests/unit/services/test_store.py index 8407d6ce4..55dadff43 100644 --- a/tests/unit/services/test_store.py +++ b/tests/unit/services/test_store.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for the store service.""" + import platform from typing import cast from unittest import mock @@ -34,7 +35,9 @@ @pytest.fixture def store(service_factory) -> services.StoreService: - store = services.StoreService(app=application.APP_METADATA, services=service_factory) + store = services.StoreService( + app=application.APP_METADATA, services=service_factory + ) store.client = mock.Mock(spec_set=client.Client) store.anonymous_client = mock.Mock(spec_set=client.AnonymousClient) return store @@ -48,7 +51,10 @@ def reusable_store(): def test_user_agent(store): - assert store._user_agent == f"Charmcraft/{charmcraft.__version__} ({store._ua_system_info})" + assert ( + store._user_agent + == f"Charmcraft/{charmcraft.__version__} ({store._ua_system_info})" + ) @pytest.mark.parametrize("system", ["Windows", "Macos"]) @@ -65,7 +71,10 @@ def test_ua_system_info_non_linux( monkeypatch.setattr(platform, "python_implementation", lambda: python) monkeypatch.setattr(platform, "python_version", lambda: python_version) - assert store._ua_system_info == f"{system} {release}; {machine}; {python} {python_version}" + assert ( + store._ua_system_info + == f"{system} {release}; {machine}; {python} {python_version}" + ) @pytest.mark.parametrize("machine", ["x86_64", "arm64", "riscv64"]) @@ -91,7 +100,9 @@ def test_ua_system_info_linux( def test_setup_with_error(emitter: RecordingEmitter, store): - store.ClientClass = mock.Mock(side_effect=[craft_store.errors.NoKeyringError, "I am a store!"]) + store.ClientClass = mock.Mock( + side_effect=[craft_store.errors.NoKeyringError, "I am a store!"] + ) store.setup() @@ -129,15 +140,23 @@ def test_login(reusable_store, permissions, description, ttl, channels): ) client.login.assert_called_once_with( - permissions=permissions, description=description, ttl=ttl, packages=None, channels=channels + permissions=permissions, + description=description, + ttl=ttl, + packages=None, + channels=channels, ) def test_login_failure(store): client = cast(mock.Mock, store.client) - client.login.side_effect = craft_store.errors.CredentialsAlreadyAvailable("charmcraft", "host") + client.login.side_effect = craft_store.errors.CredentialsAlreadyAvailable( + "charmcraft", "host" + ) - with pytest.raises(errors.CraftError, match="Cannot login because credentials were found"): + with pytest.raises( + errors.CraftError, match="Cannot login because credentials were found" + ): store.login() @@ -158,7 +177,11 @@ def test_logout(store): [ models.CharmResourceRevisionUpdateRequest( revision=123, - bases=[models.RequestCharmResourceBase(architectures=["amd64", "riscv64"])], + bases=[ + models.RequestCharmResourceBase( + architectures=["amd64", "riscv64"] + ) + ], ) ], ), @@ -170,7 +193,11 @@ def test_logout(store): [ models.CharmResourceRevisionUpdateRequest( revision=123, - bases=[models.RequestCharmResourceBase(architectures=["amd64", "riscv64"])], + bases=[ + models.RequestCharmResourceBase( + architectures=["amd64", "riscv64"] + ) + ], ), models.CharmResourceRevisionUpdateRequest( revision=456, @@ -180,7 +207,9 @@ def test_logout(store): ), ], ) -def test_set_resource_revisions_architectures_request_form(store, updates, expected_request): +def test_set_resource_revisions_architectures_request_form( + store, updates, expected_request +): store.client.list_resource_revisions.return_value = [] store.set_resource_revisions_architectures("my-charm", "my-file", updates) @@ -199,10 +228,18 @@ def test_set_resource_revisions_architectures_request_form(store, updates, expec ( {123: ["all"]}, [ - get_fake_revision(bases=[models.ResponseCharmResourceBase()], revision=0), - get_fake_revision(bases=[models.ResponseCharmResourceBase()], revision=123), + get_fake_revision( + bases=[models.ResponseCharmResourceBase()], revision=0 + ), + get_fake_revision( + bases=[models.ResponseCharmResourceBase()], revision=123 + ), + ], + [ + get_fake_revision( + bases=[models.ResponseCharmResourceBase()], revision=123 + ) ], - [get_fake_revision(bases=[models.ResponseCharmResourceBase()], revision=123)], ), ], ) @@ -251,12 +288,20 @@ def test_get_credentials(monkeypatch, store): ), ( [CharmLib(lib="my_charm.my_lib", version="1.0")], - [{"charm-name": "my-charm", "library-name": "my_lib", "api": 1, "patch": 0}], + [ + { + "charm-name": "my-charm", + "library-name": "my_lib", + "api": 1, + "patch": 0, + } + ], ), ], ) def test_fetch_libraries_metadata(monkeypatch, store, libs, expected_call): - store.get_libraries_metadata(libs) - store.anonymous_client.fetch_libraries_metadata.assert_called_once_with(expected_call) + store.anonymous_client.fetch_libraries_metadata.assert_called_once_with( + expected_call + ) diff --git a/tests/unit/store/test_client.py b/tests/unit/store/test_client.py index e739272e7..6177f3012 100644 --- a/tests/unit/store/test_client.py +++ b/tests/unit/store/test_client.py @@ -29,7 +29,9 @@ def client() -> store.Client: @pytest.fixture def anonymous_client() -> store.AnonymousClient: - return store.AnonymousClient("http://charmhub.local", "http://storage.charmhub.local") + return store.AnonymousClient( + "http://charmhub.local", "http://storage.charmhub.local" + ) @pytest.mark.parametrize( @@ -48,7 +50,9 @@ def anonymous_client() -> store.AnonymousClient: 0, 0, mock.call( - "GET", "/v1/charm/libraries/my-charm/abcdefg", params={"api": 0, "patch": 0} + "GET", + "/v1/charm/libraries/my-charm/abcdefg", + params={"api": 0, "patch": 0}, ), ), ], @@ -68,7 +72,9 @@ def test_get_library_success( ) monkeypatch.setattr(anonymous_client, "request_urlpath_json", mock_get_urlpath_json) - anonymous_client.get_library(charm_name=charm, library_id=lib_id, api=api, patch=patch) + anonymous_client.get_library( + charm_name=charm, library_id=lib_id, api=api, patch=patch + ) mock_get_urlpath_json.assert_has_calls([expected_call]) @@ -105,7 +111,9 @@ def test_get_library_success( ), ], ) -def test_fetch_libraries_metadata(monkeypatch, anonymous_client, libs, json_response, expected): +def test_fetch_libraries_metadata( + monkeypatch, anonymous_client, libs, json_response, expected +): mock_get_urlpath_json = mock.Mock(return_value=json_response) monkeypatch.setattr(anonymous_client, "request_urlpath_json", mock_get_urlpath_json) diff --git a/tests/unit/test_application.py b/tests/unit/test_application.py index c8cd28654..7a7405014 100644 --- a/tests/unit/test_application.py +++ b/tests/unit/test_application.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for application class.""" + import textwrap from unittest import mock @@ -70,7 +71,13 @@ ) @pytest.mark.parametrize( "expected", - [{"name": "test-charm", "summary": "A test charm", "description": "A charm for testing!"}], + [ + { + "name": "test-charm", + "summary": "A test charm", + "description": "A charm for testing!", + } + ], ) def test_extra_yaml_transform_success( fs: pyfakefs.fake_filesystem.FakeFilesystem, @@ -83,7 +90,9 @@ def test_extra_yaml_transform_success( fs.create_file("metadata.yaml", contents=metadata_yaml) app = application.Charmcraft(app=application.APP_METADATA, services=service_factory) - actual = app._extra_yaml_transform(charmcraft_dict, build_on="amd64", build_for=None) + actual = app._extra_yaml_transform( + charmcraft_dict, build_on="amd64", build_for=None + ) assert actual == expected @@ -224,7 +233,9 @@ def test_deprecated_prime_warning( }, id="named-reactive", ), - pytest.param({"parts": {"my-part": {"plugin": "reactive"}}}, id="reactive-plugin"), + pytest.param( + {"parts": {"my-part": {"plugin": "reactive"}}}, id="reactive-plugin" + ), pytest.param( { "parts": {"bundle": {}}, @@ -285,7 +296,9 @@ def test_expand_environment_multi_arch( ) -> None: mock_parent_expand_environment = mock.Mock() monkeypatch.setattr( - craft_application.Application, "_expand_environment", mock_parent_expand_environment + craft_application.Application, + "_expand_environment", + mock_parent_expand_environment, ) app = application.Charmcraft(app=application.APP_METADATA, services=service_factory) diff --git a/tests/unit/test_charm_builder.py b/tests/unit/test_charm_builder.py index 24fe5d300..553b06fd3 100644 --- a/tests/unit/test_charm_builder.py +++ b/tests/unit/test_charm_builder.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for CharmBuilder.""" + import pathlib import pytest @@ -77,7 +78,9 @@ def test_install_strict_dependencies_pip_failure( fs, fake_process: FakeProcess, builder, requirements ): fs.create_file("requirements.txt", contents=requirements) - no_binary_packages = utils.get_package_names(requirements.splitlines(keepends=False)) + no_binary_packages = utils.get_package_names( + requirements.splitlines(keepends=False) + ) no_binary_packages_str = ",".join(sorted(no_binary_packages)) fake_process.register( [ diff --git a/tests/unit/test_dispatch.py b/tests/unit/test_dispatch.py index 4e52170f9..dccfb92ad 100644 --- a/tests/unit/test_dispatch.py +++ b/tests/unit/test_dispatch.py @@ -15,7 +15,6 @@ # For further info, check https://github.com/canonical/charmcraft """Unit tests for dispatch script creation.""" - import pathlib import pytest @@ -52,7 +51,9 @@ def test_create_dispatch_no_entrypoint(fake_path: pathlib.Path, entrypoint): prime_dir.mkdir() dispatch_path = prime_dir / const.DISPATCH_FILENAME - pytest_check.is_false(dispatch.create_dispatch(prime_dir=prime_dir, entrypoint=entrypoint)) + pytest_check.is_false( + dispatch.create_dispatch(prime_dir=prime_dir, entrypoint=entrypoint) + ) pytest_check.is_false(dispatch_path.exists()) @@ -67,5 +68,7 @@ def test_create_dispatch_with_entrypoint(fake_path: pathlib.Path, entrypoint): dispatch_file = prime_dir / const.DISPATCH_FILENAME expected = dispatch.DISPATCH_SCRIPT_TEMPLATE.format(entrypoint=entrypoint) - pytest_check.is_true(dispatch.create_dispatch(prime_dir=prime_dir, entrypoint=entrypoint)) + pytest_check.is_true( + dispatch.create_dispatch(prime_dir=prime_dir, entrypoint=entrypoint) + ) pytest_check.equal(dispatch_file.read_text(), expected) diff --git a/tests/unit/test_parts.py b/tests/unit/test_parts.py index f3195c240..3292b8bce 100644 --- a/tests/unit/test_parts.py +++ b/tests/unit/test_parts.py @@ -35,12 +35,20 @@ {"charm-requirements": ["requirements.txt"]}, ), ( - {"charm-requirements": ["requirements.txt"], "charm-binary-python-packages": ["ops"]}, - {"charm-requirements": ["requirements.txt"], "charm-binary-python-packages": ["ops"]}, + { + "charm-requirements": ["requirements.txt"], + "charm-binary-python-packages": ["ops"], + }, + { + "charm-requirements": ["requirements.txt"], + "charm-binary-python-packages": ["ops"], + }, ), ], ) -def test_partconfig_strict_dependencies_success(fs: FakeFilesystem, part_config, expected): +def test_partconfig_strict_dependencies_success( + fs: FakeFilesystem, part_config, expected +): """Test various success scenarios for a charm part with strict dependencies.""" for file in part_config.get("charm-requirements", ["requirements.txt"]): fs.create_file(file, contents="ops~=2.5") @@ -61,10 +69,15 @@ def test_partconfig_strict_dependencies_success(fs: FakeFilesystem, part_config, {"charm-requirements": ["req.txt"], "charm-python-packages": ["ops"]}, "Value error, 'charm-python-packages' must not be set if 'charm-strict-dependencies' is enabled", ), - ({}, "Value error, 'charm-strict-dependencies' requires at least one requirements file."), + ( + {}, + "Value error, 'charm-strict-dependencies' requires at least one requirements file.", + ), ], ) -def test_partconfig_strict_dependencies_failure(fs: FakeFilesystem, part_config, message): +def test_partconfig_strict_dependencies_failure( + fs: FakeFilesystem, part_config, message +): """Test failure scenarios for a charm part with strict dependencies.""" for file in part_config.get("charm-requirements", []): fs.create_file(file, contents="ops==2.5.1\n") diff --git a/tests/unit/test_preprocess.py b/tests/unit/test_preprocess.py index 137d1f82a..e8f65360d 100644 --- a/tests/unit/test_preprocess.py +++ b/tests/unit/test_preprocess.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for project pre-processing functions.""" + import pathlib import textwrap @@ -21,7 +22,10 @@ from charmcraft import const, errors, preprocess -BASIC_BUNDLE = {"type": "bundle", "parts": {"bundle": {"plugin": "bundle", "source": "."}}} +BASIC_BUNDLE = { + "type": "bundle", + "parts": {"bundle": {"plugin": "bundle", "source": "."}}, +} BASIC_CHARM = {"type": "charm", "parts": {"charm": {"plugin": "charm", "source": "."}}} BASIC_BASES_CHARM = {**BASIC_CHARM, "bases": [{"name": "ubuntu", "channel": "22.04"}]} @@ -34,7 +38,11 @@ pytest.param(BASIC_BUNDLE.copy(), BASIC_BUNDLE, id="prefilled-bundle"), pytest.param( {"type": "charm", "bases": []}, - {"type": "charm", "bases": [], "parts": {"charm": {"plugin": "charm", "source": "."}}}, + { + "type": "charm", + "bases": [], + "parts": {"charm": {"plugin": "charm", "source": "."}}, + }, id="empty-charm", ), pytest.param(BASIC_CHARM.copy(), BASIC_CHARM, id="basic-charm"), @@ -50,7 +58,9 @@ def test_add_default_parts_correct(yaml_data, expected): ("yaml_data", "metadata_yaml", "expected"), [ pytest.param({}, None, {}, id="nonexistent"), - pytest.param({}, "{}", {"name": None, "summary": None, "description": None}, id="empty"), + pytest.param( + {}, "{}", {"name": None, "summary": None, "description": None}, id="empty" + ), pytest.param( {"name": "my-charm"}, "summary: a charm", @@ -116,7 +126,10 @@ def test_extra_yaml_transform_failure(fs, yaml_data, metadata_yaml, message): [ pytest.param({}, "", {}, id="non-bundle"), pytest.param( - {"type": "bundle"}, "{}", {"type": "bundle", "bundle": {}}, id="empty-bundle" + {"type": "bundle"}, + "{}", + {"type": "bundle", "bundle": {}}, + id="empty-bundle", ), ], ) @@ -156,7 +169,11 @@ def test_add_bundle_snippet_invalid_file(fs, contents): ("yaml_data", "config_yaml", "expected"), [ ({}, "{}", {"config": {}}), - ({}, "options:\n boop:\n type: int", {"config": {"options": {"boop": {"type": "int"}}}}), + ( + {}, + "options:\n boop:\n type: int", + {"config": {"options": {"boop": {"type": "int"}}}}, + ), ], ) def test_add_config_success(fs, yaml_data, config_yaml, expected): diff --git a/tests/unit/utils/test_charmlibs.py b/tests/unit/utils/test_charmlibs.py index 4bd9b8931..57082007f 100644 --- a/tests/unit/utils/test_charmlibs.py +++ b/tests/unit/utils/test_charmlibs.py @@ -15,6 +15,7 @@ # For further info, check https://github.com/canonical/charmcraft """Tests for store helpers commands (code in store/charmlibs.py).""" + import hashlib import pathlib import sys @@ -37,7 +38,8 @@ @pytest.mark.parametrize( - ("value", "expected"), [("my-charm.my_lib", QualifiedLibraryName("my_charm", "my_lib"))] + ("value", "expected"), + [("my-charm.my_lib", QualifiedLibraryName("my_charm", "my_lib"))], ) def test_qualified_library_name_from_string_success( value: str, expected: QualifiedLibraryName @@ -46,7 +48,8 @@ def test_qualified_library_name_from_string_success( @pytest.mark.parametrize( - ("value", "expected"), [(QualifiedLibraryName("my_charm", "my_lib"), "my-charm.my_lib")] + ("value", "expected"), + [(QualifiedLibraryName("my_charm", "my_lib"), "my-charm.my_lib")], ) def test_qualified_library_name_to_string_success( value: str, expected: QualifiedLibraryName @@ -290,7 +293,10 @@ def test_getlibinfo_missing_library_from_name(): assert lib_data.content_hash is None assert lib_data.content is None assert lib_data.full_name == test_name - assert lib_data.path == pathlib.Path("lib") / "charms" / "testcharm" / "v3" / "testlib.py" + assert ( + lib_data.path + == pathlib.Path("lib") / "charms" / "testcharm" / "v3" / "testlib.py" + ) assert lib_data.lib_name == "testlib" assert lib_data.charm_name == "testcharm" @@ -365,13 +371,18 @@ def test_getlibinternals_success_content(tmp_path, monkeypatch): internals = get_lib_internals(test_path) assert internals.content == test_path.read_text(encoding="utf8") - assert internals.content_hash == hashlib.sha256(extra_content.encode("utf8")).hexdigest() + assert ( + internals.content_hash + == hashlib.sha256(extra_content.encode("utf8")).hexdigest() + ) def test_getlibinternals_non_toplevel_names(tmp_path, monkeypatch): """Test non direct assignments.""" monkeypatch.chdir(tmp_path) - test_path = _create_lib(extra_content="logging.getLogger('kazoo.client').disabled = True") + test_path = _create_lib( + extra_content="logging.getLogger('kazoo.client').disabled = True" + ) internals = get_lib_internals(test_path) assert internals.lib_id == "test-lib-id" @@ -402,7 +413,9 @@ def test_getlibinternals_malformed_content(tmp_path, monkeypatch): (["metadata_patch", "metadata_id"], "LIBID, LIBPATCH"), ], ) -def test_getlibinternals_missing_internals_field(tmp_path, empty_args, missing, monkeypatch): +def test_getlibinternals_missing_internals_field( + tmp_path, empty_args, missing, monkeypatch +): """Some internals field is not present.""" monkeypatch.chdir(tmp_path) kwargs = {arg: "" for arg in empty_args} @@ -565,7 +578,9 @@ def test_collectpydeps_generic(tmp_path, monkeypatch): otherdir = tmp_path / "otherdir" otherdir.mkdir() monkeypatch.chdir(otherdir) - _create_lib(charm_name="charm1", lib_name="lib1.py", pydeps="PYDEPS = ['foo', 'bar']") + _create_lib( + charm_name="charm1", lib_name="lib1.py", pydeps="PYDEPS = ['foo', 'bar']" + ) _create_lib(charm_name="charm1", lib_name="lib2.py", pydeps="PYDEPS = ['bar']") _create_lib(charm_name="charm2", lib_name="lib3.py") _create_lib(charm_name="charm2", lib_name="lib3.py", pydeps="PYDEPS = ['baz']") diff --git a/tests/unit/utils/test_cli.py b/tests/unit/utils/test_cli.py index 971f36795..c439bdb1a 100644 --- a/tests/unit/utils/test_cli.py +++ b/tests/unit/utils/test_cli.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for CLI-related utilities.""" + import datetime import json from unittest.mock import call, patch @@ -171,7 +172,9 @@ def test_confirm_with_user(user_input, expected, mock_input, mock_isatty): assert mock_input.mock_calls == [call("prompt [y/N]: ")] -def test_confirm_with_user_errors_in_managed_mode(mock_is_charmcraft_running_in_managed_mode): +def test_confirm_with_user_errors_in_managed_mode( + mock_is_charmcraft_running_in_managed_mode, +): mock_is_charmcraft_running_in_managed_mode.return_value = True with pytest.raises(RuntimeError): diff --git a/tests/unit/utils/test_file.py b/tests/unit/utils/test_file.py index 04efda2cc..8652281f1 100644 --- a/tests/unit/utils/test_file.py +++ b/tests/unit/utils/test_file.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft """Unit tests for file-related utilities.""" + import os import pathlib import sys diff --git a/tests/unit/utils/test_package.py b/tests/unit/utils/test_package.py index 693ce9599..2adf26dcc 100644 --- a/tests/unit/utils/test_package.py +++ b/tests/unit/utils/test_package.py @@ -65,8 +65,12 @@ def test_get_package_names(packages, expected): [ pytest.param(set(), set(), set(), id="empty"), pytest.param({"abc==1.0.0"}, {"abc"}, set(), id="make-empty"), - pytest.param({"abc==1.0.0", "def==1.2.3"}, {"abc"}, {"def==1.2.3"}, id="remove-one"), - pytest.param({"abc==1.0.0"}, {"invalid"}, {"abc==1.0.0"}, id="irrelevant-exclusion"), + pytest.param( + {"abc==1.0.0", "def==1.2.3"}, {"abc"}, {"def==1.2.3"}, id="remove-one" + ), + pytest.param( + {"abc==1.0.0"}, {"invalid"}, {"abc==1.0.0"}, id="irrelevant-exclusion" + ), ], ) def test_exclude_packages(requirements, excluded, expected): @@ -110,20 +114,37 @@ def test_get_requirements_file_package_names(tmp_path, file_contents, expected): ["ghi", "jkl"], ), (["abc==1.0.0", "def>=1.2.3"], [], [], "--no-binary=:all:", []), - ([], ["abc==1.0.0", "def>=1.2.3"], [], "--no-binary=:all:", ["abc==1.0.0", "def>=1.2.3"]), + ( + [], + ["abc==1.0.0", "def>=1.2.3"], + [], + "--no-binary=:all:", + ["abc==1.0.0", "def>=1.2.3"], + ), ], ) -@pytest.mark.parametrize("prefix", [["/bin/pip"], ["/some/path/to/pip3"], ["pip", "--some-param"]]) +@pytest.mark.parametrize( + "prefix", [["/bin/pip"], ["/some/path/to/pip3"], ["pip", "--some-param"]] +) def test_get_pip_command( - prefix, requirements, source_deps, binary_deps, expected_no_binary, expected_other_packages + prefix, + requirements, + source_deps, + binary_deps, + expected_no_binary, + expected_other_packages, ): with tempfile.TemporaryDirectory() as tmp_dir: path = pathlib.Path(tmp_dir, "requirements.txt") path.write_text("\n".join(requirements)) - command = get_pip_command(prefix, [path], source_deps=source_deps, binary_deps=binary_deps) + command = get_pip_command( + prefix, [path], source_deps=source_deps, binary_deps=binary_deps + ) assert command[: len(prefix)] == prefix - actual_no_binary, actual_requirement, *actual_other_packgaes = command[len(prefix) :] + actual_no_binary, actual_requirement, *actual_other_packgaes = command[ + len(prefix) : + ] assert actual_no_binary == expected_no_binary assert actual_other_packgaes == expected_other_packages assert actual_requirement == f"--requirement={path}" @@ -132,7 +153,11 @@ def test_get_pip_command( @pytest.mark.parametrize( ("pip_cmd", "stdout", "expected"), [ - ("pip", "pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)\n", (22, 0, 2)), + ( + "pip", + "pip 22.0.2 from /usr/lib/python3/dist-packages/pip (python 3.10)\n", + (22, 0, 2), + ), ( "venv/bin/pip", "pip 20.0.2 from /root/venv/lib/python3.8/site-packages/pip (python 3.8)", @@ -187,7 +212,9 @@ def test_validate_strict_dependencies_success(dependencies, other_packages): ([], ["zyx", "wvut"], ["wvut", "zyx"]), ], ) -def test_validate_strict_dependencies_missing(dependencies, other_packages, extra_packages): +def test_validate_strict_dependencies_missing( + dependencies, other_packages, extra_packages +): with pytest.raises(MissingDependenciesError) as exc_info: validate_strict_dependencies(dependencies, other_packages) diff --git a/tests/unit/utils/test_platform.py b/tests/unit/utils/test_platform.py index 2f342e338..8815c4735 100644 --- a/tests/unit/utils/test_platform.py +++ b/tests/unit/utils/test_platform.py @@ -101,11 +101,15 @@ ], ) @pytest.mark.parametrize("machine", ["x86_64", "riscv64", "arm64"]) -def test_get_os_platform_linux(tmp_path, os_release, expected_system, expected_release, machine): +def test_get_os_platform_linux( + tmp_path, os_release, expected_system, expected_release, machine +): """Utilize an /etc/os-release file to determine platform.""" filepath = tmp_path / "os-release" filepath.write_text(os_release) - with patch("distro.distro._distro", distro.LinuxDistribution(os_release_file=filepath)): + with patch( + "distro.distro._distro", distro.LinuxDistribution(os_release_file=filepath) + ): with patch("platform.machine", return_value=machine): with patch("platform.system", return_value="Linux"): os_platform = get_os_platform(filepath) @@ -126,7 +130,9 @@ def test_get_os_platform_non_linux(system, release, machine): assert os_platform == OSPlatform(system, release, machine) -@given(strategies.iterables(strategies.sampled_from(sorted(const.SUPPORTED_ARCHITECTURES)))) +@given( + strategies.iterables(strategies.sampled_from(sorted(const.SUPPORTED_ARCHITECTURES))) +) def test_validate_architectures_valid_values(architectures): validate_architectures(architectures) diff --git a/tests/unit/utils/test_project.py b/tests/unit/utils/test_project.py index 7960a76e7..80db5f09b 100644 --- a/tests/unit/utils/test_project.py +++ b/tests/unit/utils/test_project.py @@ -75,8 +75,13 @@ def test_find_charm_sources_extra_charms(tmp_path, build_charm_directory, fake_c @pytest.mark.parametrize("fake_charms", [BASIC_CHARM_MAP]) -def test_find_charm_sources_non_matching_path(tmp_path, build_charm_directory, fake_charms): - charms = {name: path.with_name(f"non_matching_{name}") for name, path in fake_charms.items()} +def test_find_charm_sources_non_matching_path( + tmp_path, build_charm_directory, fake_charms +): + charms = { + name: path.with_name(f"non_matching_{name}") + for name, path in fake_charms.items() + } build_charm_directory(tmp_path, charms) actual = find_charm_sources(tmp_path, fake_charms) @@ -137,7 +142,10 @@ def test_get_charm_name_from_path_bundle(tmp_path, build_charm_directory, name, with pytest.raises(InvalidCharmPathError) as exc_info: get_charm_name_from_path(full_path) - assert exc_info.value.args[0] == f"Path does not contain source for a valid charm: {full_path}" + assert ( + exc_info.value.args[0] + == f"Path does not contain source for a valid charm: {full_path}" + ) @pytest.mark.parametrize( @@ -158,7 +166,10 @@ def test_get_charm_name_from_path_missing_file( with pytest.raises(InvalidCharmPathError) as exc_info: get_charm_name_from_path(full_path) - assert exc_info.value.args[0] == f"Path does not contain source for a valid charm: {full_path}" + assert ( + exc_info.value.args[0] + == f"Path does not contain source for a valid charm: {full_path}" + ) @pytest.mark.parametrize( @@ -169,7 +180,9 @@ def test_get_charm_name_from_path_missing_file( ("test1", "operators/test1"), ], ) -def test_get_charm_name_from_path_wrong_name(tmp_path, build_charm_directory, name, path): +def test_get_charm_name_from_path_wrong_name( + tmp_path, build_charm_directory, name, path +): build_charm_directory(tmp_path, {name: path}, file_type="bundle") full_path = tmp_path / path with (full_path / const.METADATA_FILENAME).open("w") as file: @@ -178,4 +191,7 @@ def test_get_charm_name_from_path_wrong_name(tmp_path, build_charm_directory, na with pytest.raises(InvalidCharmPathError) as exc_info: get_charm_name_from_path(full_path) - assert exc_info.value.args[0] == f"Path does not contain source for a valid charm: {full_path}" + assert ( + exc_info.value.args[0] + == f"Path does not contain source for a valid charm: {full_path}" + ) diff --git a/tests/unit/utils/test_skopeo.py b/tests/unit/utils/test_skopeo.py index 45ce80c99..31c74afaa 100644 --- a/tests/unit/utils/test_skopeo.py +++ b/tests/unit/utils/test_skopeo.py @@ -66,7 +66,9 @@ def test_find_skopeo_success(fake_process): ("kwargs", "expected"), [ pytest.param({}, [], id="empty"), - pytest.param({"insecure_policy": True}, ["--insecure-policy"], id="insecure_policy"), + pytest.param( + {"insecure_policy": True}, ["--insecure-policy"], id="insecure_policy" + ), pytest.param({"arch": "amd64"}, ["--override-arch", "amd64"], id="amd64"), pytest.param({"arch": "arm64"}, ["--override-arch", "arm64"], id="arm64"), pytest.param({"arch": "riscv64"}, ["--override-arch", "riscv64"], id="riscv64"), @@ -103,14 +105,25 @@ def fake_skopeo(fake_process): ({"preserve_digests": True}, ["--preserve-digests"]), ({"source_username": "user"}, ["--src-creds", "user"]), ({"source_password": "pass"}, ["--src-password", "pass"]), - ({"source_username": "user", "source_password": "pass"}, ["--src-creds", "user:pass"]), + ( + {"source_username": "user", "source_password": "pass"}, + ["--src-creds", "user:pass"], + ), ({"dest_username": "user"}, ["--dest-creds", "user"]), ({"dest_password": "pass"}, ["--dest-password", "pass"]), - ({"dest_username": "user", "dest_password": "pass"}, ["--dest-creds", "user:pass"]), + ( + {"dest_username": "user", "dest_password": "pass"}, + ["--dest-creds", "user:pass"], + ), ], ) def test_get_copy_command( - fake_process, fake_skopeo: Skopeo, source_image, destination_image, kwargs, expected_args + fake_process, + fake_skopeo: Skopeo, + source_image, + destination_image, + kwargs, + expected_args, ): fake_process.register( [ diff --git a/tests/unit/utils/test_store.py b/tests/unit/utils/test_store.py index 330ea3780..09b59bf67 100644 --- a/tests/unit/utils/test_store.py +++ b/tests/unit/utils/test_store.py @@ -14,12 +14,16 @@ # # For further info, check https://github.com/canonical/charmcraft """Tests for store helpers.""" + from hypothesis import given, strategies from charmcraft import utils -@given(charms=strategies.lists(strategies.text()), bundles=strategies.lists(strategies.text())) +@given( + charms=strategies.lists(strategies.text()), + bundles=strategies.lists(strategies.text()), +) def test_get_packages(charms, bundles): packages = utils.get_packages(charms=charms, bundles=bundles) result_names = [package.package_name for package in packages] From 9583793226b73478b9af24d649f0c1ca2b83c9bb Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Fri, 11 Oct 2024 18:11:43 -0400 Subject: [PATCH 5/6] chore: remove black config from pyproject.toml --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d46857efd..b9459d15e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,10 +112,6 @@ git_describe_command = "git describe --long --match '[0-9]*.[0-9]*.[0-9]*' --exc include = ["*craft*"] namespaces = false -[tool.black] -target-version = ["py310", "py311"] -line-length = 99 - [tool.codespell] ignore-words-list = "buildd,crate,keyserver,comandos,ro,dedent,dedented,tread,socio-economic" skip = "requirements*.txt,.tox,.git,build,.*_cache,__pycache__,*.tar,*.snap,*.png,./node_modules,./docs/_build,.direnv,.venv,venv,.vscode,charmcraft.spec" From f3b7108efff062339ba96c922dd349751f4b189f Mon Sep 17 00:00:00 2001 From: Alex Lowe Date: Fri, 11 Oct 2024 18:20:50 -0400 Subject: [PATCH 6/6] style(type): fix linting from autoformat --- charmcraft/parts/lifecycle.py | 6 ++++-- charmcraft/services/provider.py | 5 ++++- charmcraft/utils/yaml.py | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/charmcraft/parts/lifecycle.py b/charmcraft/parts/lifecycle.py index 8875d4499..2cff33b32 100644 --- a/charmcraft/parts/lifecycle.py +++ b/charmcraft/parts/lifecycle.py @@ -107,8 +107,10 @@ def run(self, target_step: Step) -> None: f"Running step {act.step.name} for part {act.part_name!r}" ) with instrum.Timer( - "Running step", step=act.step.name, part=act.part_name - ): # type: ignore[arg-type] + "Running step", + step=act.step.name, # type: ignore[arg-type] + part=act.part_name, # type: ignore[arg-type] + ): with emit.open_stream("Execute action") as stream: aex.execute([act], stdout=stream, stderr=stream) executor_timer.mark("Context exit") diff --git a/charmcraft/services/provider.py b/charmcraft/services/provider.py index ba3c03c85..704a98f6f 100644 --- a/charmcraft/services/provider.py +++ b/charmcraft/services/provider.py @@ -118,7 +118,10 @@ def instance( ) -> Generator[craft_providers.Executor, None, None]: """Instance override for Charmcraft.""" with super().instance( - build_info, work_dir=work_dir, allow_unstable=allow_unstable, **kwargs + build_info, + work_dir=work_dir, + allow_unstable=allow_unstable, + **kwargs, # type: ignore[arg-type] ) as instance: try: yield instance diff --git a/charmcraft/utils/yaml.py b/charmcraft/utils/yaml.py index fe1ffa684..c976bd67f 100644 --- a/charmcraft/utils/yaml.py +++ b/charmcraft/utils/yaml.py @@ -50,8 +50,8 @@ def dump_yaml(data: Any) -> str: # noqa: ANN401: yaml.dump takes anything, so w yaml.add_representer(str, _repr_str, Dumper=yaml.SafeDumper) yaml.add_representer( pydantic.AnyHttpUrl, - _repr_str, - Dumper=yaml.SafeDumper, # type: ignore[arg-type] + _repr_str, # type: ignore[arg-type] + Dumper=yaml.SafeDumper, ) yaml.add_representer( const.CharmArch,