antsibull-nox 0.1.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.
Files changed (41) hide show
  1. antsibull_nox/__init__.py +17 -14
  2. antsibull_nox/_pydantic.py +98 -0
  3. antsibull_nox/ansible.py +260 -0
  4. antsibull_nox/cli.py +132 -0
  5. antsibull_nox/collection/__init__.py +56 -0
  6. antsibull_nox/collection/data.py +106 -0
  7. antsibull_nox/collection/extract.py +23 -0
  8. antsibull_nox/collection/install.py +523 -0
  9. antsibull_nox/collection/search.py +460 -0
  10. antsibull_nox/config.py +378 -0
  11. antsibull_nox/data/action-groups.py +3 -3
  12. antsibull_nox/data/antsibull-nox-lint-config.py +29 -0
  13. antsibull_nox/data/antsibull_nox_data_util.py +91 -0
  14. antsibull_nox/data/license-check.py +6 -2
  15. antsibull_nox/data/no-unwanted-files.py +5 -1
  16. antsibull_nox/data/plugin-yamllint.py +247 -0
  17. antsibull_nox/data_util.py +0 -77
  18. antsibull_nox/init.py +83 -0
  19. antsibull_nox/interpret_config.py +244 -0
  20. antsibull_nox/lint_config.py +113 -0
  21. antsibull_nox/paths.py +19 -0
  22. antsibull_nox/python.py +81 -0
  23. antsibull_nox/sessions/__init__.py +70 -0
  24. antsibull_nox/sessions/ansible_lint.py +58 -0
  25. antsibull_nox/sessions/ansible_test.py +568 -0
  26. antsibull_nox/sessions/build_import_check.py +147 -0
  27. antsibull_nox/sessions/collections.py +137 -0
  28. antsibull_nox/sessions/docs_check.py +78 -0
  29. antsibull_nox/sessions/extra_checks.py +127 -0
  30. antsibull_nox/sessions/license_check.py +73 -0
  31. antsibull_nox/sessions/lint.py +627 -0
  32. antsibull_nox/sessions/utils.py +206 -0
  33. antsibull_nox/utils.py +85 -0
  34. {antsibull_nox-0.1.0.dist-info → antsibull_nox-0.3.0.dist-info}/METADATA +4 -2
  35. antsibull_nox-0.3.0.dist-info/RECORD +40 -0
  36. antsibull_nox-0.3.0.dist-info/entry_points.txt +2 -0
  37. antsibull_nox/collection.py +0 -545
  38. antsibull_nox/sessions.py +0 -840
  39. antsibull_nox-0.1.0.dist-info/RECORD +0 -14
  40. {antsibull_nox-0.1.0.dist-info → antsibull_nox-0.3.0.dist-info}/WHEEL +0 -0
  41. {antsibull_nox-0.1.0.dist-info → antsibull_nox-0.3.0.dist-info}/licenses/LICENSES/GPL-3.0-or-later.txt +0 -0
