diff --git a/charmcraft/application/commands/extensions.py b/charmcraft/application/commands/extensions.py index 5ec436565..ba05ce786 100644 --- a/charmcraft/application/commands/extensions.py +++ b/charmcraft/application/commands/extensions.py @@ -21,8 +21,9 @@ from craft_cli import emit -from charmcraft import extensions, utils +from charmcraft import utils from charmcraft.application.commands import base +from charmcraft.extensions import registry class ListExtensionsCommand(base.CharmcraftCommand): @@ -40,7 +41,7 @@ class ListExtensionsCommand(base.CharmcraftCommand): def run(self, parsed_args: argparse.Namespace): """Print the list of available extensions and their bases.""" - extension_data = extensions.registry.get_extensions() + extension_data = registry.get_extensions() if not parsed_args.format: extension_data = [ diff --git a/charmcraft/store/models.py b/charmcraft/store/models.py index a2bab62bb..f65703a5e 100644 --- a/charmcraft/store/models.py +++ b/charmcraft/store/models.py @@ -125,7 +125,7 @@ class Revision: created_at: datetime.datetime status: str errors: list[Error] - bases: list[Base] + bases: list[Base | None] @dataclasses.dataclass(frozen=True) @@ -152,7 +152,7 @@ class Release: channel: str expires_at: datetime.datetime resources: list[Resource] - base: Base + base: Base | None @dataclasses.dataclass(frozen=True) diff --git a/charmcraft/store/store.py b/charmcraft/store/store.py index d82b75b4f..1f2e44fd0 100644 --- a/charmcraft/store/store.py +++ b/charmcraft/store/store.py @@ -86,7 +86,7 @@ def _build_errors(item): def _build_revision(item: dict[str, Any]) -> Revision: """Build a Revision from a response item.""" - bases = [Base(**base) for base in item["bases"] if base is not None] + bases = [(None if base is None else Base(**base)) for base in item["bases"]] return Revision( revision=item["revision"], version=item["version"], @@ -401,7 +401,7 @@ def list_releases( # `datetime.datetime.fromisoformat` is available only since Py3.7 expires_at = parser.parse(expires_at) resources = [_build_resource(r) for r in item["resources"]] - base = Base(**item["base"]) + base = None if item["base"] is None else Base(**item["base"]) channel_map.append( Release( revision=item["revision"], diff --git a/pyproject.toml b/pyproject.toml index f7f1b2f07..0b1de8845 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -166,7 +166,6 @@ include = [ "tests/integration", ] analyzeUnannotatedFunctions = false -reportAttributeAccessIssue = "warning" reportIncompatibleVariableOverride = "warning" reportOptionalMemberAccess = "warning" diff --git a/tests/integration/commands/test_init.py b/tests/integration/commands/test_init.py index 9108a7d1e..3248e5358 100644 --- a/tests/integration/commands/test_init.py +++ b/tests/integration/commands/test_init.py @@ -30,7 +30,7 @@ import charmcraft from charmcraft import errors -from charmcraft.application import commands +from charmcraft.application.commands import init from charmcraft.utils import S_IXALL with contextlib.suppress(ImportError): @@ -97,7 +97,7 @@ @pytest.fixture def init_command(): - return commands.InitCommand( + return init.InitCommand( {"app": charmcraft.application.APP_METADATA, "services": None} ) @@ -107,7 +107,7 @@ def create_namespace( name="my-charm", author="J Doe", force=False, - profile=commands.init.DEFAULT_PROFILE, + profile=init.DEFAULT_PROFILE, project_dir: pathlib.Path | None = None, ): """Helper to create a valid namespace.""" @@ -269,7 +269,7 @@ def test_executable_set(new_path, init_command): bool(os.getenv("RUNNING_TOX")) and sys.version_info < (3, 11), reason="does not work inside tox in Python3.10 and below", ) -@pytest.mark.parametrize("profile", list(commands.init.PROFILES)) +@pytest.mark.parametrize("profile", list(init.PROFILES)) def test_tox_success(new_path, init_command, profile): # fix the PYTHONPATH and PATH so the tests in the initted environment use our own # virtualenv libs and bins (if any), as they need them, but we're not creating a diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py index 2fc1975f7..ba94a2acf 100644 --- a/tests/unit/commands/test_lifecycle.py +++ b/tests/unit/commands/test_lifecycle.py @@ -17,6 +17,7 @@ import argparse import pathlib +from unittest import mock import craft_cli import pytest @@ -133,13 +134,12 @@ def test_pack_update_charm_libs_empty( simple_charm, emitter: RecordingEmitter, service_factory: services.ServiceFactory, + mock_store_anonymous_client: mock.Mock, ): 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.get_library.return_value = store_lib + mock_store_anonymous_client.fetch_libraries_metadata.return_value = [store_lib] + mock_store_anonymous_client.get_library.return_value = store_lib pack._update_charm_libs() @@ -156,16 +156,15 @@ def test_pack_update_charm_libs_no_update( simple_charm, emitter: RecordingEmitter, service_factory: services.ServiceFactory, + mock_store_anonymous_client: mock.Mock, ): 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") 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.get_library.return_value = store_lib + mock_store_anonymous_client.fetch_libraries_metadata.return_value = [store_lib] + mock_store_anonymous_client.get_library.return_value = store_lib pack._update_charm_libs() @@ -181,16 +180,15 @@ def test_pack_update_charm_libs_needs_update( simple_charm, emitter: RecordingEmitter, service_factory: services.ServiceFactory, + mock_store_anonymous_client: mock.Mock, ): simple_charm.charm_libs = [models.CharmLib(lib="my_charm.my_lib", version="0.2")] store_lib = Library("lib_id", "my_lib", "my_charm", 0, 2, "Lib contents", "hash") 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.get_library.return_value = store_lib + mock_store_anonymous_client.fetch_libraries_metadata.return_value = [store_lib] + mock_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 512f5cbf7..1fe481f82 100644 --- a/tests/unit/commands/test_store.py +++ b/tests/unit/commands/test_store.py @@ -340,7 +340,7 @@ def test_register_bundle_warning(monkeypatch: pytest.MonkeyPatch, emitter): emitter.assert_progress( "\u001b[31mWARNING:\u001b[0m New bundle registration will stop working on 2024-11-01. For " - f"more information, see: {commands.store.BUNDLE_REGISTRATION_REMOVAL_URL}", + f"more information, see: {store_commands.BUNDLE_REGISTRATION_REMOVAL_URL}", permanent=True, ) mock_store.assert_called() @@ -358,6 +358,6 @@ def test_register_bundle_error(monkeypatch: pytest.MonkeyPatch, emitter): emitter.assert_message( "\u001b[31mERROR:\u001b[0m New bundle registration is discontinued as of 2024-11-01. For " - f"more information, see: {commands.store.BUNDLE_REGISTRATION_REMOVAL_URL}", + f"more information, see: {store_commands.BUNDLE_REGISTRATION_REMOVAL_URL}", ) mock_store.assert_not_called() diff --git a/tests/unit/test_parts.py b/tests/unit/test_parts.py index 3292b8bce..07c30ab00 100644 --- a/tests/unit/test_parts.py +++ b/tests/unit/test_parts.py @@ -14,6 +14,7 @@ # # For further info, check https://github.com/canonical/charmcraft +import pydantic import pytest from pyfakefs.fake_filesystem import FakeFilesystem @@ -84,7 +85,7 @@ def test_partconfig_strict_dependencies_failure( part_config.update(MINIMAL_STRICT_CHARM) - with pytest.raises(Exception) as exc_info: + with pytest.raises(pydantic.ValidationError) as exc_info: parts.process_part_config(part_config) assert message in {e["msg"] for e in exc_info.value.errors()}