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
@@ -0,0 +1,378 @@
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
+ Config file schema.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import typing as t
15
+
16
+ import pydantic as p
17
+
18
+ from ._pydantic import forbid_extras, get_formatted_error_messages
19
+ from .ansible import AnsibleCoreVersion
20
+ from .utils import Version
21
+
22
+ try:
23
+ from tomllib import load as _load_toml
24
+ except ImportError:
25
+ from tomli import load as _load_toml # type: ignore
26
+
27
+
28
+ CONFIG_FILENAME = "antsibull-nox.toml"
29
+
30
+
31
+ def _parse_version(value: t.Any) -> Version:
32
+ if isinstance(value, Version):
33
+ return value
34
+ if isinstance(value, str) and "." in value:
35
+ return Version.parse(value)
36
+ raise ValueError("Must be version string")
37
+
38
+
39
+ def _parse_ansible_core_version(value: t.Any) -> AnsibleCoreVersion:
40
+ if isinstance(value, Version):
41
+ return value
42
+ if isinstance(value, str):
43
+ if value == "devel":
44
+ return "devel"
45
+ if value == "milestone":
46
+ return "milestone"
47
+ if "." in value:
48
+ return Version.parse(value)
49
+ raise ValueError("Must be ansible-core version string")
50
+
51
+
52
+ def _validate_collection_name(value: str) -> str:
53
+ parts = value.split(".")
54
+ if len(parts) != 2:
55
+ raise ValueError("Collection name must be of the form '<namespace>.<name>'")
56
+ if not parts[0].isidentifier():
57
+ raise ValueError("Collection namespace must be Python identifier")
58
+ if not parts[1].isidentifier():
59
+ raise ValueError("Collection name must be Python identifier")
60
+ return value
61
+
62
+
63
+ CollectionName = t.Annotated[str, p.AfterValidator(_validate_collection_name)]
64
+ PVersion = t.Annotated[Version, p.BeforeValidator(_parse_version)]
65
+ PAnsibleCoreVersion = t.Annotated[
66
+ AnsibleCoreVersion, p.BeforeValidator(_parse_ansible_core_version)
67
+ ]
68
+
69
+
70
+ class _BaseModel(p.BaseModel):
71
+ model_config = p.ConfigDict(frozen=True, extra="allow", validate_default=True)
72
+
73
+
74
+ class SessionLint(_BaseModel):
75
+ """
76
+ Lint session config.
77
+ """
78
+
79
+ default: bool = True
80
+ extra_code_files: list[str] = []
81
+
82
+ # isort:
83
+ run_isort: bool = True
84
+ isort_config: t.Optional[p.FilePath] = None
85
+ isort_package: str = "isort"
86
+
87
+ # black:
88
+ run_black: bool = True
89
+ run_black_modules: t.Optional[bool] = None
90
+ black_config: t.Optional[p.FilePath] = None
91
+ black_package: str = "black"
92
+
93
+ # flake8:
94
+ run_flake8: bool = True
95
+ flake8_config: t.Optional[p.FilePath] = None
96
+ flake8_package: str = "flake8"
97
+
98
+ # pylint:
99
+ run_pylint: bool = True
100
+ pylint_rcfile: t.Optional[p.FilePath] = None
101
+ pylint_modules_rcfile: t.Optional[p.FilePath] = None
102
+ pylint_package: str = "pylint"
103
+ pylint_ansible_core_package: t.Optional[str] = "ansible-core"
104
+ pylint_extra_deps: list[str] = []
105
+
106
+ # yamllint:
107
+ run_yamllint: bool = True
108
+ yamllint_config: t.Optional[p.FilePath] = None
109
+ yamllint_config_plugins: t.Optional[p.FilePath] = None
110
+ yamllint_config_plugins_examples: t.Optional[p.FilePath] = None
111
+ yamllint_package: str = "yamllint"
112
+
113
+ # mypy:
114
+ run_mypy: bool = True
115
+ mypy_config: t.Optional[p.FilePath] = None
116
+ mypy_package: str = "mypy"
117
+ mypy_ansible_core_package: t.Optional[str] = "ansible-core"
118
+ mypy_extra_deps: list[str] = []
119
+
120
+ # antsibull-nox config lint:
121
+ run_antsibullnox_config_lint: bool = True
122
+
123
+
124
+ class SessionDocsCheck(_BaseModel):
125
+ """
126
+ Docs check session config.
127
+ """
128
+
129
+ default: bool = True
130
+
131
+ antsibull_docs_package: str = "antsibull-docs"
132
+ ansible_core_package: str = "ansible-core"
133
+ validate_collection_refs: t.Optional[t.Literal["self", "dependent", "all"]] = None
134
+ extra_collections: list[CollectionName] = []
135
+
136
+
137
+ class SessionLicenseCheck(_BaseModel):
138
+ """
139
+ License check session config.
140
+ """
141
+
142
+ default: bool = True
143
+
144
+ run_reuse: bool = True
145
+ reuse_package: str = "reuse"
146
+ run_license_check: bool = True
147
+ license_check_extra_ignore_paths: list[str] = []
148
+
149
+
150
+ class ActionGroup(_BaseModel):
151
+ """
152
+ Information about an action group.
153
+ """
154
+
155
+ # Name of the action group.
156
+ name: str
157
+ # Regex pattern to match modules that could belong to this action group.
158
+ pattern: str
159
+ # Doc fragment that members of the action group must have, but no other module
160
+ # must have
161
+ doc_fragment: str
162
+ # Exclusion list of modules that match the regex, but should not be part of the
163
+ # action group. All other modules matching the regex are assumed to be part of
164
+ # the action group.
165
+ exclusions: list[str] = []
166
+
167
+
168
+ class SessionExtraChecks(_BaseModel):
169
+ """
170
+ Extra checks session config.
171
+ """
172
+
173
+ default: bool = True
174
+
175
+ # no-unwanted-files:
176
+ run_no_unwanted_files: bool = True
177
+ no_unwanted_files_module_extensions: list[str] = [".cs", ".ps1", ".psm1", ".py"]
178
+ no_unwanted_files_other_extensions: list[str] = [".py", ".pyi"]
179
+ no_unwanted_files_yaml_extensions: list[str] = [".yml", ".yaml"]
180
+ no_unwanted_files_skip_paths: list[str] = []
181
+ no_unwanted_files_skip_directories: t.Optional[list[str]] = []
182
+ no_unwanted_files_yaml_directories: t.Optional[list[str]] = [
183
+ "plugins/test/",
184
+ "plugins/filter/",
185
+ ]
186
+ no_unwanted_files_allow_symlinks: bool = False
187
+
188
+ # action-groups:
189
+ run_action_groups: bool = False
190
+ action_groups_config: list[ActionGroup] = []
191
+
192
+
193
+ class SessionBuildImportCheck(_BaseModel):
194
+ """
195
+ Collection build and Galaxy import session config.
196
+ """
197
+
198
+ default: bool = True
199
+
200
+ ansible_core_package: str = "ansible-core"
201
+ run_galaxy_importer: bool = True
202
+ galaxy_importer_package: str = "galaxy-importer"
203
+ # https://github.com/ansible/galaxy-importer#configuration
204
+ galaxy_importer_config_path: t.Optional[p.FilePath] = None
205
+ galaxy_importer_always_show_logs: bool = False
206
+
207
+
208
+ class DevelLikeBranch(_BaseModel):
209
+ """
210
+ A Git repository + branch for a devel-like branch of ansible-core.
211
+ """
212
+
213
+ repository: t.Optional[str] = None
214
+ branch: str
215
+
216
+ @p.model_validator(mode="before")
217
+ @classmethod
218
+ def _pre_validate(cls, values: t.Any) -> t.Any:
219
+ if isinstance(values, str):
220
+ return {"branch": values}
221
+ if (
222
+ isinstance(values, list)
223
+ and len(values) == 2
224
+ and all(isinstance(v, str) for v in values)
225
+ ):
226
+ return {"repository": values[0], "branch": values[1]}
227
+ return values
228
+
229
+
230
+ class SessionAnsibleTestSanity(_BaseModel):
231
+ """
232
+ Ansible-test sanity tests session config.
233
+ """
234
+
235
+ default: bool = False
236
+
237
+ include_devel: bool = False
238
+ include_milestone: bool = False
239
+ add_devel_like_branches: list[DevelLikeBranch] = []
240
+ min_version: t.Optional[PVersion] = None
241
+ max_version: t.Optional[PVersion] = None
242
+ except_versions: list[PAnsibleCoreVersion] = []
243
+ skip_tests: list[str] = []
244
+ allow_disabled: bool = False
245
+ enable_optional_errors: bool = False
246
+
247
+
248
+ class SessionAnsibleTestUnits(_BaseModel):
249
+ """
250
+ Ansible-test unit tests session config.
251
+ """
252
+
253
+ default: bool = False
254
+
255
+ include_devel: bool = False
256
+ include_milestone: bool = False
257
+ add_devel_like_branches: list[DevelLikeBranch] = []
258
+ min_version: t.Optional[PVersion] = None
259
+ max_version: t.Optional[PVersion] = None
260
+ except_versions: list[PAnsibleCoreVersion] = []
261
+
262
+
263
+ class SessionAnsibleTestIntegrationWDefaultContainer(_BaseModel):
264
+ """
265
+ Ansible-test integration tests with default container session config.
266
+ """
267
+
268
+ default: bool = False
269
+
270
+ include_devel: bool = False
271
+ include_milestone: bool = False
272
+ add_devel_like_branches: list[DevelLikeBranch] = []
273
+ min_version: t.Optional[PVersion] = None
274
+ max_version: t.Optional[PVersion] = None
275
+ except_versions: list[PAnsibleCoreVersion] = []
276
+ core_python_versions: dict[t.Union[PAnsibleCoreVersion, str], list[PVersion]] = {}
277
+ controller_python_versions_only: bool = False
278
+
279
+ @p.model_validator(mode="after")
280
+ def _validate_core_keys(self) -> t.Self:
281
+ branch_names = [dlb.branch for dlb in self.add_devel_like_branches]
282
+ for key in self.core_python_versions:
283
+ if isinstance(key, Version) or key in {"devel", "milestone"}:
284
+ continue
285
+ if key in branch_names:
286
+ continue
287
+ raise ValueError(
288
+ f"Unknown ansible-core version or branch name {key!r} in core_python_versions"
289
+ )
290
+ return self
291
+
292
+
293
+ class SessionAnsibleLint(_BaseModel):
294
+ """
295
+ Ansible-lint session config.
296
+ """
297
+
298
+ default: bool = True
299
+
300
+ ansible_lint_package: str = "ansible-lint"
301
+ strict: bool = False
302
+
303
+
304
+ class Sessions(_BaseModel):
305
+ """
306
+ Configuration of nox sessions to add.
307
+ """
308
+
309
+ lint: t.Optional[SessionLint] = None
310
+ docs_check: t.Optional[SessionDocsCheck] = None
311
+ license_check: t.Optional[SessionLicenseCheck] = None
312
+ extra_checks: t.Optional[SessionExtraChecks] = None
313
+ build_import_check: t.Optional[SessionBuildImportCheck] = None
314
+ ansible_test_sanity: t.Optional[SessionAnsibleTestSanity] = None
315
+ ansible_test_units: t.Optional[SessionAnsibleTestUnits] = None
316
+ ansible_test_integration_w_default_container: t.Optional[
317
+ SessionAnsibleTestIntegrationWDefaultContainer
318
+ ] = None
319
+ ansible_lint: t.Optional[SessionAnsibleLint] = None
320
+
321
+
322
+ class CollectionSource(_BaseModel):
323
+ """
324
+ Source from which to install a collection.
325
+ """
326
+
327
+ source: str
328
+
329
+ @p.model_validator(mode="before")
330
+ @classmethod
331
+ def _pre_validate(cls, values):
332
+ if isinstance(values, str):
333
+ return {"source": values}
334
+ return values
335
+
336
+
337
+ class Config(_BaseModel):
338
+ """
339
+ The contents of a antsibull-nox config file.
340
+ """
341
+
342
+ collection_sources: dict[CollectionName, CollectionSource] = {}
343
+ sessions: Sessions = Sessions()
344
+
345
+
346
+ def load_config_from_toml(path: str | os.PathLike) -> Config:
347
+ """
348
+ Load a config TOML file.
349
+ """
350
+ with open(path, "rb") as f:
351
+ try:
352
+ data = _load_toml(f)
353
+ except ValueError as exc:
354
+ raise ValueError(f"Error while reading {path}: {exc}") from exc
355
+ return Config.model_validate(data)
356
+
357
+
358
+ def lint_config_toml() -> list[str]:
359
+ """
360
+ Lint config files
361
+ """
362
+ path = CONFIG_FILENAME
363
+ errors = []
364
+ forbid_extras(Config)
365
+ try:
366
+ with open(path, "rb") as f:
367
+ data = _load_toml(f)
368
+ Config.model_validate(data)
369
+ except p.ValidationError as exc:
370
+ for error in get_formatted_error_messages(exc):
371
+ errors.append(f"{path}:{error}")
372
+ except ValueError as exc:
373
+ errors.append(f"{path}:{exc}")
374
+ except FileNotFoundError:
375
+ errors.append(f"{path}: File does not exist")
376
+ except IOError as exc:
377
+ errors.append(f"{path}:{exc}")
378
+ return errors
@@ -16,8 +16,8 @@ import typing as t
16
16
 
