diff --git a/requirements-devel.txt b/requirements-devel.txt
index 76bd289e11..0278ca317d 100644
--- a/requirements-devel.txt
+++ b/requirements-devel.txt
@@ -11,6 +11,7 @@ click==8.1.7
codespell==2.2.6
colorama==0.4.6
coverage==7.4.0
+craft-application==1.2.1
craft-archives==1.1.3
craft-cli==2.5.1
craft-grammar==1.1.2
diff --git a/requirements.txt b/requirements.txt
index 5e83e098f2..e12a3420d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,6 +5,7 @@ cffi==1.16.0
chardet==5.2.0
charset-normalizer==3.3.2
click==8.1.7
+craft-application==1.2.1
craft-archives==1.1.3
craft-cli==2.5.1
craft-grammar==1.1.2
diff --git a/setup.py b/setup.py
index 2d55d28f73..6490822a2f 100755
--- a/setup.py
+++ b/setup.py
@@ -97,6 +97,7 @@ def recursive_data_files(directory, install_directory):
"attrs",
"catkin-pkg; sys_platform == 'linux'",
"click",
+ "craft-application",
"craft-archives",
"craft-cli",
"craft-grammar",
@@ -154,7 +155,7 @@ def recursive_data_files(directory, install_directory):
entry_points=dict(
console_scripts=[
"snapcraft_legacy = snapcraft_legacy.cli.__main__:run",
- "snapcraft = snapcraft.cli:run",
+ "snapcraft = snapcraft.application:main",
]
),
data_files=(
diff --git a/snapcraft/__main__.py b/snapcraft/__main__.py
index 8675237d10..fc8b55b821 100644
--- a/snapcraft/__main__.py
+++ b/snapcraft/__main__.py
@@ -18,6 +18,6 @@
import sys
-from snapcraft import cli
+from snapcraft import application
-sys.exit(cli.run())
+sys.exit(application.main())
diff --git a/snapcraft/application.py b/snapcraft/application.py
new file mode 100644
index 0000000000..d2e83264d9
--- /dev/null
+++ b/snapcraft/application.py
@@ -0,0 +1,244 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Main Snapcraft Application."""
+
+from __future__ import annotations
+
+import os
+import signal
+import sys
+
+import craft_cli
+from craft_application import Application, AppMetadata, util
+from craft_cli import emit
+from overrides import override
+
+from snapcraft import cli, errors, models, services
+from snapcraft.commands import unimplemented
+
+APP_METADATA = AppMetadata(
+ name="snapcraft",
+ summary="Package, distribute, and update snaps for Linux and IoT",
+ ProjectClass=models.Project,
+ source_ignore_patterns=["*.snap"],
+)
+
+
+class Snapcraft(Application):
+ """Snapcraft application definition."""
+
+ @override
+ def _configure_services(self, platform: str | None, build_for: str | None) -> None:
+ if build_for is None:
+ build_for = util.get_host_architecture()
+
+ self.services.set_kwargs("package", platform=platform, build_for=build_for)
+ super()._configure_services(platform, build_for)
+
+ @property
+ def command_groups(self):
+ """Short-circuit the standard command groups for now."""
+ # TODO: Remove this once we've got lifecycle commands and version migrated.
+ return self._command_groups
+
+ def run(self) -> int:
+ """Fall back to the old snapcraft entrypoint."""
+ self._get_dispatcher()
+ raise errors.ClassicFallback()
+
+ @override
+ def _get_dispatcher(self) -> craft_cli.Dispatcher:
+ """Configure this application. Should be called by the run method.
+
+ Side-effect: This method may exit the process.
+
+ :returns: A ready-to-run Dispatcher object
+ """
+ # Set the logging level to DEBUG for all craft-libraries. This is OK even if
+ # the specific application doesn't use a specific library, the call does not
+ # import the package.
+ util.setup_loggers(*self._cli_loggers)
+
+ craft_cli.emit.init(
+ mode=craft_cli.EmitterMode.BRIEF,
+ appname=self.app.name,
+ greeting=f"Starting {self.app.name}",
+ log_filepath=self.log_path,
+ streaming_brief=True,
+ )
+
+ dispatcher = craft_cli.Dispatcher(
+ self.app.name,
+ self.command_groups,
+ summary=str(self.app.summary),
+ extra_global_args=self._global_arguments,
+ # TODO: craft-application should allow setting the default command without
+ # overriding `_get_dispatcher()`
+ default_command=unimplemented.Pack,
+ )
+
+ try:
+ craft_cli.emit.trace("pre-parsing arguments...")
+ # Workaround for the fact that craft_cli requires a command.
+ # https://github.com/canonical/craft-cli/issues/141
+ if "--version" in sys.argv or "-V" in sys.argv:
+ try:
+ global_args = dispatcher.pre_parse_args(["pull", *sys.argv[1:]])
+ except craft_cli.ArgumentParsingError:
+ global_args = dispatcher.pre_parse_args(sys.argv[1:])
+ else:
+ global_args = dispatcher.pre_parse_args(sys.argv[1:])
+
+ if global_args.get("version"):
+ craft_cli.emit.ended_ok()
+ print(f"{self.app.name} {self.app.version}")
+ sys.exit(0)
+ except craft_cli.ProvideHelpException as err:
+ print(err, file=sys.stderr) # to stderr, as argparse normally does
+ craft_cli.emit.ended_ok()
+ sys.exit(0)
+ except craft_cli.ArgumentParsingError as err:
+ print(err, file=sys.stderr) # to stderr, as argparse normally does
+ craft_cli.emit.ended_ok()
+ sys.exit(64) # Command line usage error from sysexits.h
+ except KeyboardInterrupt as err:
+ self._emit_error(craft_cli.CraftError("Interrupted."), cause=err)
+ sys.exit(128 + signal.SIGINT)
+ # pylint: disable-next=broad-exception-caught
+ except Exception as err: # noqa: BLE001
+ self._emit_error(
+ craft_cli.CraftError(
+ f"Internal error while loading {self.app.name}: {err!r}"
+ )
+ )
+ if os.getenv("CRAFT_DEBUG") == "1":
+ raise
+ sys.exit(70) # EX_SOFTWARE from sysexits.h
+
+ craft_cli.emit.trace("Preparing application...")
+ self.configure(global_args)
+
+ return dispatcher
+
+
+def main() -> int:
+ """Run craft-application based snapcraft with classic fallback."""
+ util.setup_loggers(
+ "craft_parts", "craft_providers", "craft_store", "snapcraft.remote"
+ )
+
+ snapcraft_services = services.SnapcraftServiceFactory(app=APP_METADATA)
+
+ app = Snapcraft(app=APP_METADATA, services=snapcraft_services)
+
+ app.add_command_group(
+ "Lifecycle",
+ [
+ unimplemented.Clean,
+ unimplemented.Pull,
+ unimplemented.Build,
+ unimplemented.Stage,
+ unimplemented.Prime,
+ unimplemented.Pack,
+ unimplemented.RemoteBuild,
+ unimplemented.Snap, # Hidden (legacy compatibility)
+ unimplemented.Plugins,
+ unimplemented.ListPlugins,
+ unimplemented.Try,
+ ],
+ )
+ app.add_command_group(
+ "Extensions",
+ [
+ unimplemented.ListExtensions,
+ unimplemented.Extensions,
+ unimplemented.ExpandExtensions,
+ ],
+ )
+ app.add_command_group(
+ "Store Account",
+ [
+ unimplemented.Login,
+ unimplemented.ExportLogin,
+ unimplemented.Logout,
+ unimplemented.Whoami,
+ ],
+ )
+ app.add_command_group(
+ "Store Snap Names",
+ [
+ unimplemented.Register,
+ unimplemented.Names,
+ unimplemented.ListRegistered,
+ unimplemented.List,
+ unimplemented.Metrics,
+ unimplemented.UploadMetadata,
+ ],
+ )
+ app.add_command_group(
+ "Store Snap Release Management",
+ [
+ unimplemented.Release,
+ unimplemented.Close,
+ unimplemented.Status,
+ unimplemented.Upload,
+ unimplemented.Push,
+ unimplemented.Promote,
+ unimplemented.ListRevisions,
+ unimplemented.Revisions,
+ ],
+ )
+ app.add_command_group(
+ "Store Snap Tracks",
+ [
+ unimplemented.ListTracks,
+ unimplemented.Tracks,
+ unimplemented.SetDefaultTrack,
+ ],
+ )
+ app.add_command_group(
+ "Store Key Management",
+ [
+ unimplemented.CreateKey,
+ unimplemented.RegisterKey,
+ unimplemented.SignBuild,
+ unimplemented.ListKeys,
+ ],
+ )
+ app.add_command_group(
+ "Store Validation Sets",
+ [
+ unimplemented.EditValidationSets,
+ unimplemented.ListValidationSets,
+ unimplemented.Validate,
+ unimplemented.Gated,
+ ],
+ )
+ app.add_command_group(
+ "Other",
+ [
+ unimplemented.Version,
+ unimplemented.Lint,
+ unimplemented.Init,
+ ],
+ )
+
+ try:
+ return app.run()
+ except errors.ClassicFallback:
+ emit.debug("Falling back from craft-application to snapcraft.")
+ return cli.run()
diff --git a/snapcraft/cli.py b/snapcraft/cli.py
index 829c76ec68..0e364df84a 100644
--- a/snapcraft/cli.py
+++ b/snapcraft/cli.py
@@ -192,19 +192,6 @@ def get_dispatcher() -> craft_cli.Dispatcher:
_ORIGINAL_LIB_NAME_LOG_LEVEL[lib_name] = logger.level
logger.setLevel(logging.DEBUG)
- if utils.is_managed_mode():
- log_filepath = utils.get_managed_environment_log_path()
- else:
- log_filepath = None
-
- emit.init(
- mode=get_verbosity(),
- appname="snapcraft",
- greeting=f"Starting Snapcraft {__version__}",
- log_filepath=log_filepath,
- streaming_brief=True,
- )
-
return craft_cli.Dispatcher(
"snapcraft",
COMMAND_GROUPS,
diff --git a/snapcraft/commands/discovery.py b/snapcraft/commands/discovery.py
index 4f43894c83..2c3fc0d3d6 100644
--- a/snapcraft/commands/discovery.py
+++ b/snapcraft/commands/discovery.py
@@ -24,14 +24,13 @@
from craft_parts.plugins import get_registered_plugins
from overrides import overrides
-from snapcraft import errors
+from snapcraft import errors, models
from snapcraft.parts.yaml_utils import (
apply_yaml,
extract_parse_info,
get_snap_project,
process_yaml,
)
-from snapcraft.projects import Project
from snapcraft.utils import get_host_architecture
if TYPE_CHECKING:
@@ -80,7 +79,7 @@ def run(self, parsed_args):
# determine the base
extract_parse_info(yaml_data_for_arch)
- project = Project.unmarshal(yaml_data_for_arch)
+ project = models.Project.unmarshal(yaml_data_for_arch)
base = project.get_effective_base()
message = (
f"Displaying plugins available to the current base {base!r} project"
diff --git a/snapcraft/commands/extensions.py b/snapcraft/commands/extensions.py
index 0e9d30050a..48d19da778 100644
--- a/snapcraft/commands/extensions.py
+++ b/snapcraft/commands/extensions.py
@@ -26,14 +26,13 @@
from overrides import overrides
from pydantic import BaseModel
-from snapcraft import extensions
+from snapcraft import extensions, models
from snapcraft.parts.yaml_utils import (
apply_yaml,
extract_parse_info,
get_snap_project,
process_yaml,
)
-from snapcraft.projects import Project
from snapcraft.utils import get_host_architecture
from snapcraft_legacy.internal.project_loader import (
find_extension,
@@ -135,5 +134,5 @@ def run(self, parsed_args):
# not part of the Project model
extract_parse_info(yaml_data_for_arch)
- Project.unmarshal(yaml_data_for_arch)
+ models.Project.unmarshal(yaml_data_for_arch)
emit.message(yaml.safe_dump(yaml_data_for_arch, indent=4, sort_keys=False))
diff --git a/snapcraft/commands/lint.py b/snapcraft/commands/lint.py
index 15fb9bb014..3705519f0c 100644
--- a/snapcraft/commands/lint.py
+++ b/snapcraft/commands/lint.py
@@ -32,7 +32,7 @@
from craft_providers.util import snap_cmd
from overrides import overrides
-from snapcraft import errors, linters, projects, providers
+from snapcraft import errors, linters, models, providers
from snapcraft.meta import snap_yaml
from snapcraft.parts.yaml_utils import apply_yaml, extract_parse_info, process_yaml
from snapcraft.utils import (
@@ -257,7 +257,7 @@ def _unsquash_snap(self, snap_file: Path) -> Iterator[Path]:
yield Path(temp_dir)
- def _load_project(self, snapcraft_yaml_file: Path) -> Optional[projects.Project]:
+ def _load_project(self, snapcraft_yaml_file: Path) -> Optional[models.Project]:
"""Load a snapcraft Project from a snapcraft.yaml, if present.
The snapcraft.yaml exist for snaps built with the `--enable-manifest` parameter.
@@ -284,7 +284,7 @@ def _load_project(self, snapcraft_yaml_file: Path) -> Optional[projects.Project]
yaml_data_for_arch = apply_yaml(yaml_data, arch, arch)
# discard parse-info - it is not needed
extract_parse_info(yaml_data_for_arch)
- project = projects.Project.unmarshal(yaml_data_for_arch)
+ project = models.Project.unmarshal(yaml_data_for_arch)
return project
def _install_snap(
@@ -343,14 +343,14 @@ def _install_snap(
return Path("/snap") / snap_metadata.name / "current"
- def _load_lint_filters(self, project: Optional[projects.Project]) -> projects.Lint:
+ def _load_lint_filters(self, project: Optional[models.Project]) -> models.Lint:
"""Load lint filters from a Project and disable the classic linter.
:param project: Project from the snap file, if present.
:returns: Lint config with classic linter disabled.
"""
- lint_config = projects.Lint(ignore=["classic"])
+ lint_config = models.Lint(ignore=["classic"])
if project:
if project.lint:
diff --git a/snapcraft/commands/unimplemented.py b/snapcraft/commands/unimplemented.py
new file mode 100644
index 0000000000..9c3613285f
--- /dev/null
+++ b/snapcraft/commands/unimplemented.py
@@ -0,0 +1,300 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Unimplemented commands that should be sent to snapcraft's cli handler instead."""
+
+import argparse
+from typing import final
+
+from snapcraft import commands, errors
+
+# pylint: disable=missing-class-docstring
+
+
+class UnimplementedMixin:
+ """A mixin that allows you to declare a command unimplemented.
+
+ Lets us scaffold the snapcraft help but then fall back
+ """
+
+ @final
+ def run(self, parsed_args: argparse.Namespace) -> None:
+ """Execute a command's functionality."""
+ raise errors.ClassicFallback()
+
+
+class ExportLogin(
+ UnimplementedMixin, commands.StoreExportLoginCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Login(
+ UnimplementedMixin, commands.StoreLoginCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Logout(
+ UnimplementedMixin, commands.StoreLogoutCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Whoami(
+ UnimplementedMixin, commands.StoreWhoAmICommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListPlugins(
+ UnimplementedMixin, commands.ListPluginsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Plugins(
+ UnimplementedMixin, commands.PluginsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ExpandExtensions(
+ UnimplementedMixin, commands.ExpandExtensionsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListExtensions(
+ UnimplementedMixin, commands.ListExtensionsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Extensions(
+ UnimplementedMixin, commands.ExtensionsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Init(UnimplementedMixin, commands.InitCommand): # noqa: D101 (missing docstring)
+ pass
+
+
+class CreateKey(
+ UnimplementedMixin, commands.StoreLegacyCreateKeyCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Gated(
+ UnimplementedMixin, commands.StoreLegacyGatedCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListKeys(
+ UnimplementedMixin, commands.StoreLegacyListKeysCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListValidationSets(
+ UnimplementedMixin, commands.StoreLegacyListValidationSetsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Metrics(
+ UnimplementedMixin, commands.StoreLegacyMetricsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Promote(
+ UnimplementedMixin, commands.StoreLegacyPromoteCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class RegisterKey(
+ UnimplementedMixin, commands.StoreLegacyRegisterKeyCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class SetDefaultTrack(
+ UnimplementedMixin, commands.StoreLegacySetDefaultTrackCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class SignBuild(
+ UnimplementedMixin, commands.StoreLegacySignBuildCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class UploadMetadata(
+ UnimplementedMixin, commands.StoreLegacyUploadMetadataCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Validate(
+ UnimplementedMixin, commands.StoreLegacyValidateCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Build(
+ UnimplementedMixin, commands.BuildCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Clean(
+ UnimplementedMixin, commands.CleanCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Pack(UnimplementedMixin, commands.PackCommand): # noqa: D101 (missing docstring)
+ pass
+
+
+class Prime(
+ UnimplementedMixin, commands.PrimeCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Pull(UnimplementedMixin, commands.PullCommand): # noqa: D101 (missing docstring)
+ pass
+
+
+class Snap(UnimplementedMixin, commands.SnapCommand): # noqa: D101 (missing docstring)
+ pass
+
+
+class Stage(
+ UnimplementedMixin, commands.StageCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Try(UnimplementedMixin, commands.TryCommand): # noqa: D101 (missing docstring)
+ pass
+
+
+class Lint(UnimplementedMixin, commands.LintCommand): # noqa: D101 (missing docstring)
+ pass
+
+
+class Close(
+ UnimplementedMixin, commands.StoreCloseCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Release(
+ UnimplementedMixin, commands.StoreReleaseCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class List(
+ UnimplementedMixin, commands.StoreLegacyListCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListRegistered(
+ UnimplementedMixin, commands.StoreLegacyListRegisteredCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Names(
+ UnimplementedMixin, commands.StoreNamesCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Register(
+ UnimplementedMixin, commands.StoreRegisterCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListRevisions(
+ UnimplementedMixin, commands.StoreListRevisionsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class ListTracks(
+ UnimplementedMixin, commands.StoreListTracksCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Revisions(
+ UnimplementedMixin, commands.StoreRevisionsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Status(
+ UnimplementedMixin, commands.StoreStatusCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Tracks(
+ UnimplementedMixin, commands.StoreTracksCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Push(
+ UnimplementedMixin, commands.StoreLegacyPushCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Upload(
+ UnimplementedMixin, commands.StoreUploadCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class EditValidationSets(
+ UnimplementedMixin, commands.StoreEditValidationSetsCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class Version(
+ UnimplementedMixin, commands.VersionCommand
+): # noqa: D101 (missing docstring)
+ pass
+
+
+class RemoteBuild(
+ UnimplementedMixin, commands.RemoteBuildCommand
+): # noqa: D101 (missing docstring)
+ pass
diff --git a/snapcraft/errors.py b/snapcraft/errors.py
index 37148a05ea..8ca46df4ee 100644
--- a/snapcraft/errors.py
+++ b/snapcraft/errors.py
@@ -21,6 +21,10 @@
from craft_cli import CraftError
+class ClassicFallback(Exception):
+ """Temporary class to fall back to non craft-application launcher."""
+
+
class SnapcraftError(CraftError):
"""Failure in a Snapcraft operation."""
diff --git a/snapcraft/linters/base.py b/snapcraft/linters/base.py
index c8a819c5b0..e27c8eef5b 100644
--- a/snapcraft/linters/base.py
+++ b/snapcraft/linters/base.py
@@ -25,8 +25,7 @@
import pydantic
from craft_cli import emit
-from snapcraft import projects
-from snapcraft.elf import ElfFile
+from snapcraft import elf, models
if TYPE_CHECKING:
from snapcraft.meta.snap_yaml import SnapMetadata
@@ -91,11 +90,11 @@ def __init__(
self,
name: str,
snap_metadata: "SnapMetadata",
- lint: Optional[projects.Lint],
+ lint: Optional[models.Lint],
):
self._name = name
self._snap_metadata = snap_metadata
- self._lint = lint or projects.Lint(ignore=[])
+ self._lint = lint or models.Lint(ignore=[])
@abc.abstractmethod
def run(self) -> List[LinterIssue]:
@@ -105,7 +104,7 @@ def run(self) -> List[LinterIssue]:
"""
def _is_file_ignored(
- self, filepath: Union[ElfFile, Path], category: str = ""
+ self, filepath: Union[elf.ElfFile, Path], category: str = ""
) -> bool:
"""Check if the file name matches an ignored file pattern.
@@ -121,7 +120,7 @@ def _is_file_ignored(
# No "extend()" because we don't want to affect the original list.
ignored_files = ignored_files + self._lint.ignored_files(category)
- if isinstance(filepath, ElfFile):
+ if isinstance(filepath, elf.ElfFile):
path = filepath.path
else:
path = filepath
diff --git a/snapcraft/linters/linters.py b/snapcraft/linters/linters.py
index 70ca40d838..507c8642cc 100644
--- a/snapcraft/linters/linters.py
+++ b/snapcraft/linters/linters.py
@@ -26,7 +26,7 @@
from craft_cli import emit
-from snapcraft import projects
+from snapcraft import models
from snapcraft.meta import snap_yaml
from .base import Linter, LinterIssue, LinterResult
@@ -111,7 +111,7 @@ def _update_status(status: LinterStatus, result: LinterResult) -> LinterStatus:
return status
-def run_linters(location: Path, *, lint: Optional[projects.Lint]) -> List[LinterIssue]:
+def run_linters(location: Path, *, lint: Optional[models.Lint]) -> List[LinterIssue]:
"""Run all the defined linters.
:param location: The root of the snap payload subtree to run linters on.
@@ -149,7 +149,7 @@ def run_linters(location: Path, *, lint: Optional[projects.Lint]) -> List[Linter
def _ignore_matching_filenames(
- issues: List[LinterIssue], *, lint: Optional[projects.Lint]
+ issues: List[LinterIssue], *, lint: Optional[models.Lint]
) -> None:
"""Mark any remaining filename match as ignored."""
if lint is None:
diff --git a/snapcraft/meta/manifest.py b/snapcraft/meta/manifest.py
index 4ae5925cd0..7876e9e2d4 100644
--- a/snapcraft/meta/manifest.py
+++ b/snapcraft/meta/manifest.py
@@ -23,8 +23,7 @@
from pydantic_yaml import YamlModel
-from snapcraft import __version__, errors, os_release, utils
-from snapcraft.projects import Project
+from snapcraft import __version__, errors, models, os_release, utils
class Manifest(YamlModel):
@@ -68,7 +67,7 @@ class Config: # pylint: disable=too-few-public-methods
def write( # noqa PLR0913
- project: Project,
+ project: models.Project,
prime_dir: Path,
*,
arch: str,
diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py
index afb7953f72..65adbed4af 100644
--- a/snapcraft/meta/snap_yaml.py
+++ b/snapcraft/meta/snap_yaml.py
@@ -21,12 +21,12 @@
from typing import Any, Dict, List, Literal, Optional, Set, Union, cast
import yaml
+from craft_application.models import UniqueStrList
from craft_cli import emit
from pydantic import Extra, ValidationError, validator
from pydantic_yaml import YamlModel
-from snapcraft import errors
-from snapcraft.projects import App, Project, UniqueStrList
+from snapcraft import errors, models
from snapcraft.utils import get_ld_library_paths, process_version
@@ -188,13 +188,13 @@ class Links(_SnapMetadataModel):
@staticmethod
def _normalize_value(
value: Optional[Union[str, UniqueStrList]]
- ) -> Optional[List[str]]:
+ ) -> Optional[UniqueStrList]:
if isinstance(value, str):
- value = [value]
+ value = cast(UniqueStrList, [value])
return value
@classmethod
- def from_project(cls, project: Project) -> "Links":
+ def from_project(cls, project: models.Project) -> "Links":
"""Create Links from a Project."""
return cls(
contact=cls._normalize_value(project.contact),
@@ -338,7 +338,7 @@ def read(prime_dir: Path) -> SnapMetadata:
return SnapMetadata.unmarshal(data)
-def _create_snap_app(app: App, assumes: Set[str]) -> SnapApp:
+def _create_snap_app(app: models.App, assumes: Set[str]) -> SnapApp:
app_sockets: Dict[str, Socket] = {}
if app.sockets:
for socket_name, socket in app.sockets.items():
@@ -410,7 +410,7 @@ def _get_grade(grade: Optional[str], build_base: Optional[str]) -> str:
return grade
-def write(project: Project, prime_dir: Path, *, arch: str):
+def write(project: models.Project, prime_dir: Path, *, arch: str):
"""Create a snap.yaml file.
:param project: Snapcraft project.
diff --git a/snapcraft/models/__init__.py b/snapcraft/models/__init__.py
new file mode 100644
index 0000000000..9f56e187d3
--- /dev/null
+++ b/snapcraft/models/__init__.py
@@ -0,0 +1,42 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2022-2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+"""Data models for snapcraft."""
+
+from .project import (
+ MANDATORY_ADOPTABLE_FIELDS,
+ App,
+ Architecture,
+ ArchitectureProject,
+ ContentPlug,
+ GrammarAwareProject,
+ Hook,
+ Lint,
+ Project,
+ Socket,
+)
+
+__all__ = [
+ "MANDATORY_ADOPTABLE_FIELDS",
+ "App",
+ "Architecture",
+ "ArchitectureProject",
+ "ContentPlug",
+ "GrammarAwareProject",
+ "Hook",
+ "Lint",
+ "Project",
+ "Socket",
+]
diff --git a/snapcraft/projects.py b/snapcraft/models/project.py
similarity index 95%
rename from snapcraft/projects.py
rename to snapcraft/models/project.py
index feadca15d7..a05c76a40f 100644
--- a/snapcraft/projects.py
+++ b/snapcraft/models/project.py
@@ -17,13 +17,15 @@
"""Project file definition and helpers."""
import re
-from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
import pydantic
+from craft_application import models
+from craft_application.models import BuildInfo, UniqueStrList
from craft_archives import repo
from craft_cli import emit
from craft_grammar.models import GrammarSingleEntryDictList, GrammarStr, GrammarStrList
-from pydantic import PrivateAttr, conlist, constr
+from pydantic import PrivateAttr, constr
from snapcraft import parts, utils
from snapcraft.elf.elf_utils import get_arch_triplet
@@ -36,35 +38,15 @@
is_architecture_supported,
)
-
-class ProjectModel(pydantic.BaseModel):
- """Base model for snapcraft project classes."""
-
- class Config: # pylint: disable=too-few-public-methods
- """Pydantic model configuration."""
-
- validate_assignment = True
- extra = "forbid"
- allow_mutation = True # project is updated with adopted metadata
- allow_population_by_field_name = True
- alias_generator = lambda s: s.replace("_", "-") # noqa: E731
-
-
# A workaround for mypy false positives
# see https://github.com/samuelcolvin/pydantic/issues/975#issuecomment-551147305
# fmt: off
if TYPE_CHECKING:
ProjectName = str
- ProjectSummary = str
- ProjectTitle = str
ProjectVersion = str
- UniqueStrList = List[str]
else:
ProjectName = constr(max_length=40)
- ProjectSummary = constr(max_length=78)
- ProjectTitle = constr(max_length=40)
ProjectVersion = constr(max_length=32, strict=True)
- UniqueStrList = conlist(str, unique_items=True)
# fmt: on
@@ -149,7 +131,8 @@ def _expand_architectures(architectures):
# convert strings into Architecture objects
if isinstance(architecture, str):
architectures[index] = Architecture(
- build_on=[architecture], build_for=[architecture]
+ build_on=cast(UniqueStrList, [architecture]),
+ build_for=cast(UniqueStrList, [architecture]),
)
elif isinstance(architecture, Architecture):
# convert strings to lists
@@ -187,7 +170,7 @@ def _validate_architectures_all_keyword(architectures):
)
-class Socket(ProjectModel):
+class Socket(models.CraftBaseModel):
"""Snapcraft app socket definition."""
listen_stream: Union[int, str]
@@ -212,7 +195,7 @@ def _validate_list_stream(cls, listen_stream):
return listen_stream
-class Lint(ProjectModel):
+class Lint(models.CraftBaseModel):
"""Linter configuration.
:ivar ignore: A list describing which files should have issues ignored for given linters.
@@ -264,7 +247,7 @@ def ignored_files(self, linter_name: str) -> List[str]:
return self._lint_ignores[linter_name]
-class App(ProjectModel):
+class App(models.CraftBaseModel):
"""Snapcraft project app definition."""
command: str
@@ -282,8 +265,8 @@ class App(ProjectModel):
restart_delay: Optional[str]
timer: Optional[str]
daemon: Optional[Literal["simple", "forking", "oneshot", "notify", "dbus"]]
- after: UniqueStrList = []
- before: UniqueStrList = []
+ after: UniqueStrList = cast(UniqueStrList, [])
+ before: UniqueStrList = cast(UniqueStrList, [])
refresh_mode: Optional[Literal["endure", "restart"]]
stop_mode: Optional[
Literal[
@@ -368,7 +351,7 @@ def _validate_aliases(cls, aliases):
return aliases
-class Hook(ProjectModel):
+class Hook(models.CraftBaseModel):
"""Snapcraft project hook definition."""
command_chain: Optional[List[str]]
@@ -389,14 +372,14 @@ def _validate_plugs(cls, plugs):
return plugs
-class Architecture(ProjectModel, extra=pydantic.Extra.forbid):
+class Architecture(models.CraftBaseModel, extra=pydantic.Extra.forbid):
"""Snapcraft project architecture definition."""
build_on: Union[str, UniqueStrList]
build_for: Optional[Union[str, UniqueStrList]]
-class ContentPlug(ProjectModel):
+class ContentPlug(models.CraftBaseModel):
"""Snapcraft project content plug definition."""
content: Optional[str]
@@ -418,7 +401,7 @@ def _validate_default_provider(cls, default_provider):
MANDATORY_ADOPTABLE_FIELDS = ("version", "summary", "description")
-class Project(ProjectModel):
+class Project(models.Project):
"""Snapcraft project definition.
See https://snapcraft.io/docs/snapcraft-yaml-reference
@@ -427,29 +410,26 @@ class Project(ProjectModel):
- system-usernames
"""
- name: ProjectName
- title: Optional[ProjectTitle]
- base: Optional[str]
+ # snapcraft's `name` is more general than craft-application
+ name: ProjectName # type: ignore[assignment]
build_base: Optional[str]
compression: Literal["lzo", "xz"] = "xz"
- version: Optional[ProjectVersion]
- contact: Optional[Union[str, UniqueStrList]]
+ # TODO: ensure we have a test for version being retrieved using adopt-info
+ # snapcraft's `version` is more general than craft-application
+ version: Optional[ProjectVersion] # type: ignore[assignment]
donation: Optional[Union[str, UniqueStrList]]
- issues: Optional[Union[str, UniqueStrList]]
- source_code: Optional[str]
+ # snapcraft's `source_code` is more general than craft-application
+ source_code: Optional[str] # type: ignore[assignment]
website: Optional[str]
- summary: Optional[ProjectSummary]
- description: Optional[str]
type: Optional[Literal["app", "base", "gadget", "kernel", "snapd"]]
icon: Optional[str]
confinement: Literal["classic", "devmode", "strict"]
layout: Optional[
Dict[str, Dict[Literal["symlink", "bind", "bind-file", "type"], str]]
]
- license: Optional[str]
grade: Optional[Literal["stable", "devel"]]
architectures: List[Union[str, Architecture]] = [get_host_architecture()]
- assumes: UniqueStrList = []
+ assumes: UniqueStrList = cast(UniqueStrList, [])
package_repositories: List[Dict[str, Any]] = [] # handled by repo
hooks: Optional[Dict[str, Hook]]
passthrough: Optional[Dict[str, Any]]
@@ -457,7 +437,6 @@ class Project(ProjectModel):
plugs: Optional[Dict[str, Union[ContentPlug, Any]]]
slots: Optional[Dict[str, Any]]
lint: Optional[Lint]
- parts: Dict[str, Any] # parts are handled by craft-parts
epoch: Optional[str]
adopt_info: Optional[str]
system_usernames: Optional[Dict[str, Any]]
@@ -756,6 +735,11 @@ def get_build_for_arch_triplet(self) -> Optional[str]:
return None
+ def get_build_plan(self) -> List[BuildInfo]:
+ """Get the build plan for this project."""
+ # TODO
+ raise NotImplementedError("Not implemented yet!")
+
class _GrammarAwareModel(pydantic.BaseModel):
class Config:
@@ -791,7 +775,7 @@ def validate_grammar(cls, data: Dict[str, Any]) -> None:
raise ProjectValidationError(_format_pydantic_errors(err.errors())) from err
-class ArchitectureProject(ProjectModel, extra=pydantic.Extra.ignore):
+class ArchitectureProject(models.CraftBaseModel, extra=pydantic.Extra.ignore):
"""Project definition containing only architecture data."""
architectures: List[Union[str, Architecture]] = [get_host_architecture()]
diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py
index 6d8ed2625d..f1c82374cf 100644
--- a/snapcraft/parts/lifecycle.py
+++ b/snapcraft/parts/lifecycle.py
@@ -29,12 +29,11 @@
from craft_parts import ProjectInfo, Step, StepInfo, callbacks
from craft_providers import Executor
-from snapcraft import errors, linters, pack, providers, ua_manager, utils
+from snapcraft import errors, linters, models, pack, providers, ua_manager, utils
from snapcraft.elf import Patcher, SonameCache, elf_utils
from snapcraft.elf import errors as elf_errors
from snapcraft.linters import LinterStatus
from snapcraft.meta import manifest, snap_yaml
-from snapcraft.projects import Architecture, ArchitectureProject, Project
from snapcraft.utils import (
convert_architecture_deb_to_platform,
get_host_architecture,
@@ -99,7 +98,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None:
parallel_build_count=build_count,
target_arch=build_for,
)
- project = Project.unmarshal(yaml_data_for_arch)
+ project = models.Project.unmarshal(yaml_data_for_arch)
_run_command(
command_name,
@@ -115,7 +114,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None:
def _run_command( # noqa PLR0913 # pylint: disable=too-many-branches, too-many-statements
command_name: str,
*,
- project: Project,
+ project: models.Project,
parse_info: Dict[str, List[str]],
assets_dir: Path,
start_time: datetime,
@@ -227,7 +226,7 @@ def _run_lifecycle_and_pack( # noqa PLR0913
*,
command_name: str,
step_name: str,
- project: Project,
+ project: models.Project,
project_dir: Path,
assets_dir: Path,
start_time: datetime,
@@ -278,7 +277,7 @@ def _run_lifecycle_and_pack( # noqa PLR0913
def _generate_metadata(
*,
- project: Project,
+ project: models.Project,
lifecycle: PartsLifecycle,
project_dir: Path,
assets_dir: Path,
@@ -319,7 +318,7 @@ def _generate_metadata(
def _generate_manifest(
- project: Project,
+ project: models.Project,
*,
lifecycle: PartsLifecycle,
start_time: datetime,
@@ -353,7 +352,7 @@ def _generate_manifest(
shutil.copy(snap_project.project_file, lifecycle.prime_dir / "snap")
-def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None:
+def _clean_provider(project: models.Project, parsed_args: "argparse.Namespace") -> None:
"""Clean the provider environment.
:param project: The project to clean.
@@ -374,7 +373,7 @@ def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None
# pylint: disable-next=too-many-branches, too-many-statements
def _run_in_provider( # noqa PLR0915
- project: Project, command_name: str, parsed_args: "argparse.Namespace"
+ project: models.Project, command_name: str, parsed_args: "argparse.Namespace"
) -> None:
"""Pack image in provider instance."""
emit.debug("Checking build provider availability")
@@ -509,7 +508,7 @@ def _set_global_environment(info: ProjectInfo) -> None:
def _check_experimental_plugins(
- project: Project, enable_experimental_plugins: bool
+ project: models.Project, enable_experimental_plugins: bool
) -> None:
"""Ensure the experimental plugin flag is enabled to use unstable plugins."""
for name, part in project.parts.items():
@@ -642,13 +641,13 @@ def get_build_plan(
:return: List of tuples of every valid build-on->build-for combination.
"""
- archs = ArchitectureProject.unmarshal(yaml_data).architectures
+ archs = models.ArchitectureProject.unmarshal(yaml_data).architectures
host_arch = get_host_architecture()
build_plan: List[Tuple[str, str]] = []
# `isinstance()` calls are for mypy type checking and should not change logic
- for arch in [arch for arch in archs if isinstance(arch, Architecture)]:
+ for arch in [arch for arch in archs if isinstance(arch, models.Architecture)]:
for build_on in arch.build_on:
if build_on in host_arch and isinstance(arch.build_for, list):
build_plan.append((host_arch, arch.build_for[0]))
diff --git a/snapcraft/parts/project_check.py b/snapcraft/parts/project_check.py
index e914a8fdc9..dc51c97d84 100644
--- a/snapcraft/parts/project_check.py
+++ b/snapcraft/parts/project_check.py
@@ -22,8 +22,7 @@
from craft_cli import emit
-from snapcraft import errors
-from snapcraft.projects import Project
+from snapcraft import errors, models
_EXPECTED_SNAP_DIR_PATTERNS = {
re.compile(r"^snapcraft.yaml$"),
@@ -36,7 +35,7 @@
}
-def run_project_checks(project: Project, *, assets_dir: Path) -> None:
+def run_project_checks(project: models.Project, *, assets_dir: Path) -> None:
"""Execute consistency checks for project and project files.
The checks done here are meant to be light, and not rely on the
diff --git a/snapcraft/parts/setup_assets.py b/snapcraft/parts/setup_assets.py
index cfacf41c08..74d20009ba 100644
--- a/snapcraft/parts/setup_assets.py
+++ b/snapcraft/parts/setup_assets.py
@@ -27,14 +27,13 @@
import requests
from craft_cli import emit
-from snapcraft import errors
-from snapcraft.projects import Project
+from snapcraft import errors, models
from .desktop_file import DesktopFile
def setup_assets(
- project: Project, *, assets_dir: Path, project_dir: Path, prime_dir: Path
+ project: models.Project, *, assets_dir: Path, project_dir: Path, prime_dir: Path
) -> None:
"""Copy assets to the appropriate locations in the snap filesystem.
diff --git a/snapcraft/parts/update_metadata.py b/snapcraft/parts/update_metadata.py
index 327fe72b2e..8652601fc9 100644
--- a/snapcraft/parts/update_metadata.py
+++ b/snapcraft/parts/update_metadata.py
@@ -17,14 +17,15 @@
"""External metadata helpers."""
from pathlib import Path
-from typing import Dict, Final, List
+from typing import Dict, Final, List, cast
import pydantic
+from craft_application.models import ProjectTitle, SummaryStr, VersionStr
from craft_cli import emit
from snapcraft import errors
from snapcraft.meta import ExtractedMetadata
-from snapcraft.projects import MANDATORY_ADOPTABLE_FIELDS, Project
+from snapcraft.models import MANDATORY_ADOPTABLE_FIELDS, Project
_VALID_ICON_EXTENSIONS: Final[List[str]] = ["png", "svg"]
@@ -52,16 +53,16 @@ def update_project_metadata(
for metadata in metadata_list:
# Data specified in the project yaml has precedence over extracted data
if metadata.title and not project.title:
- project.title = metadata.title
+ project.title = cast(ProjectTitle, metadata.title)
if metadata.summary and not project.summary:
- project.summary = metadata.summary
+ project.summary = cast(SummaryStr, metadata.summary)
if metadata.description and not project.description:
project.description = metadata.description
if metadata.version and not project.version:
- project.version = metadata.version
+ project.version = cast(VersionStr, metadata.version)
if metadata.grade and not project.grade:
project.grade = metadata.grade # type: ignore
diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py
index 053c957c8b..7ca593515e 100644
--- a/snapcraft/parts/yaml_utils.py
+++ b/snapcraft/parts/yaml_utils.py
@@ -25,7 +25,7 @@
from snapcraft import errors, utils
from snapcraft.extensions import apply_extensions
-from snapcraft.projects import Architecture, GrammarAwareProject
+from snapcraft.models import Architecture, GrammarAwareProject
from . import grammar
diff --git a/snapcraft/services/__init__.py b/snapcraft/services/__init__.py
new file mode 100644
index 0000000000..2df3b797aa
--- /dev/null
+++ b/snapcraft/services/__init__.py
@@ -0,0 +1,25 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Snapcraft services."""
+
+from snapcraft.services.package import Package
+from snapcraft.services.service_factory import SnapcraftServiceFactory
+
+__all__ = [
+ "Package",
+ "SnapcraftServiceFactory",
+]
diff --git a/snapcraft/services/package.py b/snapcraft/services/package.py
new file mode 100644
index 0000000000..2d5d6f4d95
--- /dev/null
+++ b/snapcraft/services/package.py
@@ -0,0 +1,57 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Snapcraft Package service."""
+
+from __future__ import annotations
+
+import pathlib
+
+from craft_application import PackageService, models
+from overrides import override
+
+
+class Package(PackageService):
+ """Package service subclass for Snapcraft."""
+
+ @override
+ def pack(self, prime_dir: pathlib.Path, dest: pathlib.Path) -> list[pathlib.Path]:
+ """Create one or more packages as appropriate.
+
+ :param prime_dir: Path to the directory to pack.
+ :param dest: Directory into which to write the package(s).
+
+ :returns: A list of paths to created packages.
+ """
+ # TODO
+ raise NotImplementedError(
+ "Packing using the package service not yet implemented."
+ )
+
+ @override
+ def write_metadata(self, path: pathlib.Path) -> None:
+ """Write the project metadata to metadata.yaml in the given directory.
+
+ :param path: The path to the prime directory.
+ """
+ # TODO
+ raise NotImplementedError("Writing metadata not yet implemented.")
+
+ @property
+ def metadata(self) -> models.BaseMetadata:
+ """Get the metadata model for this project."""
+ # TODO: get metadata from project
+ return models.BaseMetadata()
diff --git a/snapcraft/services/service_factory.py b/snapcraft/services/service_factory.py
new file mode 100644
index 0000000000..574944cda1
--- /dev/null
+++ b/snapcraft/services/service_factory.py
@@ -0,0 +1,37 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Snapcraft Service Factory."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from craft_application import ServiceFactory
+
+from snapcraft import models, services
+
+
+@dataclass
+class SnapcraftServiceFactory(ServiceFactory):
+ """Snapcraft-specific Service Factory."""
+
+ project: models.Project | None = None # type: ignore[reportIncompatibleVariableOverride]
+
+ # These are overrides of default ServiceFactory services
+ PackageClass: type[services.Package] = ( # type: ignore[reportIncompatibleVariableOverride]
+ services.Package
+ )
diff --git a/snapcraft/snap_config.py b/snapcraft/snap_config.py
index 288e1b5d35..e78a6c4282 100644
--- a/snapcraft/snap_config.py
+++ b/snapcraft/snap_config.py
@@ -58,7 +58,7 @@ def unmarshal(cls, data: Dict[str, Any]) -> "SnapConfig":
try:
snap_config = cls(**data)
except pydantic.ValidationError as error:
- # TODO: use `_format_pydantic_errors()` from projects.py
+ # TODO: use `_format_pydantic_errors()` from project.py
raise ValueError(f"error parsing snap config: {error}") from error
return snap_config
diff --git a/tests/unit/commands/test_init.py b/tests/unit/commands/test_init.py
index eb559b5712..91965417ab 100644
--- a/tests/unit/commands/test_init.py
+++ b/tests/unit/commands/test_init.py
@@ -22,8 +22,8 @@
import pytest
from snapcraft import cli
+from snapcraft.models.project import Project
from snapcraft.parts.yaml_utils import _SNAP_PROJECT_FILES, apply_yaml, process_yaml
-from snapcraft.projects import Project
@pytest.fixture(autouse=True)
diff --git a/tests/unit/commands/test_lint.py b/tests/unit/commands/test_lint.py
index 9588d52739..9040af1154 100644
--- a/tests/unit/commands/test_lint.py
+++ b/tests/unit/commands/test_lint.py
@@ -25,11 +25,10 @@
from craft_providers.bases import BuilddBaseAlias
from craft_providers.multipass import MultipassProvider
-from snapcraft import cli
+from snapcraft import cli, models
from snapcraft.commands.lint import LintCommand
from snapcraft.errors import SnapcraftError
from snapcraft.meta.snap_yaml import SnapMetadata
-from snapcraft.projects import Lint, Project
@pytest.fixture
@@ -70,7 +69,7 @@ def fake_snapcraft_project():
"summary": "test summary",
"parts": {"part1": {"plugin": "nil"}},
}
- return Project.unmarshal(data)
+ return models.Project.unmarshal(data)
@pytest.fixture
@@ -393,7 +392,7 @@ def test_lint_managed_mode(
cli.run()
mock_run_linters.assert_called_once_with(
- lint=Lint(ignore=["classic"]),
+ lint=models.Lint(ignore=["classic"]),
location=Path("/snap/test/current"),
)
mock_report.assert_called_once_with(
@@ -449,7 +448,7 @@ def test_lint_managed_mode_without_snapcraft_yaml(
cli.run()
mock_run_linters.assert_called_once_with(
- lint=Lint(ignore=["classic"]),
+ lint=models.Lint(ignore=["classic"]),
location=Path("/snap/test/current"),
)
mock_report.assert_called_once_with(
@@ -602,7 +601,7 @@ def test_lint_managed_mode_assert(
cli.run()
mock_run_linters.assert_called_once_with(
- lint=Lint(ignore=["classic"]),
+ lint=models.Lint(ignore=["classic"]),
location=Path("/snap/test/current"),
)
mock_report.assert_called_once_with(
@@ -666,7 +665,7 @@ def test_lint_managed_mode_assert_error(
cli.run()
mock_run_linters.assert_called_once_with(
- lint=Lint(ignore=["classic"]),
+ lint=models.Lint(ignore=["classic"]),
location=Path("/snap/test/current"),
)
mock_report.assert_called_once_with(
@@ -699,30 +698,30 @@ def test_lint_managed_mode_assert_error(
["project_lint", "expected_lint"],
[
(
- Lint(ignore=[]),
- Lint(ignore=["classic"]),
+ models.Lint(ignore=[]),
+ models.Lint(ignore=["classic"]),
),
(
- Lint(ignore=["library"]),
- Lint(ignore=["library", "classic"]),
+ models.Lint(ignore=["library"]),
+ models.Lint(ignore=["library", "classic"]),
),
(
- Lint(ignore=["library", "classic"]),
- Lint(ignore=["library", "classic"]),
+ models.Lint(ignore=["library", "classic"]),
+ models.Lint(ignore=["library", "classic"]),
),
(
- Lint(ignore=[{"classic": ["bin/test1", "bin/test2"]}]),
- Lint(ignore=["classic"]),
+ models.Lint(ignore=[{"classic": ["bin/test1", "bin/test2"]}]),
+ models.Lint(ignore=["classic"]),
),
(
- Lint(ignore=["library", {"classic": ["bin/test1", "bin/test2"]}]),
- Lint(ignore=["library", "classic"]),
+ models.Lint(ignore=["library", {"classic": ["bin/test1", "bin/test2"]}]),
+ models.Lint(ignore=["library", "classic"]),
),
(
- Lint(
+ models.Lint(
ignore=["library", "classic", {"classic": ["bin/test1", "bin/test2"]}]
),
- Lint(ignore=["library", "classic"]),
+ models.Lint(ignore=["library", "classic"]),
),
],
)
@@ -851,7 +850,7 @@ def test_load_project_complex(mocker, tmp_path):
)
result = LintCommand(None)._load_project(snapcraft_yaml_file=snap_file)
- assert result == Project.unmarshal(
+ assert result == models.Project.unmarshal(
{
"name": "test-name",
"base": "core22",
diff --git a/tests/unit/linters/test_base.py b/tests/unit/linters/test_base.py
index db37e22460..6adc041e19 100644
--- a/tests/unit/linters/test_base.py
+++ b/tests/unit/linters/test_base.py
@@ -18,7 +18,7 @@
import pytest
import yaml
-from snapcraft import projects
+from snapcraft import models
from snapcraft.linters.base import LinterResult
@@ -83,7 +83,7 @@ def lint_ignore_data():
def test_lint_all_ignored(lint_ignore_data):
- lint = projects.Lint(**lint_ignore_data)
+ lint = models.Lint(**lint_ignore_data)
assert lint.all_ignored("linter1")
assert not lint.all_ignored("linter2")
@@ -91,7 +91,7 @@ def test_lint_all_ignored(lint_ignore_data):
def test_lint_ignored_files(lint_ignore_data):
- lint = projects.Lint(**lint_ignore_data)
+ lint = models.Lint(**lint_ignore_data)
assert lint.ignored_files("linter1") == ["*"]
assert lint.ignored_files("linter2") == ["file1", "/lib/file2*"]
diff --git a/tests/unit/linters/test_classic_linter.py b/tests/unit/linters/test_classic_linter.py
index 6c6e7e7e25..04491418de 100644
--- a/tests/unit/linters/test_classic_linter.py
+++ b/tests/unit/linters/test_classic_linter.py
@@ -19,7 +19,7 @@
import pytest
-from snapcraft import linters, projects
+from snapcraft import linters, models
from snapcraft.elf import elf_utils
from snapcraft.linters.base import LinterIssue, LinterResult
from snapcraft.linters.classic_linter import ClassicLinter
@@ -63,7 +63,7 @@ def test_classic_linter(mocker, new_dir, confinement, stage_libc, text):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
issues = linters.run_linters(new_dir, lint=None)
@@ -125,11 +125,11 @@ def test_classic_linter_filter(mocker, new_dir):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
issues = linters.run_linters(
- new_dir, lint=projects.Lint(ignore=[{"classic": ["elf.*"]}])
+ new_dir, lint=models.Lint(ignore=[{"classic": ["elf.*"]}])
)
assert issues == [
LinterIssue(
diff --git a/tests/unit/linters/test_library_linter.py b/tests/unit/linters/test_library_linter.py
index 683439e0bb..00b3d05cbb 100644
--- a/tests/unit/linters/test_library_linter.py
+++ b/tests/unit/linters/test_library_linter.py
@@ -20,7 +20,7 @@
import pytest
-from snapcraft import linters, projects
+from snapcraft import linters, models
from snapcraft.elf import _elf_file, elf_utils
from snapcraft.linters.base import LinterIssue, LinterResult
from snapcraft.linters.library_linter import LibraryLinter
@@ -53,7 +53,7 @@ def test_library_linter_missing_library(mocker, new_dir):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
issues = linters.run_linters(new_dir, lint=None)
@@ -108,7 +108,7 @@ def test_library_linter_unused_library(mocker, new_dir):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
issues = linters.run_linters(new_dir, lint=None)
@@ -149,11 +149,11 @@ def test_library_linter_filter_missing_library(mocker, new_dir, filter_name):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
issues = linters.run_linters(
- new_dir, lint=projects.Lint(ignore=[{filter_name: ["elf.*"]}])
+ new_dir, lint=models.Lint(ignore=[{filter_name: ["elf.*"]}])
)
assert issues == []
@@ -194,11 +194,11 @@ def test_library_linter_filter_unused_library(mocker, new_dir, filter_name):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
issues = linters.run_linters(
- new_dir, lint=projects.Lint(ignore=[{filter_name: ["lib/libfoo.*"]}])
+ new_dir, lint=models.Lint(ignore=[{filter_name: ["lib/libfoo.*"]}])
)
assert issues == []
@@ -231,13 +231,13 @@ def test_library_linter_mixed_filters(mocker, new_dir):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
# lib/libfoo.so is an *unused* library, but here we filter out *missing* library
# issues for this path.
issues = linters.run_linters(
- new_dir, lint=projects.Lint(ignore=[{"missing-library": ["lib/libfoo.*"]}])
+ new_dir, lint=models.Lint(ignore=[{"missing-library": ["lib/libfoo.*"]}])
)
# The "unused library" issue must be generated.
assert issues == [
diff --git a/tests/unit/linters/test_linters.py b/tests/unit/linters/test_linters.py
index 2daf388518..cf2adec6b7 100644
--- a/tests/unit/linters/test_linters.py
+++ b/tests/unit/linters/test_linters.py
@@ -21,7 +21,7 @@
import pytest
from overrides import overrides
-from snapcraft import linters, projects
+from snapcraft import linters, models
from snapcraft.linters.base import Linter, LinterResult
from snapcraft.linters.linters import _ignore_matching_filenames
from snapcraft.meta import snap_yaml
@@ -164,7 +164,7 @@ def test_run_linters(self, mocker, new_dir, linter_issue):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
@@ -193,14 +193,14 @@ def test_run_linters_ignore(self, mocker, new_dir, linter_issue):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(
project,
prime_dir=Path(new_dir),
arch="amd64",
)
- lint = projects.Lint(ignore=["test"])
+ lint = models.Lint(ignore=["test"])
issues = linters.run_linters(new_dir, lint=lint)
assert issues == []
@@ -218,15 +218,15 @@ def test_run_linters_ignore_all_categories(self, mocker, new_dir, linter_issue):
"parts": {},
}
- project = projects.Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
snap_yaml.write(project, prime_dir=Path(new_dir), arch="amd64")
- lint = projects.Lint(ignore=["test-1", "test-2"])
+ lint = models.Lint(ignore=["test-1", "test-2"])
issues = linters.run_linters(new_dir, lint=lint)
assert issues == []
def test_ignore_matching_filenames(self, linter_issue):
- lint = projects.Lint(ignore=[{"test": ["foo*", "some/dir/*"]}])
+ lint = models.Lint(ignore=[{"test": ["foo*", "some/dir/*"]}])
issues = [
linter_issue(filename="foo.txt", result=LinterResult.WARNING),
linter_issue(filename="bar.txt", result=LinterResult.WARNING),
@@ -245,7 +245,7 @@ def test_ignore_matching_filenames(self, linter_issue):
def test_base_linter_is_file_ignored():
"""Test the base Linter class' ignore mechanism with categories."""
- lint = projects.Lint(
+ lint = models.Lint(
ignore=[
{"test": ["test-path"]},
{"test-1": ["test-1-path"]},
diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py
index dafe7a6816..ab9f9d16ce 100644
--- a/tests/unit/meta/test_snap_yaml.py
+++ b/tests/unit/meta/test_snap_yaml.py
@@ -23,7 +23,7 @@
from snapcraft.meta import snap_yaml
from snapcraft.meta.snap_yaml import ContentPlug, ContentSlot, SnapMetadata
-from snapcraft.projects import Project
+from snapcraft.models import Project
def _override_data(to_dict, from_dict):
diff --git a/tests/unit/models/__init__.py b/tests/unit/models/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tests/unit/test_projects.py b/tests/unit/models/test_projects.py
similarity index 99%
rename from tests/unit/test_projects.py
rename to tests/unit/models/test_projects.py
index cb52ec2746..deee80a103 100644
--- a/tests/unit/test_projects.py
+++ b/tests/unit/models/test_projects.py
@@ -14,13 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-from typing import Any, Dict
+from typing import Any, Dict, cast
import pydantic
import pytest
+from craft_application.models import UniqueStrList
from snapcraft import errors
-from snapcraft.projects import (
+from snapcraft.models import (
MANDATORY_ADOPTABLE_FIELDS,
Architecture,
ContentPlug,
@@ -100,7 +101,8 @@ def test_project_defaults(self, project_yaml_data):
assert project.adopt_info is None
assert project.architectures == [
Architecture(
- build_on=[get_host_architecture()], build_for=[get_host_architecture()]
+ build_on=cast(UniqueStrList, [get_host_architecture()]),
+ build_for=cast(UniqueStrList, [get_host_architecture()]),
)
]
assert project.ua_services is None
diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py
index e4f5b33d46..5f782b3b01 100644
--- a/tests/unit/parts/test_lifecycle.py
+++ b/tests/unit/parts/test_lifecycle.py
@@ -28,10 +28,10 @@
from snapcraft import errors
from snapcraft.elf import ElfFile
+from snapcraft.models import MANDATORY_ADOPTABLE_FIELDS, Project
from snapcraft.parts import lifecycle as parts_lifecycle
from snapcraft.parts.plugins import KernelPlugin
from snapcraft.parts.update_metadata import update_project_metadata
-from snapcraft.projects import MANDATORY_ADOPTABLE_FIELDS, Project
from snapcraft.utils import get_host_architecture
_SNAPCRAFT_YAML_FILENAMES = [
@@ -1306,8 +1306,12 @@ def test_lifecycle_run_in_provider_default(
mock_prepare_instance = mocker.patch(
"snapcraft.parts.lifecycle.providers.prepare_instance"
)
- mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-1")
- mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-2")
+ mocker.patch(
+ "snapcraft.models.project.Project.get_build_on", return_value="test-arch-1"
+ )
+ mocker.patch(
+ "snapcraft.models.project.Project.get_build_for", return_value="test-arch-2"
+ )
expected_command = [
"snapcraft",
@@ -1395,8 +1399,12 @@ def test_lifecycle_run_in_provider_all_options(
mock_prepare_instance = mocker.patch(
"snapcraft.parts.lifecycle.providers.prepare_instance"
)
- mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-1")
- mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-2")
+ mocker.patch(
+ "snapcraft.models.project.Project.get_build_on", return_value="test-arch-1"
+ )
+ mocker.patch(
+ "snapcraft.models.project.Project.get_build_for", return_value="test-arch-2"
+ )
# build the expected command to be executed in the provider
parts = ["test-part-1", "test-part-2"]
@@ -1499,8 +1507,12 @@ def test_lifecycle_run_in_provider_try(
mocker.patch("snapcraft.parts.lifecycle.providers.capture_logs_from_instance")
mocker.patch("snapcraft.parts.lifecycle.providers.ensure_provider_is_available")
mocker.patch("snapcraft.parts.lifecycle.providers.prepare_instance")
- mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-1")
- mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-2")
+ mocker.patch(
+ "snapcraft.models.project.Project.get_build_on", return_value="test-arch-1"
+ )
+ mocker.patch(
+ "snapcraft.models.project.Project.get_build_for", return_value="test-arch-2"
+ )
project = Project.unmarshal(snapcraft_yaml(base="core22"))
parts_lifecycle._run_in_provider(
@@ -1551,8 +1563,8 @@ def test_lifecycle_run_in_provider(
mocker.patch("snapcraft.parts.lifecycle.providers.capture_logs_from_instance")
mocker.patch("snapcraft.parts.lifecycle.providers.ensure_provider_is_available")
mocker.patch("snapcraft.parts.lifecycle.providers.prepare_instance")
- mocker.patch("snapcraft.projects.Project.get_build_on")
- mocker.patch("snapcraft.projects.Project.get_build_for")
+ mocker.patch("snapcraft.models.project.Project.get_build_on")
+ mocker.patch("snapcraft.models.project.Project.get_build_for")
project = Project.unmarshal(snapcraft_yaml(base="core22"))
parts_lifecycle._run_in_provider(
@@ -1586,7 +1598,9 @@ def test_lifecycle_run_in_provider_devel_base(
tmp_path,
):
"""Verify the `devel` base is handled properly when launching an instance."""
- mocker.patch("snapcraft.projects.Project.get_effective_base", return_value="devel")
+ mocker.patch(
+ "snapcraft.models.project.Project.get_effective_base", return_value="devel"
+ )
mock_base_configuration = Mock()
mocker.patch(
"snapcraft.parts.lifecycle.providers.get_base_configuration",
@@ -1595,8 +1609,8 @@ def test_lifecycle_run_in_provider_devel_base(
mocker.patch("snapcraft.parts.lifecycle.providers.capture_logs_from_instance")
mocker.patch("snapcraft.parts.lifecycle.providers.ensure_provider_is_available")
mocker.patch("snapcraft.parts.lifecycle.providers.prepare_instance")
- mocker.patch("snapcraft.projects.Project.get_build_on")
- mocker.patch("snapcraft.projects.Project.get_build_for")
+ mocker.patch("snapcraft.models.project.Project.get_build_on")
+ mocker.patch("snapcraft.models.project.Project.get_build_for")
project = Project.unmarshal(snapcraft_yaml(base="core22"))
parts_lifecycle._run_in_provider(
diff --git a/tests/unit/parts/test_project_check.py b/tests/unit/parts/test_project_check.py
index 0dd680c4e9..035127c934 100644
--- a/tests/unit/parts/test_project_check.py
+++ b/tests/unit/parts/test_project_check.py
@@ -20,9 +20,8 @@
import pytest
import yaml
-from snapcraft import errors
+from snapcraft import errors, models
from snapcraft.parts.project_check import run_project_checks
-from snapcraft.projects import Project
@pytest.fixture
@@ -48,7 +47,7 @@ def snapcraft_yaml(new_dir):
def test_no_snap_dir(emitter, snapcraft_yaml):
- project = Project.unmarshal(snapcraft_yaml)
+ project = models.Project.unmarshal(snapcraft_yaml)
run_project_checks(project, assets_dir=Path("snap"))
emitter.assert_interactions([])
@@ -72,7 +71,7 @@ def test_icon(new_dir):
)
yaml_data = yaml.safe_load(content)
- project = Project.unmarshal(yaml_data)
+ project = models.Project.unmarshal(yaml_data)
# Test without icon raises error
with pytest.raises(errors.SnapcraftError) as raised:
@@ -86,7 +85,7 @@ def test_icon(new_dir):
def test_accepted_artifacts(new_dir, emitter, snapcraft_yaml):
- project = Project.unmarshal(snapcraft_yaml)
+ project = models.Project.unmarshal(snapcraft_yaml)
assets_dir = Path("snap")
file_assets = [
@@ -114,7 +113,7 @@ def test_accepted_artifacts(new_dir, emitter, snapcraft_yaml):
def test_unexpected_things(new_dir, emitter, snapcraft_yaml):
- project = Project.unmarshal(snapcraft_yaml)
+ project = models.Project.unmarshal(snapcraft_yaml)
assets_dir = Path("snap")
file_assets = [
diff --git a/tests/unit/parts/test_setup_assets.py b/tests/unit/parts/test_setup_assets.py
index eaf75825b1..24510492c5 100644
--- a/tests/unit/parts/test_setup_assets.py
+++ b/tests/unit/parts/test_setup_assets.py
@@ -22,7 +22,7 @@
import pytest
-from snapcraft import errors
+from snapcraft import errors, models
from snapcraft.parts import setup_assets as parts_setup_assets
from snapcraft.parts.setup_assets import (
_create_hook_wrappers,
@@ -32,7 +32,6 @@
_write_hook_wrapper,
setup_assets,
)
-from snapcraft.projects import Project
@pytest.fixture
@@ -89,7 +88,7 @@ def kernel_yaml_file(new_dir):
def test_gadget(yaml_data, gadget_yaml_file, new_dir):
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"type": "gadget",
@@ -113,7 +112,7 @@ def test_gadget(yaml_data, gadget_yaml_file, new_dir):
def test_gadget_missing(yaml_data, new_dir):
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"type": "gadget",
@@ -136,7 +135,7 @@ def test_gadget_missing(yaml_data, new_dir):
def test_kernel(yaml_data, kernel_yaml_file, new_dir):
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
{
"name": "custom-kernel",
"type": "kernel",
@@ -161,7 +160,7 @@ def test_kernel(yaml_data, kernel_yaml_file, new_dir):
def test_kernel_missing(yaml_data, new_dir):
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
{
"name": "custom-kernel",
"type": "kernel",
@@ -225,7 +224,7 @@ def test_setup_assets_happy(self, desktop_file, yaml_data, new_dir):
Path("prime/usr/share/icons/my-icon.svg").touch()
# define project
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"adopt-info": "part",
@@ -270,7 +269,7 @@ def test_setup_assets_icon_in_assets_dir(self, desktop_file, yaml_data, new_dir)
Path("snap/gui/icon.svg").touch()
# define project
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"adopt-info": "part",
@@ -319,7 +318,7 @@ def test_setup_assets_no_apps(self, desktop_file, yaml_data, new_dir):
Path("snap/gui").mkdir()
# define project
- project = Project.unmarshal(yaml_data({"adopt-info": "part"}))
+ project = models.Project.unmarshal(yaml_data({"adopt-info": "part"}))
# setting up assets does not crash
setup_assets(
@@ -337,7 +336,7 @@ def test_setup_assets_remote_icon(self, desktop_file, yaml_data, new_dir):
# define project
# pylint: disable=line-too-long
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"adopt-info": "part",
@@ -388,7 +387,7 @@ class TestCommandChain:
"""Command chain items are valid."""
def test_setup_assets_app_command_chain_error(self, yaml_data, new_dir):
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"adopt-info": "part1",
@@ -417,7 +416,7 @@ def test_setup_assets_app_command_chain_error(self, yaml_data, new_dir):
def test_setup_assets_hook_command_chain_error(self, yaml_data, new_dir):
# define project
- project = Project.unmarshal(
+ project = models.Project.unmarshal(
yaml_data(
{
"adopt-info": "part1",
diff --git a/tests/unit/parts/test_update_metadata.py b/tests/unit/parts/test_update_metadata.py
index 538310c93b..893e65db7d 100644
--- a/tests/unit/parts/test_update_metadata.py
+++ b/tests/unit/parts/test_update_metadata.py
@@ -21,8 +21,8 @@
import pytest
from snapcraft.meta import ExtractedMetadata
+from snapcraft.models import App, Project
from snapcraft.parts.update_metadata import update_project_metadata
-from snapcraft.projects import App, Project
@pytest.fixture
diff --git a/tests/unit/parts/test_yaml_utils.py b/tests/unit/parts/test_yaml_utils.py
index 02fe5387f1..6a69e25e26 100644
--- a/tests/unit/parts/test_yaml_utils.py
+++ b/tests/unit/parts/test_yaml_utils.py
@@ -21,8 +21,8 @@
import pytest
from snapcraft import errors
+from snapcraft.models import Architecture
from snapcraft.parts import yaml_utils
-from snapcraft.projects import Architecture
def test_yaml_load():