antsibull_nox/__init__.py CHANGED
@@ -10,24 +10,27 @@ Antsibull Nox Helper.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- from .sessions import (
14
- ActionGroup,
15
- add_build_import_check,
16
- add_docs_check,
17
- add_extra_checks,
18
- add_license_check,
19
- add_lint_sessions,
13
+ from .config import (
14
+ CONFIG_FILENAME,
15
+ load_config_from_toml,
20
16
  )
17
+ from .interpret_config import interpret_config
18
+ from .sessions.ansible_test import add_ansible_test_session
19
+
20
+ __version__ = "0.3.0"
21
+
22
+
23
+ def load_antsibull_nox_toml() -> None:
24
+ """
25
+ Load and interpret antsibull-nox.toml config file.
26
+ """
27
+ config = load_config_from_toml(CONFIG_FILENAME)
28
+ interpret_config(config)
21
29
 
22
- __version__ = "0.1.0"
23
30
 
24
31
  # pylint:disable=duplicate-code
25
32
  __all__ = (
26
33
  "__version__",
27
- "ActionGroup",
28
- "add_build_import_check",
29
- "add_docs_check",
30
- "add_extra_checks",
31
- "add_license_check",
32
- "add_lint_sessions",
34
+ "add_ansible_test_session",
35
+ "load_antsibull_nox_toml",
33
36
  )
@@ -0,0 +1,98 @@
1
+ # Author: Felix Fontein <felix@fontein.de>
2
+ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
3
+ # https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ # SPDX-FileCopyrightText: Ansible Project, 2024
6
+
7
+ # ========================================================================
8
+ # **Vendored** from antsibull-core.
9
+ #
10
+ # I vendored this to avoid depending on antsibull-core only for this.
11
+ # TBH, most of this should be part of pydantic anyway. See
12
+ # https://github.com/pydantic/pydantic/discussions/2652 for a discussion
13
+ # on this.
14
+ #
15
+ # pylint: disable=missing-function-docstring
16
+ # ========================================================================
17
+
18
+ """
19
+ Helpers for pydantic.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import typing as t
25
+ from collections.abc import Callable, Collection
26
+
27
+ import pydantic as p
28
+
29
+ if t.TYPE_CHECKING:
30
+ from typing_extensions import TypeGuard
31
+
32
+
33
+ def _is_basemodel(a_type: t.Any) -> TypeGuard[type[p.BaseModel]]:
34
+ try:
35
+ return issubclass(a_type, p.BaseModel)
36
+ except TypeError:
37
+ # If inspect.isclass(a_type) is checked first, no TypeError happens for
38
+ # Python 3.11+.
39
+
40
+ # On Python 3.9 and 3.10, issubclass(dict[int, int], p.BaseModel) raises
41
+ # "TypeError: issubclass() arg 1 must be a class".
42
+ # (https://github.com/pydantic/pydantic/discussions/5970)
43
+ return False
44
+
45
+
46
+ def _modify_config(
47
+ cls: type[p.BaseModel],
48
+ processed_classes: set[type[p.BaseModel]],
49
+ change_config: Callable[[p.ConfigDict], bool],
50
+ ) -> bool:
51
+ if cls in processed_classes:
52
+ return False
53
+ change = False
54
+ for field_info in cls.model_fields.values():
55
+ if _is_basemodel(field_info.annotation):
56
+ change |= _modify_config(
57
+ field_info.annotation,
58
+ processed_classes,
59
+ change_config,
60
+ )
61
+ for subcls in t.get_args(field_info.annotation):
62
+ if _is_basemodel(subcls):
63
+ change |= _modify_config(subcls, processed_classes, change_config)
64
+ change |= change_config(cls.model_config)
65
+ if change:
66
+ cls.model_rebuild(force=True)
67
+ processed_classes.add(cls)
68
+ return change
69
+
70
+
71
+ def set_extras(
72
+ models: type[p.BaseModel] | Collection[type[p.BaseModel]],
73
+ value: t.Literal["allow", "ignore", "forbid"],
74
+ ) -> None:
75
+ def change_config(model_config: p.ConfigDict) -> bool:
76
+ if model_config.get("extra") == value:
77
+ return False
78
+ model_config["extra"] = value
79
+ return True
80
+
81
+ processed_classes: set[type[p.BaseModel]] = set()
82
+ if isinstance(models, Collection):
83
+ for cls in models:
84
+ _modify_config(cls, processed_classes, change_config)
85
+ else:
86
+ _modify_config(models, processed_classes, change_config)
87
+
88
+
89
+ def forbid_extras(models: type[p.BaseModel] | Collection[type[p.BaseModel]]) -> None:
90
+ set_extras(models, "forbid")
91
+
92
+
93
+ def get_formatted_error_messages(error: p.ValidationError) -> list[str]:
94
+ def format_error(err) -> str:
95
+ location = " -> ".join(str(loc) for loc in err["loc"])
96
+ return f'{location}: {err["msg"]}'
97
+
98
+ return [format_error(err) for err in error.errors()]
@@ -0,0 +1,260 @@
1
+ # Author: Felix Fontein <felix@fontein.de>
2
+ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
3
+ # https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ # SPDX-FileCopyrightText: 2025, Ansible Project
6
+
7
+ """
8
+ Ansible-core version utilities.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import functools
14
+ import typing as t
15
+ from dataclasses import dataclass
16
+
17
+ from antsibull_fileutils.yaml import load_yaml_file
18
+ from packaging.specifiers import SpecifierSet as PckgSpecifierSet
19
+ from packaging.version import Version as PckgVersion
20
+
21
+ from .utils import Version, version_range
22
+
23
+ AnsibleCoreVersion = t.Union[Version, t.Literal["milestone", "devel"]]
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class AnsibleCoreInfo:
28
+ """
29
+ Information on an ansible-core version.
30
+ """
31
+
32
+ ansible_core_version: Version
33
+ controller_python_versions: tuple[Version, ...]
34
+ remote_python_versions: tuple[Version, ...]
35
+
36
+
37
+ _SUPPORTED_CORE_VERSIONS: dict[Version, AnsibleCoreInfo] = {
38
+ Version.parse(ansible_version): AnsibleCoreInfo(
39
+ ansible_core_version=Version.parse(ansible_version),
40
+ controller_python_versions=tuple(
41
+ Version.parse(v) for v in controller_python_versions
42
+ ),
43
+ remote_python_versions=tuple(Version.parse(v) for v in remote_python_versions),
44
+ )
45
+ for ansible_version, (controller_python_versions, remote_python_versions) in {
46
+ "2.9": [
47
+ ["2.7", "3.5", "3.6", "3.7", "3.8"],
48
+ ["2.6", "2.7", "3.5", "3.6", "3.7", "3.8"],
49
+ ],
50
+ "2.10": [
51
+ ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"],
52
+ ["2.6", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9"],
53
+ ],
54
+ "2.11": [
55
+ ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9"],
56
+ ["2.6", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9"],
57
+ ],
58
+ "2.12": [
59
+ ["3.8", "3.9", "3.10"],
60
+ ["2.6", "2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"],
61
+ ],
62
+ "2.13": [
63
+ ["3.8", "3.9", "3.10"],
64
+ ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10"],
65
+ ],
66
+ "2.14": [
67
+ ["3.9", "3.10", "3.11"],
68
+ ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"],
69
+ ],
70
+ "2.15": [
71
+ ["3.9", "3.10", "3.11"],
72
+ ["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11"],
73
+ ],
74
+ "2.16": [
75
+ ["3.10", "3.11", "3.12"],
76
+ ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"],
77
+ ],
78
+ "2.17": [
79
+ ["3.10", "3.11", "3.12"],
80
+ ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"],
81
+ ],
82
+ "2.18": [
83
+ ["3.11", "3.12", "3.13"],
84
+ ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"],
85
+ ],
86
+ "2.19": [
87
+ ["3.11", "3.12", "3.13"],
88
+ ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"],
89
+ ],
90
+ # The following might need updates. Look for the "``ansible-core`` support matrix" table in:
91
+ # https://github.com/ansible/ansible-documentation/blob/devel/docs/docsite/rst/reference_appendices/release_and_maintenance.rst?plain=1
92
+ # It contains commented-out entries for future ansible-core versions.
93
+ "2.20": [
94
+ ["3.12", "3.13", "3.14"],
95
+ ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"],
96
+ ],
97
+ "2.21": [
98
+ ["3.12", "3.13", "3.14"],
99
+ ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"],
100
+ ],
101
+ "2.22": [
102
+ ["3.13", "3.14", "3.15"],
103
+ ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"],
104
+ ],
105
+ "2.23": [
106
+ ["3.13", "3.14", "3.15"],
107
+ ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"],
108
+ ],
109
+ "2.24": [
110
+ ["3.14", "3.15", "3.16"],
111
+ ["3.11", "3.12", "3.13", "3.14", "3.15", "3.16"],
112
+ ],
113
+ "2.25": [
114
+ ["3.14", "3.15", "3.16"],
115
+ ["3.11", "3.12", "3.13", "3.14", "3.15", "3.16"],
116
+ ],
117
+ }.items()
118
+ }
119
+
120
+ _MIN_SUPPORTED_VERSION = Version.parse("2.9")
121
+ _CURRENT_DEVEL_VERSION = Version.parse("2.19")
122
+ _CURRENT_MILESTONE_VERSION = Version.parse("2.19")
123
+
124
+
125
+ def get_ansible_core_info(
126
+ core_version: AnsibleCoreVersion,
127
+ ) -> AnsibleCoreInfo:
128
+ """
129
+ Retrieve information on an ansible-core version.
130
+ """
131
+ version: Version
132
+ if core_version == "devel":
133
+ version = _CURRENT_DEVEL_VERSION
134
+ elif core_version == "milestone":
135
+ version = _CURRENT_MILESTONE_VERSION
136
+ else:
137
+ version = core_version
138
+ if version in _SUPPORTED_CORE_VERSIONS:
139
+ return _SUPPORTED_CORE_VERSIONS[version]
140
+ raise ValueError(f"Unknown ansible-core version {version}")
141
+
142
+
143
+ _ANSIBLE_REPO = "ansible/ansible"
144
+ _ANSIBLE_EOL_REPO = "ansible-community/eol-ansible"
145
+ _ANSIBLE_EOL_MAX_VERSION = Version(2, 14)
146
+
147
+
148
+ def get_ansible_core_package_name(
149
+ core_version: AnsibleCoreVersion,
150
+ *,
151
+ source: t.Literal["git", "pypi"] = "git",
152
+ ansible_repo: str | None = None,
153
+ branch_name: str | None = None,
154
+ ) -> str:
155
+ """
156
+ Return the package name for a specific ansible-core version.
157
+
158
+ The result can be passed to pip to install that version of ansible-core.
159
+ """
160
+ if not isinstance(core_version, Version):
161
+ # milestone and devel are not available from PyPI
162
+ source = "git"
163
+
164
+ if source == "git":
165
+ if branch_name is None:
166
+ if isinstance(core_version, str):
167
+ branch_name = core_version
168
+ else:
169
+ branch_name = f"stable-{core_version}"
170
+ if ansible_repo is None:
171
+ if (
172
+ isinstance(core_version, Version)
173
+ and core_version <= _ANSIBLE_EOL_MAX_VERSION
174
+ ):
175
+ ansible_repo = _ANSIBLE_EOL_REPO
176
+ else:
177
+ ansible_repo = _ANSIBLE_REPO
178
+ return f"https://github.com/{ansible_repo}/archive/{branch_name}.tar.gz"
179
+
180
+ assert isinstance(core_version, Version)
181
+ next_core_version = core_version.next_minor_version()
182
+ base = "ansible-core"
183
+ if core_version == Version(2, 9):
184
+ base = "ansible"
185
+ elif core_version == Version(2, 10):
186
+ base = "ansible-base"
187
+ return f"{base}>={core_version},<{next_core_version}"
188
+
189
+
190
+ @functools.cache
191
+ def _read_requires_ansible() -> PckgSpecifierSet:
192
+ path = "meta/runtime.yml"
193
+ try:
194
+ runtime_data = load_yaml_file(path)
195
+ except FileNotFoundError as exc:
196
+ raise ValueError(f"Cannot open {path}") from exc
197
+ except Exception as exc:
198
+ raise ValueError(f"Cannot parse {path}: {exc}") from exc
199
+
200
+ requires_ansible = runtime_data.get("requires_ansible")
201
+ if not isinstance(requires_ansible, str):
202
+ raise ValueError(f"{path} does not contain an 'requires_ansible' string")
203
+ try:
204
+ return PckgSpecifierSet(requires_ansible)
205
+ except Exception as exc:
206
+ raise ValueError(
207
+ f"{path} contains an invalid 'requires_ansible' string: {exc}"
208
+ ) from exc
209
+
210
+
211
+ @functools.cache
212
+ def get_supported_core_versions(
213
+ *,
214
+ include_devel: bool = False,
215
+ include_milestone: bool = False,
216
+ min_version: Version | None = None,
217
+ max_version: Version | None = None,
218
+ except_versions: tuple[AnsibleCoreVersion, ...] | None = None,
219
+ ) -> list[AnsibleCoreVersion]:
220
+ """
221
+ Extracts a list of supported ansible-core versions from meta/runtime.yml.
222
+
223
+ If ``min_version`` is specified, no version below that version will be returned.
224
+ If ``max_version`` is specified, no version above that version will be returned.
225
+ If ``except_versions`` is specified, no version in that tuple will be returned.
226
+ """
227
+ if except_versions is None:
228
+ except_versions = ()
229
+
230
+ ra_specifier = _read_requires_ansible()
231
+
232
+ result: list[AnsibleCoreVersion] = []
233
+ for version in version_range(
234
+ _MIN_SUPPORTED_VERSION, _CURRENT_DEVEL_VERSION, inclusive=False
235
+ ):
236
+ if version in except_versions:
237
+ continue
238
+ if min_version is not None and version < min_version:
239
+ continue
240
+ if max_version is not None and version > max_version:
241
+ continue
242
+ # We're using x.y.999 to check whether *some* ansible-core x.y version is supported.
243
+ # This is not entirely correct, since collections might specfiy that only certain older x.y
244
+ # versions are OK, but I'd consider such behavior a bug of the collection and something
245
+ # you really shouldn't do as a collection maintainer.
246
+ v = PckgVersion(f"{version.major}.{version.minor}.999")
247
+ if v in ra_specifier:
248
+ result.append(version)
249
+ if include_milestone and "milestone" not in except_versions:
250
+ result.append("milestone")
251
+ if include_devel and "devel" not in except_versions:
252
+ result.append("devel")
253
+ return result
254
+
255
+
256
+ __all__ = [
257
+ "AnsibleCoreInfo",
258
+ "get_ansible_core_info",
259
+ "get_ansible_core_package_name",
260
+ ]
antsibull_nox/cli.py ADDED
@@ -0,0 +1,132 @@
1
+ # Author: Felix Fontein <felix@fontein.de>
2
+ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
3
+ # https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ # SPDX-FileCopyrightText: 2025, Ansible Project
6
+
7
+ # PYTHON_ARGCOMPLETE_OK
8
+
9
+ """Entrypoint to the antsibull-docs script."""
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import os
15
+ import os.path
16
+ import sys
17
+ from collections.abc import Callable
18
+
19
+ from . import __version__
20
+ from .init import create_initial_config as _create_initial_config
21
+ from .lint_config import lint_config as _lint_config
22
+
23
+ try:
24
+ import argcomplete
25
+
26
+ HAS_ARGCOMPLETE = True
27
+ except ImportError:
28
+ HAS_ARGCOMPLETE = False
29
+
30
+
31
+ def lint_config(_: argparse.Namespace) -> int:
32
+ """
33
+ Lint antsibull-nox config file.
34
+ """
35
+ errors = _lint_config()
36
+ for error in errors:
37
+ print(error)
38
+ return 0 if len(errors) == 0 else 3
39
+
40
+
41
+ def create_initial_config(_: argparse.Namespace) -> int:
42
+ """
43
+ Create noxfile.py and antsibull-nox.toml.
44
+ """
45
+ try:
46
+ _create_initial_config()
47
+ except Exception as exc: # pylint: disable=broad-exception-caught
48
+ print(f"Error: {exc}", file=sys.stderr)
49
+ return 3
50
+ return 0
51
+
52
+
53
+ # Mapping from command line subcommand names to functions which implement those.
54
+ # The functions need to take a single argument, the processed list of args.
55
+ ARGS_MAP: dict[str, Callable[[argparse.Namespace], int]] = {
56
+ "lint-config": lint_config,
57
+ "init": create_initial_config,
58
+ }
59
+
60
+
61
+ class InvalidArgumentError(Exception):
62
+ """
63
+ Error while parsing arguments.
64
+ """
65
+
66
+
67
+ def parse_args(program_name: str, args: list[str]) -> argparse.Namespace:
68
+ """
69
+ Parse the command line arguments.
70
+ """
71
+
72
+ toplevel_parser = argparse.ArgumentParser(
73
+ prog=program_name,
74
+ description="Script to manage generated documentation for ansible",
75
+ )
76
+ toplevel_parser.add_argument(
77
+ "--version",
78
+ action="version",
79
+ version=__version__,
80
+ help="Print the antsibull-nox version",
81
+ )
82
+ subparsers = toplevel_parser.add_subparsers(
83
+ title="Subcommands", dest="command", help="for help use: `SUBCOMMANDS -h`"
84
+ )
85
+ subparsers.required = True
86
+
87
+ subparsers.add_parser(
88
+ "lint-config",
89
+ description="Lint antsibull-nox configuration file",
90
+ )
91
+
92
+ subparsers.add_parser(
93
+ "init",
94
+ description="Create noxfile and antsibull-nox configuration file",
95
+ )
96
+
97
+ # This must come after all parser setup
98
+ if HAS_ARGCOMPLETE:
99
+ argcomplete.autocomplete(toplevel_parser)
100
+
101
+ parsed_args: argparse.Namespace = toplevel_parser.parse_args(args)
102
+ return parsed_args
103
+
104
+
105
+ def run(args: list[str]) -> int:
106
+ """
107
+ Run the program.
108
+ """
109
+ program_name = os.path.basename(args[0])
110
+ try:
111
+ parsed_args: argparse.Namespace = parse_args(program_name, args[1:])
112
+ except InvalidArgumentError as e:
113
+ print(e, file=sys.stderr)
114
+ return 2
115
+
116
+ return ARGS_MAP[parsed_args.command](parsed_args)
117
+
118
+
119
+ def main() -> int:
120
+ """
121
+ Entrypoint called from the script.
122
+
123
+ Return codes:
124
+ :0: Success
125
+ :1: Unhandled error. See the Traceback for more information.
126
+ :2: There was a problem with the command line arguments
127
+ """
128
+ return run(sys.argv)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ sys.exit(main())
@@ -0,0 +1,56 @@
1
+ # Author: Felix Fontein <felix@fontein.de>
2
+ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
3
+ # https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ # SPDX-FileCopyrightText: 2025, Ansible Project
6
+
7
+ """
8
+ Handle Ansible collections.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ from antsibull_fileutils.yaml import load_yaml_file, store_yaml_file
16
+
17
+ from .data import CollectionData, CollectionSource, SetupResult
18
+ from .install import (
19
+ Runner,
20
+ setup_collection_sources,
21
+ setup_collections,
22
+ setup_current_tree,
23
+ )
24
+ from .search import GALAXY_YML, CollectionList, load_collection_data_from_disk
25
+
26
+
27
+ def force_collection_version(path: Path, *, version: str) -> bool:
28
+ """
29
+ Make sure galaxy.yml contains this version.
30
+
31
+ Returns ``True`` if the version was changed, and ``False`` if the version
32
+ was already set to this value.
33
+ """
34
+ galaxy_yml = path / GALAXY_YML
35
+ try:
36
+ data = load_yaml_file(galaxy_yml)
37
+ except Exception as exc:
38
+ raise ValueError(f"Cannot parse {galaxy_yml}: {exc}") from exc
39
+ if data.get("version") == version:
40
+ return False
41
+ data["version"] = version
42
+ store_yaml_file(galaxy_yml, data)
43
+ return True
44
+
45
+
46
+ __all__ = [
47
+ "CollectionData",
48
+ "CollectionList",
49
+ "CollectionSource",
50
+ "SetupResult",
51
+ "Runner",
52
+ "load_collection_data_from_disk",
53
+ "setup_collections",
54
+ "setup_current_tree",
55
+ "setup_collection_sources",
56
+ ]
@@ -0,0 +1,106 @@
1
+ # Author: Felix Fontein <felix@fontein.de>
2
+ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or
3
+ # https://www.gnu.org/licenses/gpl-3.0.txt)
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ # SPDX-FileCopyrightText: 2025, Ansible Project
6
+
7
+ """
8
+ Data types for collections.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+
17
+ @dataclass
18
+ class CollectionData: # pylint: disable=too-many-instance-attributes
19
+ """
20
+ An Ansible collection.
21
+ """
22
+
23
+ collections_root_path: Path | None
24
+ path: Path
25
+ namespace: str
26
+ name: str
27
+ full_name: str
28
+ version: str | None
29
+ dependencies: dict[str, str]
30
+ current: bool
31
+
32
+ @classmethod
33
+ def create(
34
+ cls,
35
+ *,
36
+ collections_root_path: Path | None = None,
37
+ path: Path,
38
+ full_name: str,
39
+ version: str | None = None,
40
+ dependencies: dict[str, str] | None = None,
41
+ current: bool = False,
42
+ ):
43
+ """
44
+ Create a CollectionData object.
45
+ """
46
+ namespace, name = full_name.split(".", 1)
47
+ return CollectionData(
48
+ collections_root_path=collections_root_path,
49
+ path=path,
50
+ namespace=namespace,
51
+ name=name,
52
+ full_name=full_name,
53
+ version=version,
54
+ dependencies=dependencies or {},
55
+ current=current,
56
+ )
57
+
58
+
59
+ @dataclass
60
+ class SetupResult:
61
+ """
62
+ Information on how the collections are set up.
63
+ """
64
+
65
+ # The path of the ansible_collections directory.
66
+ root: Path
67
+
68
+ # Data on the current collection (as in the repository).
69
+ current_collection: CollectionData
70
+
71
+ # If it was installed, the path of the current collection inside the collection tree below root.
72
+ current_path: Path | None
73
+
74
+
75
+ @dataclass
76
+ class CollectionSource:
77
+ """
78
+ Collection installation source.
79
+ """
80
+
81
+ # The collection's name
82
+ name: str
83
+
84
+ # The collection's installation source (can be passed to 'ansible-galaxy collection install')
85
+ source: str
86
+
87
+ @staticmethod
88
+ def parse(collection_name: str, source: str | CollectionSource) -> CollectionSource:
89
+ """
90
+ Parse a CollectionSource object.
91
+ """
92
+ if isinstance(source, str):
93
+ return CollectionSource(name=collection_name, source=source)
94
+
95
+ if source.name != collection_name:
96
+ raise ValueError(
97
+ f"Collection name should be {collection_name!r}, but is {source.name!r}"
98
+ )
99
+ return source
100
+
101
+
102
+ __all__ = [
103
+ "CollectionData",
104
+ "SetupResult",
105
+ "CollectionSource",
106
+ ]