17
17
  import yaml
18
18
 
19
- from antsibull_nox.data_util import setup
20
- from antsibull_nox.sessions import ActionGroup
19
+ from antsibull_nox.data.antsibull_nox_data_util import setup
20
+ from antsibull_nox.sessions.extra_checks import ActionGroup
21
21
 
22
22
 
23
23
  def compile_patterns(
@@ -41,7 +41,7 @@ def load_redirects(
41
41
  try:
42
42
  with open(meta_runtime, "rb") as f:
43
43
  data = yaml.safe_load(f)
44
- action_groups = data["action_groups"]
44
+ action_groups = data.get("action_groups", {})
45
45
  except Exception as exc:
46
46
  errors.append(f"{meta_runtime}: cannot load action groups: {exc}")
47
47
  return {}
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env python
2
+
3
+ # Copyright (c) 2025, Felix Fontein <felix@fontein.de>
4
+ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt
5
+ # or https://www.gnu.org/licenses/gpl-3.0.txt)
6
+ # SPDX-License-Identifier: GPL-3.0-or-later
7
+
8
+ """Run antsibull-nox lint-config."""
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+
14
+ from antsibull_nox.data.antsibull_nox_data_util import setup
15
+ from antsibull_nox.lint_config import lint_config
16
+
17
+
18
+ def main() -> int:
19
+ """Main entry point."""
20
+ _, __ = setup()
21
+
22
+ errors = lint_config()
23
+ for error in errors:
24
+ print(error)
25
+ return len(errors) > 0
26
+
27
+
28
+ if __name__ == "__main__":
29
+ sys.exit(main())
@@ -0,0 +1,91 @@
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
+ Utility code for scripts in data.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sys
15
+ import typing as t
16
+
17
+
18
+ def setup() -> tuple[list[str], dict[str, t.Any]]:
19
+ """
20
+ Fetch list of paths and potential extra configuration.
21
+
22
+ First thing to call in an extra sanity check script in data/.
23
+ """
24
+ if len(sys.argv) == 3 and sys.argv[1] == "--data":
25
+ # Preferred way: load information from JSON file
26
+ path = sys.argv[2]
27
+ try:
28
+ with open(path, "rb") as f:
29
+ data = json.load(f)
30
+ except Exception as exc:
31
+ raise ValueError(f"Error while reading JSON from {path}") from exc
32
+ try:
33
+ paths = get_list_of_strings(data, "paths")
34
+ except ValueError as exc:
35
+ raise ValueError(f"Invalid JSON content in {path}: {exc}") from exc
36
+ data.pop("paths")
37
+ return paths, data
38
+ if len(sys.argv) >= 2:
39
+ # It's also possible to pass a list of paths on the command line, to simplify
40
+ # testing these scripts.
41
+ return sys.argv[1:], {}
42
+ # Alternatively one can pass a list of files from stdin, for example by piping
43
+ # the output of 'git ls-files' into this script. This is also for testing these
44
+ # scripts.
45
+ return sys.stdin.read().splitlines(), {}
46
+
47
+
48
+ def get_list_of_strings(
49
+ data: dict[str, t.Any],
50
+ key: str,
51
+ *,
52
+ default: list[str] | None = None,
53
+ ) -> list[str]:
54
+ """
55
+ Retrieves a list of strings from key ``key`` of the JSON object ``data``.
56
+
57
+ If ``default`` is set to a list, a missing key results in this value being returned.
58
+ """
59
+ sentinel = object()
60
+ value = data.get(key, sentinel)
61
+ if value is sentinel:
62
+ if default is not None:
63
+ return default
64
+ raise ValueError(f"{key!r} is not a present")
65
+ if not isinstance(value, list):
66
+ raise ValueError(f"{key!r} is not a list, but {type(key)}")
67
+ if not all(isinstance(entry, str) for entry in value):
68
+ raise ValueError(f"{key!r} is not a list of strings")
69
+ return t.cast(list[str], value)
70
+
71
+
72
+ def get_bool(
73
+ data: dict[str, t.Any],
74
+ key: str,
75
+ *,
76
+ default: bool | None = None,
77
+ ) -> bool:
78
+ """
79
+ Retrieves a boolean from key ``key`` of the JSON object ``data``.
80
+
81
+ If ``default`` is set to a boolean, a missing key results in this value being returned.
82
+ """
83
+ sentinel = object()
84
+ value = data.get(key, sentinel)
85
+ if value is sentinel:
86
+ if default is not None:
87
+ return default
88
+ raise ValueError(f"{key!r} is not a present")
89
+ if not isinstance(value, bool):
90
+ raise ValueError(f"{key!r} is not a bool, but {type(key)}")
91
+ return value
@@ -11,9 +11,10 @@ from __future__ import annotations
11
11
 
12
12
  import glob
13
13
  import os
14
+ import stat
14
15
  import sys
15
16
 
16
- from antsibull_nox.data_util import get_list_of_strings, setup
17
+ from antsibull_nox.data.antsibull_nox_data_util import get_list_of_strings, setup
17
18
 
18
19
 
19
20
  def format_license_list(licenses: list[str]) -> str:
@@ -108,7 +109,10 @@ def main() -> int:
108
109
  path = path[2:]
109
110
  if path in ignore_paths or path.startswith("tests/output/"):
110
111
  continue
111
- if os.stat(path).st_size == 0:
112
+ sr = os.stat(path)
113
+ if not stat.S_ISREG(sr.st_mode):
114
+ continue
115
+ if sr.st_size == 0:
112
116
  continue
113
117
  if not path.endswith(".license") and os.path.exists(path + ".license"):
114
118
  path = path + ".license"
@@ -12,7 +12,11 @@ from __future__ import annotations
12
12
  import os
13
13
  import sys
14
14
 
15
- from antsibull_nox.data_util import get_bool, get_list_of_strings, setup
15
+ from antsibull_nox.data.antsibull_nox_data_util import (
16
+ get_bool,
17
+ get_list_of_strings,
18
+ setup,
19
+ )
16
20
 
17
21
 
18
22
  def main() -> int: