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.
- antsibull_nox/__init__.py +17 -14
- antsibull_nox/_pydantic.py +98 -0
- antsibull_nox/ansible.py +260 -0
- antsibull_nox/cli.py +132 -0
- antsibull_nox/collection/__init__.py +56 -0
- antsibull_nox/collection/data.py +106 -0
- antsibull_nox/collection/extract.py +23 -0
- antsibull_nox/collection/install.py +523 -0
- antsibull_nox/collection/search.py +460 -0
- antsibull_nox/config.py +378 -0
- antsibull_nox/data/action-groups.py +3 -3
- antsibull_nox/data/antsibull-nox-lint-config.py +29 -0
- antsibull_nox/data/antsibull_nox_data_util.py +91 -0
- antsibull_nox/data/license-check.py +6 -2
- antsibull_nox/data/no-unwanted-files.py +5 -1
- antsibull_nox/data/plugin-yamllint.py +247 -0
- antsibull_nox/data_util.py +0 -77
- antsibull_nox/init.py +83 -0
- antsibull_nox/interpret_config.py +244 -0
- antsibull_nox/lint_config.py +113 -0
- antsibull_nox/paths.py +19 -0
- antsibull_nox/python.py +81 -0
- antsibull_nox/sessions/__init__.py +70 -0
- antsibull_nox/sessions/ansible_lint.py +58 -0
- antsibull_nox/sessions/ansible_test.py +568 -0
- antsibull_nox/sessions/build_import_check.py +147 -0
- antsibull_nox/sessions/collections.py +137 -0
- antsibull_nox/sessions/docs_check.py +78 -0
- antsibull_nox/sessions/extra_checks.py +127 -0
- antsibull_nox/sessions/license_check.py +73 -0
- antsibull_nox/sessions/lint.py +627 -0
- antsibull_nox/sessions/utils.py +206 -0
- antsibull_nox/utils.py +85 -0
- {antsibull_nox-0.1.0.dist-info → antsibull_nox-0.3.0.dist-info}/METADATA +4 -2
- antsibull_nox-0.3.0.dist-info/RECORD +40 -0
- antsibull_nox-0.3.0.dist-info/entry_points.txt +2 -0
- antsibull_nox/collection.py +0 -545
- antsibull_nox/sessions.py +0 -840
- antsibull_nox-0.1.0.dist-info/RECORD +0 -14
- {antsibull_nox-0.1.0.dist-info → antsibull_nox-0.3.0.dist-info}/WHEEL +0 -0
- {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 .
|
14
|
-
|
15
|
-
|
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
|
-
"
|
28
|
-
"
|
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()]
|
antsibull_nox/ansible.py
ADDED
@@ -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
|
+
]
|