craft-application 2.0.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {craft-application-2.0.0 → craft-application-2.2.0}/PKG-INFO +2 -2
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/__init__.py +1 -1
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/_version.py +2 -2
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/application.py +101 -26
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/errors.py +2 -2
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/recipe.py +53 -14
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/package.py +6 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/yaml.py +1 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application.egg-info/PKG-INFO +2 -2
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application.egg-info/SOURCES.txt +4 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application.egg-info/requires.txt +1 -1
- {craft-application-2.0.0 → craft-application-2.2.0}/pyproject.toml +1 -1
- craft-application-2.2.0/tests/integration/data/valid_projects/grammar/src/on-amd64-to-amd64/hello.txt +1 -0
- craft-application-2.2.0/tests/integration/data/valid_projects/grammar/src/on-amd64-to-arm64/hello.txt +1 -0
- craft-application-2.2.0/tests/integration/data/valid_projects/grammar/stderr +1 -0
- craft-application-2.2.0/tests/integration/data/valid_projects/grammar/testcraft.yaml +11 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/test_application.py +5 -5
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/test_launchpad.py +62 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/test_application.py +331 -26
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/test_yaml.py +10 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.editorconfig +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/.jira_sync_config.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/ISSUE_TEMPLATE/bug.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/ISSUE_TEMPLATE/task.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/release-drafter.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/release-drafter.yml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/renovate.json5 +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/workflows/cla-check.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/workflows/docs.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/workflows/release-drafter.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.github/workflows/tests.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.gitignore +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.pre-commit-config.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.readthedocs.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/.yamllint.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/HACKING.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/LICENSE +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/README.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/commands/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/commands/base.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/commands/lifecycle.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/commands/other.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/grammar.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/errors.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/launchpad.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/base.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/build.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/code.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/distro.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/project.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/util.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/misc/instance_bashrc +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/models/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/models/base.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/models/constraints.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/models/grammar.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/models/metadata.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/models/project.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/py.typed +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/remote/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/remote/errors.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/remote/git.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/remote/utils.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/remote/worktree.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/secrets.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/base.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/lifecycle.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/provider.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/remotebuild.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/request.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/services/service_factory.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/callbacks.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/error_formatting.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/logging.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/paths.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/platforms.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/repositories.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/snap_config.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application/util/string.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application.egg-info/dependency_links.txt +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/craft_application.egg-info/top_level.txt +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/_static/css/custom.css +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/conf.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/explanation/index.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/howto/index.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/index.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/reference/index.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/docs/tutorials/index.rst +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/setup.cfg +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/build-secrets/secret-source-folder/source-file.txt +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/build-secrets/testcraft.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/invalid_projects/build-error/testcraft.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/valid_projects/adoption/stderr +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/valid_projects/adoption/testcraft.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/valid_projects/basic/stderr +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/valid_projects/basic/testcraft.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/valid_projects/environment/stderr +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/data/valid_projects/environment/testcraft.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/launchpad/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/launchpad/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/launchpad/test_anonymous_access.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/services/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/services/test_lifecycle.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/services/test_provider.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/services/test_request.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/services/test_service_factory.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/integration/test_version.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/commands/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/commands/test_base.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/commands/test_lifecycle.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/commands/test_other.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/models/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/models/test_base.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/models/test_code.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/launchpad/test_util.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/models/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/models/project_models/basic_project.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/models/project_models/full_project.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/models/project_models/invalid_project.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/models/test_constraints.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/models/test_project.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/remote/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/remote/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/remote/test_errors.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/remote/test_git.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/remote/test_utils.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/remote/test_worktree.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/conftest.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_lifecycle.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_package.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_provider.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_remotebuild.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_repositories.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_request.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/services/test_service_factory.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/test_errors.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/test_grammar.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/test_nothing.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/test_secrets.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/__init__.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/invalid_yaml/_README +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/invalid_yaml/duplicate_second_level.yaml-invalid +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/invalid_yaml/duplicate_top_level.yaml-invalid +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/invalid_yaml/unhashable.yaml-invalid +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/test_error_formatting.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/test_logging.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/test_paths.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/test_snap_config.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/test_string.py +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tests/unit/util/valid_yaml/empty.yaml +0 -0
- {craft-application-2.0.0 → craft-application-2.2.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: craft-application
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A framework for *craft applications.
|
|
5
5
|
Classifier: Development Status :: 5 - Production/Stable
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -35,7 +35,7 @@ Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
|
35
35
|
Requires-Dist: pyfakefs~=5.3; extra == "dev"
|
|
36
36
|
Requires-Dist: pytest==8.1.1; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-check==2.3.1; extra == "dev"
|
|
38
|
-
Requires-Dist: pytest-cov==
|
|
38
|
+
Requires-Dist: pytest-cov==5.0.0; extra == "dev"
|
|
39
39
|
Requires-Dist: pytest-mock==3.12.0; extra == "dev"
|
|
40
40
|
Requires-Dist: pytest-rerunfailures==13.0; extra == "dev"
|
|
41
41
|
Requires-Dist: pytest-time>=0.3.1; extra == "dev"
|
|
@@ -32,7 +32,7 @@ except ImportError: # pragma: no cover
|
|
|
32
32
|
from importlib.metadata import version, PackageNotFoundError
|
|
33
33
|
|
|
34
34
|
try:
|
|
35
|
-
__version__ = version("craft-
|
|
35
|
+
__version__ = version("craft-application")
|
|
36
36
|
except PackageNotFoundError:
|
|
37
37
|
__version__ = "dev"
|
|
38
38
|
|
|
@@ -125,6 +125,7 @@ class Application:
|
|
|
125
125
|
self._command_groups: list[craft_cli.CommandGroup] = []
|
|
126
126
|
self._global_arguments: list[craft_cli.GlobalArgument] = [GLOBAL_VERSION]
|
|
127
127
|
self._cli_loggers = DEFAULT_CLI_LOGGERS | set(extra_loggers)
|
|
128
|
+
self._full_build_plan: list[models.BuildInfo] = []
|
|
128
129
|
self._build_plan: list[models.BuildInfo] = []
|
|
129
130
|
# When build_secrets are enabled, this contains the secret info to pass to
|
|
130
131
|
# managed instances.
|
|
@@ -235,6 +236,7 @@ class Application:
|
|
|
235
236
|
"""
|
|
236
237
|
if project_dir is None:
|
|
237
238
|
project_dir = self.project_dir
|
|
239
|
+
|
|
238
240
|
return (project_dir / f"{self.app.name}.yaml").resolve(strict=True)
|
|
239
241
|
|
|
240
242
|
def get_project(
|
|
@@ -255,18 +257,29 @@ class Application:
|
|
|
255
257
|
if self.__project is not None:
|
|
256
258
|
return self.__project
|
|
257
259
|
|
|
258
|
-
|
|
260
|
+
try:
|
|
261
|
+
project_path = self._resolve_project_path(self.project_dir)
|
|
262
|
+
except FileNotFoundError as err:
|
|
263
|
+
raise errors.ProjectFileMissingError(
|
|
264
|
+
f"Project file '{self.app.name}.yaml' not found in '{self.project_dir}'.",
|
|
265
|
+
details="The project file could not be found.",
|
|
266
|
+
resolution="Ensure the project file exists.",
|
|
267
|
+
retcode=66, # EX_NOINPUT from sysexits.h
|
|
268
|
+
) from err
|
|
259
269
|
craft_cli.emit.debug(f"Loading project file '{project_path!s}'")
|
|
260
270
|
|
|
261
271
|
with project_path.open() as file:
|
|
262
272
|
yaml_data = util.safe_yaml_load(file)
|
|
263
273
|
|
|
274
|
+
host_arch = util.get_host_architecture()
|
|
264
275
|
build_planner = self.app.BuildPlannerClass.unmarshal(yaml_data)
|
|
265
|
-
|
|
266
|
-
self._build_plan =
|
|
276
|
+
self._full_build_plan = build_planner.get_build_plan()
|
|
277
|
+
self._build_plan = filter_plan(
|
|
278
|
+
self._full_build_plan, platform, build_for, host_arch
|
|
279
|
+
)
|
|
267
280
|
|
|
268
|
-
if platform:
|
|
269
|
-
all_platforms = {b.platform: b for b in
|
|
281
|
+
if platform and not build_for:
|
|
282
|
+
all_platforms = {b.platform: b for b in self._full_build_plan}
|
|
270
283
|
if platform not in all_platforms:
|
|
271
284
|
raise errors.InvalidPlatformError(platform, list(all_platforms.keys()))
|
|
272
285
|
build_for = all_platforms[platform].build_for
|
|
@@ -274,7 +287,7 @@ class Application:
|
|
|
274
287
|
# validate project grammar
|
|
275
288
|
GrammarAwareProject.validate_grammar(yaml_data)
|
|
276
289
|
|
|
277
|
-
build_on =
|
|
290
|
+
build_on = host_arch
|
|
278
291
|
yaml_data = self._transform_project_yaml(yaml_data, build_on, build_for)
|
|
279
292
|
self.__project = self.app.ProjectClass.from_yaml_data(yaml_data, project_path)
|
|
280
293
|
|
|
@@ -310,7 +323,10 @@ class Application:
|
|
|
310
323
|
if build_for and build_for != build_info.build_for:
|
|
311
324
|
continue
|
|
312
325
|
|
|
313
|
-
env = {
|
|
326
|
+
env = {
|
|
327
|
+
"CRAFT_PLATFORM": build_info.platform,
|
|
328
|
+
"CRAFT_VERBOSITY_LEVEL": craft_cli.emit.get_mode().name,
|
|
329
|
+
}
|
|
314
330
|
|
|
315
331
|
if self.app.features.build_secrets:
|
|
316
332
|
# If using build secrets, put them in the environment of the managed
|
|
@@ -351,12 +367,7 @@ class Application:
|
|
|
351
367
|
|
|
352
368
|
:returns: A ready-to-run Dispatcher object
|
|
353
369
|
"""
|
|
354
|
-
dispatcher =
|
|
355
|
-
self.app.name,
|
|
356
|
-
self.command_groups,
|
|
357
|
-
summary=str(self.app.summary),
|
|
358
|
-
extra_global_args=self._global_arguments,
|
|
359
|
-
)
|
|
370
|
+
dispatcher = self._create_dispatcher()
|
|
360
371
|
|
|
361
372
|
try:
|
|
362
373
|
craft_cli.emit.trace("pre-parsing arguments...")
|
|
@@ -371,8 +382,8 @@ class Application:
|
|
|
371
382
|
global_args = dispatcher.pre_parse_args(sys.argv[1:])
|
|
372
383
|
|
|
373
384
|
if global_args.get("version"):
|
|
385
|
+
craft_cli.emit.message(f"{self.app.name} {self.app.version}")
|
|
374
386
|
craft_cli.emit.ended_ok()
|
|
375
|
-
print(f"{self.app.name} {self.app.version}")
|
|
376
387
|
sys.exit(0)
|
|
377
388
|
except craft_cli.ProvideHelpException as err:
|
|
378
389
|
print(err, file=sys.stderr) # to stderr, as argparse normally does
|
|
@@ -400,6 +411,19 @@ class Application:
|
|
|
400
411
|
|
|
401
412
|
return dispatcher
|
|
402
413
|
|
|
414
|
+
def _create_dispatcher(self) -> craft_cli.Dispatcher:
|
|
415
|
+
"""Create the Dispatcher that will run the application's command.
|
|
416
|
+
|
|
417
|
+
Subclasses can override this if they need to create a Dispatcher with
|
|
418
|
+
different parameters.
|
|
419
|
+
"""
|
|
420
|
+
return craft_cli.Dispatcher(
|
|
421
|
+
self.app.name,
|
|
422
|
+
self.command_groups,
|
|
423
|
+
summary=str(self.app.summary),
|
|
424
|
+
extra_global_args=self._global_arguments,
|
|
425
|
+
)
|
|
426
|
+
|
|
403
427
|
def _get_app_plugins(self) -> dict[str, PluginType]:
|
|
404
428
|
"""Get the plugins for this application.
|
|
405
429
|
|
|
@@ -490,6 +514,7 @@ class Application:
|
|
|
490
514
|
return_code = 128 + signal.SIGINT
|
|
491
515
|
except craft_cli.CraftError as err:
|
|
492
516
|
self._emit_error(err)
|
|
517
|
+
return_code = err.retcode
|
|
493
518
|
except craft_parts.PartsError as err:
|
|
494
519
|
self._emit_error(
|
|
495
520
|
craft_cli.CraftError(
|
|
@@ -549,7 +574,20 @@ class Application:
|
|
|
549
574
|
self._render_secrets(yaml_data)
|
|
550
575
|
|
|
551
576
|
# Expand grammar.
|
|
552
|
-
if
|
|
577
|
+
if "parts" in yaml_data:
|
|
578
|
+
if not build_for:
|
|
579
|
+
# At the moment there is no perfect solution for what do to do
|
|
580
|
+
# resolve the grammar if there's no explicitly-provided target
|
|
581
|
+
# arch. However, we must resolve it with *something* otherwise
|
|
582
|
+
# we might have an invalid parts definition full of grammar
|
|
583
|
+
# declarations.
|
|
584
|
+
if self._build_plan:
|
|
585
|
+
# If we have a build plan use one of its items, because
|
|
586
|
+
# it's likely contemplated on the grammar.
|
|
587
|
+
build_for = self._build_plan[0].build_for
|
|
588
|
+
else:
|
|
589
|
+
# As a last resort, default to the same arch as the host.
|
|
590
|
+
build_for = build_on
|
|
553
591
|
craft_cli.emit.debug(f"Processing grammar (on {build_on} for {build_for})")
|
|
554
592
|
yaml_data["parts"] = grammar.process_parts(
|
|
555
593
|
parts_yaml_data=yaml_data["parts"],
|
|
@@ -623,32 +661,69 @@ class Application:
|
|
|
623
661
|
# Set the logging level to DEBUG for all craft-libraries. This is OK even if
|
|
624
662
|
# the specific application doesn't use a specific library, the call does not
|
|
625
663
|
# import the package.
|
|
664
|
+
emitter_mode: craft_cli.EmitterMode = craft_cli.EmitterMode.BRIEF
|
|
665
|
+
invalid_emitter_level = False
|
|
626
666
|
util.setup_loggers(*self._cli_loggers)
|
|
627
667
|
|
|
668
|
+
# environment variable takes precedence over the default
|
|
669
|
+
emitter_verbosity_level_env = os.environ.get("CRAFT_VERBOSITY_LEVEL", None)
|
|
670
|
+
|
|
671
|
+
if emitter_verbosity_level_env:
|
|
672
|
+
try:
|
|
673
|
+
emitter_mode = craft_cli.EmitterMode[
|
|
674
|
+
emitter_verbosity_level_env.strip().upper()
|
|
675
|
+
]
|
|
676
|
+
except KeyError:
|
|
677
|
+
invalid_emitter_level = True
|
|
678
|
+
|
|
628
679
|
craft_cli.emit.init(
|
|
629
|
-
mode=
|
|
680
|
+
mode=emitter_mode,
|
|
630
681
|
appname=self.app.name,
|
|
631
682
|
greeting=f"Starting {self.app.name}, version {self.app.version}",
|
|
632
683
|
log_filepath=self.log_path,
|
|
633
684
|
streaming_brief=True,
|
|
634
685
|
)
|
|
635
686
|
|
|
687
|
+
craft_cli.emit.debug(f"Log verbosity level set to {emitter_mode.name}")
|
|
688
|
+
|
|
689
|
+
if invalid_emitter_level:
|
|
690
|
+
craft_cli.emit.progress(
|
|
691
|
+
f"Invalid verbosity level '{emitter_verbosity_level_env}', using default 'BRIEF'.\n"
|
|
692
|
+
f"Valid levels are: {', '.join(emitter.name for emitter in craft_cli.EmitterMode)}",
|
|
693
|
+
permanent=True,
|
|
694
|
+
)
|
|
695
|
+
|
|
636
696
|
|
|
637
|
-
def
|
|
697
|
+
def filter_plan(
|
|
638
698
|
build_plan: list[BuildInfo],
|
|
639
699
|
platform: str | None,
|
|
640
700
|
build_for: str | None,
|
|
701
|
+
host_arch: str | None,
|
|
641
702
|
) -> list[BuildInfo]:
|
|
642
|
-
"""Filter out
|
|
643
|
-
|
|
703
|
+
"""Filter out build plans that are not matching build-on, build-for, and platform.
|
|
704
|
+
|
|
705
|
+
If the host_arch is None, ignore the build-on check for remote builds.
|
|
706
|
+
"""
|
|
707
|
+
new_plan_matched_build_for: list[BuildInfo] = []
|
|
708
|
+
new_plan_matched_platform_name: list[BuildInfo] = []
|
|
644
709
|
|
|
645
|
-
plan: list[BuildInfo] = []
|
|
646
710
|
for build_info in build_plan:
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
711
|
+
if platform and build_info.platform != platform:
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
if host_arch and build_info.build_on != host_arch:
|
|
715
|
+
continue
|
|
716
|
+
|
|
717
|
+
if build_for and build_info.build_for != build_for:
|
|
718
|
+
continue
|
|
719
|
+
|
|
720
|
+
if build_for and build_info.platform == build_for:
|
|
721
|
+
# prioritize platform name if matched build_for
|
|
722
|
+
new_plan_matched_platform_name.append(build_info)
|
|
723
|
+
continue
|
|
650
724
|
|
|
651
|
-
|
|
652
|
-
plan.append(build_info)
|
|
725
|
+
new_plan_matched_build_for.append(build_info)
|
|
653
726
|
|
|
654
|
-
|
|
727
|
+
if new_plan_matched_platform_name:
|
|
728
|
+
return new_plan_matched_platform_name
|
|
729
|
+
return new_plan_matched_build_for
|
|
@@ -126,7 +126,7 @@ class EmptyBuildPlanError(CraftError):
|
|
|
126
126
|
|
|
127
127
|
def __init__(self) -> None:
|
|
128
128
|
message = "No build matches the current platform."
|
|
129
|
-
resolution = 'Check the "--platform"
|
|
129
|
+
resolution = 'Check the "--platform" and "--build-for" parameters.'
|
|
130
130
|
|
|
131
131
|
super().__init__(message=message, resolution=resolution)
|
|
132
132
|
|
|
@@ -136,7 +136,7 @@ class MultipleBuildsError(CraftError):
|
|
|
136
136
|
|
|
137
137
|
def __init__(self) -> None:
|
|
138
138
|
message = "Multiple builds match the current platform."
|
|
139
|
-
resolution = 'Check the "--platform"
|
|
139
|
+
resolution = 'Check the "--platform" and "--bulld-for" parameters.'
|
|
140
140
|
|
|
141
141
|
super().__init__(message=message, resolution=resolution)
|
|
142
142
|
|
{craft-application-2.0.0 → craft-application-2.2.0}/craft_application/launchpad/models/recipe.py
RENAMED
|
@@ -34,6 +34,7 @@ from collections.abc import Collection, Iterable
|
|
|
34
34
|
from typing import TYPE_CHECKING
|
|
35
35
|
|
|
36
36
|
import lazr.restfulclient.errors # type: ignore[import-untyped]
|
|
37
|
+
from craft_cli import emit
|
|
37
38
|
from typing_extensions import Any, Self, TypedDict, override
|
|
38
39
|
|
|
39
40
|
from .. import errors, util
|
|
@@ -43,6 +44,8 @@ from .base import LaunchpadObject, Pocket
|
|
|
43
44
|
if TYPE_CHECKING:
|
|
44
45
|
from .. import Launchpad
|
|
45
46
|
|
|
47
|
+
_ATTEMPT_COUNT = 4
|
|
48
|
+
|
|
46
49
|
|
|
47
50
|
class RecipeType(enum.Enum):
|
|
48
51
|
"""The type of recipe."""
|
|
@@ -244,13 +247,31 @@ class SnapRecipe(_StoreRecipe):
|
|
|
244
247
|
"An auto-build archive may only be provided if auto-build is enabled.",
|
|
245
248
|
)
|
|
246
249
|
|
|
247
|
-
snap_entry =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
snap_entry = None
|
|
251
|
+
|
|
252
|
+
for attempt in range(_ATTEMPT_COUNT):
|
|
253
|
+
emit.debug(
|
|
254
|
+
f"Trying to create snap recipe {name!r} (attempt {attempt + 1}/{_ATTEMPT_COUNT})"
|
|
255
|
+
)
|
|
256
|
+
try:
|
|
257
|
+
snap_entry = lp.lp.snaps.new(
|
|
258
|
+
name=name,
|
|
259
|
+
owner=util.get_person_link(owner),
|
|
260
|
+
store_upload=bool(store_name),
|
|
261
|
+
auto_build=auto_build,
|
|
262
|
+
**kwargs,
|
|
263
|
+
)
|
|
264
|
+
except lazr.restfulclient.errors.BadRequest as err:
|
|
265
|
+
emit.debug(str(err))
|
|
266
|
+
if attempt >= _ATTEMPT_COUNT - 1:
|
|
267
|
+
raise
|
|
268
|
+
time.sleep(3)
|
|
269
|
+
continue
|
|
270
|
+
else:
|
|
271
|
+
break
|
|
272
|
+
|
|
273
|
+
if not snap_entry:
|
|
274
|
+
raise ValueError("Failed to create snap recipe")
|
|
254
275
|
|
|
255
276
|
return cls(lp, lp_obj=snap_entry)
|
|
256
277
|
|
|
@@ -370,13 +391,31 @@ class CharmRecipe(_StoreRecipe):
|
|
|
370
391
|
)
|
|
371
392
|
cls._fill_repo_info(kwargs, git_ref=git_ref)
|
|
372
393
|
|
|
373
|
-
charm_entry =
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
394
|
+
charm_entry = None
|
|
395
|
+
|
|
396
|
+
for attempt in range(_ATTEMPT_COUNT):
|
|
397
|
+
emit.debug(
|
|
398
|
+
f"Trying to create charm recipe {name!r} (attempt {attempt + 1}/{_ATTEMPT_COUNT})"
|
|
399
|
+
)
|
|
400
|
+
try:
|
|
401
|
+
charm_entry = lp.lp.charm_recipes.new(
|
|
402
|
+
name=name,
|
|
403
|
+
owner=util.get_person_link(owner),
|
|
404
|
+
project=f"/{project}",
|
|
405
|
+
auto_build=auto_build,
|
|
406
|
+
**kwargs,
|
|
407
|
+
)
|
|
408
|
+
except lazr.restfulclient.errors.BadRequest as err:
|
|
409
|
+
emit.debug(str(err))
|
|
410
|
+
if attempt >= _ATTEMPT_COUNT - 1:
|
|
411
|
+
raise
|
|
412
|
+
time.sleep(3)
|
|
413
|
+
continue
|
|
414
|
+
else:
|
|
415
|
+
break
|
|
416
|
+
|
|
417
|
+
if not charm_entry:
|
|
418
|
+
raise ValueError("Failed to create charm recipe")
|
|
380
419
|
|
|
381
420
|
return cls(lp, charm_entry)
|
|
382
421
|
|
|
@@ -57,6 +57,9 @@ class PackageService(base.ProjectService):
|
|
|
57
57
|
emit.debug(f"Update project variables: {update_vars}")
|
|
58
58
|
self._project.__dict__.update(update_vars)
|
|
59
59
|
|
|
60
|
+
# Give subclasses a chance to update the project with their own logic
|
|
61
|
+
self._extra_project_updates()
|
|
62
|
+
|
|
60
63
|
unset_fields = [
|
|
61
64
|
field
|
|
62
65
|
for field in self._app.mandatory_adoptable_fields
|
|
@@ -78,3 +81,6 @@ class PackageService(base.ProjectService):
|
|
|
78
81
|
"""
|
|
79
82
|
path.mkdir(parents=True, exist_ok=True)
|
|
80
83
|
self.metadata.to_yaml_file(path / "metadata.yaml")
|
|
84
|
+
|
|
85
|
+
def _extra_project_updates(self) -> None:
|
|
86
|
+
"""Perform domain-specific updates to the project before packing."""
|
|
@@ -132,6 +132,7 @@ def dump_yaml(data: Any, stream: TextIO | None = None, **kwargs: Any) -> str | N
|
|
|
132
132
|
str, _repr_str, Dumper=cast(type[yaml.Dumper], yaml.SafeDumper)
|
|
133
133
|
)
|
|
134
134
|
kwargs.setdefault("sort_keys", False)
|
|
135
|
+
kwargs.setdefault("allow_unicode", True)
|
|
135
136
|
return cast( # This cast is needed for pyright but not mypy
|
|
136
137
|
str | None, yaml.dump(data, stream, Dumper=yaml.SafeDumper, **kwargs)
|
|
137
138
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: craft-application
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A framework for *craft applications.
|
|
5
5
|
Classifier: Development Status :: 5 - Production/Stable
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -35,7 +35,7 @@ Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
|
35
35
|
Requires-Dist: pyfakefs~=5.3; extra == "dev"
|
|
36
36
|
Requires-Dist: pytest==8.1.1; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-check==2.3.1; extra == "dev"
|
|
38
|
-
Requires-Dist: pytest-cov==
|
|
38
|
+
Requires-Dist: pytest-cov==5.0.0; extra == "dev"
|
|
39
39
|
Requires-Dist: pytest-mock==3.12.0; extra == "dev"
|
|
40
40
|
Requires-Dist: pytest-rerunfailures==13.0; extra == "dev"
|
|
41
41
|
Requires-Dist: pytest-time>=0.3.1; extra == "dev"
|
|
@@ -99,6 +99,10 @@ tests/integration/data/valid_projects/basic/stderr
|
|
|
99
99
|
tests/integration/data/valid_projects/basic/testcraft.yaml
|
|
100
100
|
tests/integration/data/valid_projects/environment/stderr
|
|
101
101
|
tests/integration/data/valid_projects/environment/testcraft.yaml
|
|
102
|
+
tests/integration/data/valid_projects/grammar/stderr
|
|
103
|
+
tests/integration/data/valid_projects/grammar/testcraft.yaml
|
|
104
|
+
tests/integration/data/valid_projects/grammar/src/on-amd64-to-amd64/hello.txt
|
|
105
|
+
tests/integration/data/valid_projects/grammar/src/on-amd64-to-arm64/hello.txt
|
|
102
106
|
tests/integration/launchpad/__init__.py
|
|
103
107
|
tests/integration/launchpad/conftest.py
|
|
104
108
|
tests/integration/launchpad/test_anonymous_access.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
on-amd64-to-amd64
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
on-amd64-to-arm64
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Packed package_1.0.tar.zst
|
|
@@ -103,7 +103,7 @@ INVALID_PROJECTS_DIR = TEST_DATA_DIR / "invalid_projects"
|
|
|
103
103
|
(["-h"], "", BASIC_USAGE, 0),
|
|
104
104
|
(["--version"], VERSION_INFO, "", 0),
|
|
105
105
|
(["-V"], VERSION_INFO, "", 0),
|
|
106
|
-
(["-q", "--version"],
|
|
106
|
+
(["-q", "--version"], "", "", 0),
|
|
107
107
|
(["--invalid-parameter"], "", BASIC_USAGE, 64),
|
|
108
108
|
(["non-command"], "", INVALID_COMMAND, 64),
|
|
109
109
|
],
|
|
@@ -214,15 +214,15 @@ def test_non_lifecycle_command_does_not_require_project(monkeypatch, app):
|
|
|
214
214
|
|
|
215
215
|
|
|
216
216
|
@pytest.mark.parametrize("cmd", ["clean", "pull", "build", "stage", "prime", "pack"])
|
|
217
|
-
def test_run_always_load_project(monkeypatch, app, cmd):
|
|
217
|
+
def test_run_always_load_project(capsys, monkeypatch, app, cmd):
|
|
218
218
|
"""Run a lifecycle command without having a project shall fail."""
|
|
219
219
|
monkeypatch.setenv("CRAFT_DEBUG", "1")
|
|
220
220
|
monkeypatch.setattr("sys.argv", ["testcraft", cmd])
|
|
221
221
|
|
|
222
|
-
|
|
223
|
-
app.run()
|
|
222
|
+
assert app.run() == 66 # noqa: PLR2004
|
|
224
223
|
|
|
225
|
-
|
|
224
|
+
captured = capsys.readouterr()
|
|
225
|
+
assert "'testcraft.yaml' not found" in captured.err
|
|
226
226
|
|
|
227
227
|
|
|
228
228
|
@pytest.mark.parametrize("help_param", ["-h", "--help"])
|
|
@@ -22,9 +22,11 @@ from unittest import mock
|
|
|
22
22
|
|
|
23
23
|
import launchpadlib.launchpad
|
|
24
24
|
import launchpadlib.uris
|
|
25
|
+
import lazr.restfulclient.errors
|
|
25
26
|
import pytest
|
|
26
27
|
from craft_application import launchpad
|
|
27
28
|
from craft_application.launchpad import models
|
|
29
|
+
from lazr.restfulclient.resource import Entry
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def flatten_enum(e: type[enum.Enum]) -> list:
|
|
@@ -180,6 +182,36 @@ def test_get_recipe_snap(fake_launchpad, type_, owner):
|
|
|
180
182
|
)
|
|
181
183
|
|
|
182
184
|
|
|
185
|
+
def test_recipe_snap_new_retry(emitter, mocker):
|
|
186
|
+
"""Test that a SnapRecipe is retried when it fails to create."""
|
|
187
|
+
mocker.patch("time.sleep")
|
|
188
|
+
response = mocker.Mock(
|
|
189
|
+
status=400, reason="Bad Request", items=mocker.Mock(return_value=[])
|
|
190
|
+
)
|
|
191
|
+
mock_launchpad = mock.Mock()
|
|
192
|
+
|
|
193
|
+
mock_entry = mock.Mock(spec=Entry)
|
|
194
|
+
mock_entry.resource_type_link = "http://blah/#snap"
|
|
195
|
+
mock_entry.name = "test-snap"
|
|
196
|
+
|
|
197
|
+
mock_launchpad.lp.snaps.new = mock.Mock(
|
|
198
|
+
side_effect=[
|
|
199
|
+
lazr.restfulclient.errors.BadRequest(response=response, content=""),
|
|
200
|
+
mock_entry,
|
|
201
|
+
]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
recipe = launchpad.models.SnapRecipe.new(
|
|
205
|
+
mock_launchpad, "my_recipe", "test_user", git_ref="my_ref"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
assert isinstance(recipe, models.SnapRecipe)
|
|
209
|
+
assert mock_launchpad.lp.snaps.new.call_count == 2 # noqa: PLR2004
|
|
210
|
+
|
|
211
|
+
emitter.assert_debug("Trying to create snap recipe 'my_recipe' (attempt 1/4)")
|
|
212
|
+
emitter.assert_debug("Trying to create snap recipe 'my_recipe' (attempt 2/4)")
|
|
213
|
+
|
|
214
|
+
|
|
183
215
|
@pytest.mark.parametrize("type_", ["charm", "Charm", models.RecipeType.CHARM])
|
|
184
216
|
def test_get_recipe_charm(fake_launchpad, type_):
|
|
185
217
|
with mock.patch("craft_application.launchpad.models.CharmRecipe.get") as mock_get:
|
|
@@ -200,6 +232,36 @@ def test_get_recipe_charm_has_project(fake_launchpad):
|
|
|
200
232
|
fake_launchpad.get_recipe(models.RecipeType.CHARM, "my_recipe")
|
|
201
233
|
|
|
202
234
|
|
|
235
|
+
def test_recipe_charm_new_retry(emitter, mocker):
|
|
236
|
+
"""Test that a CharmRecipe is retried when it fails to create."""
|
|
237
|
+
mocker.patch("time.sleep")
|
|
238
|
+
response = mocker.Mock(
|
|
239
|
+
status=400, reason="Bad Request", items=mocker.Mock(return_value=[])
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
mock_entry = mock.Mock(spec=Entry)
|
|
243
|
+
mock_entry.resource_type_link = "http://whatever/#charm_recipe"
|
|
244
|
+
mock_entry.name = "test-charm"
|
|
245
|
+
|
|
246
|
+
mock_launchpad = mock.Mock()
|
|
247
|
+
mock_launchpad.lp.charm_recipes.new = mock.Mock(
|
|
248
|
+
side_effect=[
|
|
249
|
+
lazr.restfulclient.errors.BadRequest(response=response, content=""),
|
|
250
|
+
mock_entry,
|
|
251
|
+
]
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
recipe = launchpad.models.CharmRecipe.new(
|
|
255
|
+
mock_launchpad, "my_recipe", "test_user", "test_project", git_ref="my_ref"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assert isinstance(recipe, models.CharmRecipe)
|
|
259
|
+
assert mock_launchpad.lp.charm_recipes.new.call_count == 2 # noqa: PLR2004
|
|
260
|
+
|
|
261
|
+
emitter.assert_debug("Trying to create charm recipe 'my_recipe' (attempt 1/4)")
|
|
262
|
+
emitter.assert_debug("Trying to create charm recipe 'my_recipe' (attempt 2/4)")
|
|
263
|
+
|
|
264
|
+
|
|
203
265
|
@pytest.mark.parametrize(
|
|
204
266
|
("name", "owner", "project", "path"),
|
|
205
267
|
[
|