ansible-core 2.19.0b3__py3-none-any.whl → 2.19.0b5__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 (174) hide show
  1. ansible/_internal/__init__.py +2 -2
  2. ansible/_internal/_collection_proxy.py +1 -1
  3. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  4. ansible/_internal/_errors/_captured.py +25 -30
  5. ansible/_internal/_errors/_error_factory.py +89 -0
  6. ansible/_internal/_errors/_error_utils.py +240 -0
  7. ansible/_internal/_errors/_task_timeout.py +28 -0
  8. ansible/_internal/_event_formatting.py +127 -0
  9. ansible/_internal/_json/__init__.py +6 -6
  10. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  11. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  12. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  13. ansible/_internal/_ssh/__init__.py +0 -0
  14. ansible/_internal/_ssh/_agent_launch.py +91 -0
  15. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  16. ansible/_internal/_templating/__init__.py +5 -3
  17. ansible/_internal/_templating/_datatag.py +2 -1
  18. ansible/_internal/_templating/_engine.py +3 -4
  19. ansible/_internal/_templating/_jinja_bits.py +21 -16
  20. ansible/_internal/_templating/_jinja_common.py +18 -27
  21. ansible/_internal/_templating/_jinja_plugins.py +31 -3
  22. ansible/_internal/_templating/_lazy_containers.py +5 -5
  23. ansible/_internal/_templating/_transform.py +20 -19
  24. ansible/_internal/_templating/_utils.py +1 -1
  25. ansible/_internal/_testing.py +26 -0
  26. ansible/_internal/_yaml/_dumper.py +1 -1
  27. ansible/_internal/_yaml/_errors.py +7 -7
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  29. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  30. ansible/cli/__init__.py +5 -82
  31. ansible/cli/arguments/option_helpers.py +8 -5
  32. ansible/cli/doc.py +84 -28
  33. ansible/cli/inventory.py +1 -1
  34. ansible/compat/importlib_resources.py +9 -12
  35. ansible/config/base.yml +27 -23
  36. ansible/config/manager.py +142 -101
  37. ansible/constants.py +1 -1
  38. ansible/errors/__init__.py +96 -49
  39. ansible/executor/module_common.py +8 -10
  40. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  41. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  42. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  43. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  44. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  45. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  46. ansible/executor/powershell/module_manifest.py +52 -0
  47. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  48. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  49. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  50. ansible/executor/process/worker.py +38 -113
  51. ansible/executor/task_executor.py +26 -61
  52. ansible/executor/task_result.py +2 -4
  53. ansible/galaxy/collection/__init__.py +1 -4
  54. ansible/inventory/manager.py +1 -0
  55. ansible/module_utils/_internal/__init__.py +0 -3
  56. ansible/module_utils/_internal/_ambient_context.py +3 -3
  57. ansible/module_utils/_internal/_ansiballz.py +4 -2
  58. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  59. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  60. ansible/module_utils/_internal/_deprecator.py +66 -48
  61. ansible/module_utils/_internal/_errors.py +88 -17
  62. ansible/module_utils/_internal/_event_utils.py +61 -0
  63. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  64. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  65. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  66. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  67. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  68. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  69. ansible/module_utils/_internal/_plugin_info.py +1 -1
  70. ansible/module_utils/_internal/_stack.py +22 -0
  71. ansible/module_utils/_internal/_text_utils.py +6 -0
  72. ansible/module_utils/_internal/_traceback.py +11 -8
  73. ansible/module_utils/ansible_release.py +1 -1
  74. ansible/module_utils/basic.py +49 -15
  75. ansible/module_utils/common/arg_spec.py +2 -2
  76. ansible/module_utils/common/collections.py +6 -0
  77. ansible/module_utils/common/json.py +2 -2
  78. ansible/module_utils/common/text/converters.py +3 -3
  79. ansible/module_utils/common/validation.py +1 -1
  80. ansible/module_utils/common/warnings.py +80 -23
  81. ansible/module_utils/common/yaml.py +1 -1
  82. ansible/module_utils/datatag.py +5 -2
  83. ansible/module_utils/facts/system/distribution.py +16 -3
  84. ansible/module_utils/facts/virtual/linux.py +2 -2
  85. ansible/module_utils/parsing/convert_bool.py +6 -0
  86. ansible/module_utils/service.py +2 -9
  87. ansible/modules/apt_repository.py +7 -29
  88. ansible/modules/assemble.py +4 -4
  89. ansible/modules/async_status.py +13 -11
  90. ansible/modules/async_wrapper.py +5 -5
  91. ansible/modules/cron.py +3 -5
  92. ansible/modules/dnf5.py +15 -22
  93. ansible/modules/git.py +1 -6
  94. ansible/modules/hostname.py +0 -1
  95. ansible/modules/pip.py +2 -4
  96. ansible/modules/service.py +3 -9
  97. ansible/modules/sysvinit.py +3 -3
  98. ansible/parsing/ajson.py +3 -5
  99. ansible/parsing/dataloader.py +4 -4
  100. ansible/parsing/mod_args.py +1 -1
  101. ansible/parsing/plugin_docs.py +2 -2
  102. ansible/parsing/utils/yaml.py +3 -3
  103. ansible/parsing/vault/__init__.py +4 -4
  104. ansible/playbook/playbook_include.py +1 -1
  105. ansible/playbook/taggable.py +0 -3
  106. ansible/plugins/__init__.py +0 -25
  107. ansible/plugins/action/__init__.py +9 -32
  108. ansible/plugins/action/add_host.py +1 -1
  109. ansible/plugins/action/assemble.py +8 -16
  110. ansible/plugins/action/async_status.py +7 -2
  111. ansible/plugins/action/copy.py +8 -7
  112. ansible/plugins/action/gather_facts.py +8 -8
  113. ansible/plugins/action/package.py +5 -8
  114. ansible/plugins/action/script.py +8 -15
  115. ansible/plugins/action/service.py +3 -7
  116. ansible/plugins/action/template.py +6 -8
  117. ansible/plugins/action/unarchive.py +5 -15
  118. ansible/plugins/action/uri.py +9 -20
  119. ansible/plugins/callback/__init__.py +4 -6
  120. ansible/plugins/callback/junit.py +4 -2
  121. ansible/plugins/connection/local.py +2 -2
  122. ansible/plugins/connection/ssh.py +17 -9
  123. ansible/plugins/connection/winrm.py +5 -2
  124. ansible/plugins/doc_fragments/constructed.py +2 -2
  125. ansible/plugins/filter/core.py +13 -6
  126. ansible/plugins/filter/encryption.py +4 -4
  127. ansible/plugins/inventory/__init__.py +11 -10
  128. ansible/plugins/inventory/script.py +1 -1
  129. ansible/plugins/list.py +69 -16
  130. ansible/plugins/loader.py +10 -9
  131. ansible/plugins/lookup/csvfile.py +16 -71
  132. ansible/plugins/lookup/first_found.py +2 -1
  133. ansible/plugins/shell/__init__.py +56 -2
  134. ansible/plugins/shell/powershell.py +66 -9
  135. ansible/plugins/shell/sh.py +9 -5
  136. ansible/plugins/test/core.py +21 -15
  137. ansible/plugins/test/finished.yml +1 -1
  138. ansible/plugins/test/uri.py +2 -5
  139. ansible/release.py +1 -1
  140. ansible/template/__init__.py +30 -2
  141. ansible/utils/collection_loader/__init__.py +2 -0
  142. ansible/utils/display.py +107 -128
  143. ansible/utils/hashing.py +0 -1
  144. ansible/utils/listify.py +6 -4
  145. ansible/utils/plugin_docs.py +2 -1
  146. ansible/utils/unsafe_proxy.py +1 -1
  147. ansible/vars/hostvars.py +1 -1
  148. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
  149. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
  150. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  151. ansible_test/_data/completion/docker.txt +3 -3
  152. ansible_test/_data/completion/remote.txt +1 -0
  153. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  154. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  155. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  156. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  157. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  158. ansible_test/_internal/util.py +20 -0
  159. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  160. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  161. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  162. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  163. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  164. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
  165. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  166. ansible/_internal/_errors/_utils.py +0 -310
  167. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  168. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
  169. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
  170. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
  171. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
  172. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
  173. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
  174. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,6 +1,6 @@
1
- base image=quay.io/ansible/base-test-container:8.1.0 python=3.13,3.8,3.9,3.10,3.11,3.12
2
- default image=quay.io/ansible/default-test-container:11.5.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=collection
3
- default image=quay.io/ansible/ansible-core-test-container:11.5.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core
1
+ base image=quay.io/ansible/base-test-container:8.2.0 python=3.13,3.8,3.9,3.10,3.11,3.12
2
+ default image=quay.io/ansible/default-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=collection
3
+ default image=quay.io/ansible/ansible-core-test-container:11.6.0 python=3.13,3.8,3.9,3.10,3.11,3.12 context=ansible-core
4
4
  alpine321 image=quay.io/ansible/alpine321-test-container:9.1.0 python=3.12 cgroup=none audit=none
5
5
  fedora41 image=quay.io/ansible/fedora41-test-container:9.0.0 python=3.13 cgroup=v2-only
6
6
  ubuntu2204 image=quay.io/ansible/ubuntu2204-test-container:9.0.0 python=3.10
@@ -8,6 +8,7 @@ freebsd python_dir=/usr/local/bin become=su_sudo provider=aws arch=x86_64
8
8
  macos/15.3 python=3.13 python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
9
9
  macos python_dir=/usr/local/bin become=sudo provider=parallels arch=x86_64
10
10
  rhel/9.5 python=3.9,3.12 become=sudo provider=aws arch=x86_64
11
+ rhel/10.0 python=3.12 become=sudo provider=aws arch=x86_64
11
12
  rhel become=sudo provider=aws arch=x86_64
12
13
  ubuntu/22.04 python=3.10 become=sudo provider=aws arch=x86_64
13
14
  ubuntu/24.04 python=3.12 become=sudo provider=aws arch=x86_64
@@ -1,5 +1,5 @@
1
1
  # edit "sanity.ansible-doc.in" and generate with: hacking/update-sanity-requirements.py --test ansible-doc
2
2
  Jinja2==3.1.6
3
3
  MarkupSafe==3.0.2
4
- packaging==24.2
4
+ packaging==25.0
5
5
  PyYAML==6.0.2
@@ -1,9 +1,9 @@
1
1
  # edit "sanity.changelog.in" and generate with: hacking/update-sanity-requirements.py --test changelog
2
2
  antsibull-changelog==0.29.0
3
3
  docutils==0.18.1
4
- packaging==24.2
4
+ packaging==25.0
5
5
  PyYAML==6.0.2
6
6
  rstcheck==5.0.0
7
7
  semantic-version==2.10.0
8
8
  types-docutils==0.18.3
9
- typing_extensions==4.12.2
9
+ typing_extensions==4.13.2
@@ -1,2 +1,2 @@
1
1
  # edit "sanity.pep8.in" and generate with: hacking/update-sanity-requirements.py --test pep8
2
- pycodestyle==2.12.1
2
+ pycodestyle==2.13.0
@@ -1,9 +1,9 @@
1
1
  # edit "sanity.pylint.in" and generate with: hacking/update-sanity-requirements.py --test pylint
2
- astroid==3.3.9
3
- dill==0.3.9
2
+ astroid==3.3.10
3
+ dill==0.4.0
4
4
  isort==6.0.1
5
5
  mccabe==0.7.0
6
- platformdirs==4.3.7
7
- pylint==3.3.6
6
+ platformdirs==4.3.8
7
+ pylint==3.3.7
8
8
  PyYAML==6.0.2
9
9
  tomlkit==0.13.2
@@ -1,4 +1,4 @@
1
1
  # edit "sanity.yamllint.in" and generate with: hacking/update-sanity-requirements.py --test yamllint
2
2
  pathspec==0.12.1
3
3
  PyYAML==6.0.2
4
- yamllint==1.36.2
4
+ yamllint==1.37.1
@@ -254,12 +254,29 @@ def filter_args(args: list[str], filters: dict[str, int]) -> list[str]:
254
254
  """Return a filtered version of the given command line arguments."""
255
255
  remaining = 0
256
256
  result = []
257
+ pass_through_args: list[str] = []
258
+ pass_through_explicit = False
259
+ pass_through_implicit = False
257
260
 
258
261
  for arg in args:
262
+ if pass_through_explicit:
263
+ pass_through_args.append(arg)
264
+ continue
265
+
266
+ if arg == '--':
267
+ pass_through_explicit = True
268
+ continue
269
+
259
270
  if not arg.startswith('-') and remaining:
260
271
  remaining -= 1
272
+ pass_through_implicit = not remaining
261
273
  continue
262
274
 
275
+ if not arg.startswith('-') and pass_through_implicit:
276
+ pass_through_args.append(arg)
277
+ continue
278
+
279
+ pass_through_implicit = False
263
280
  remaining = 0
264
281
 
265
282
  parts = arg.split('=', 1)
@@ -271,6 +288,9 @@ def filter_args(args: list[str], filters: dict[str, int]) -> list[str]:
271
288
 
272
289
  result.append(arg)
273
290
 
291
+ if pass_through_args:
292
+ result += ['--'] + pass_through_args
293
+
274
294
  return result
275
295
 
276
296
 
@@ -6,6 +6,7 @@ load-plugins=
6
6
  pylint.extensions.docstyle,
7
7
 
8
8
  disable=
9
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
9
10
  docstring-first-line-empty,
10
11
  consider-using-f-string, # Python 2.x support still required
11
12
  cyclic-import, # consistent results require running with --jobs 1 and testing all files
@@ -6,6 +6,7 @@ load-plugins=
6
6
  pylint.extensions.docstyle,
7
7
 
8
8
  disable=
9
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
9
10
  docstring-first-line-empty,
10
11
  consider-using-f-string, # many occurrences
11
12
  cyclic-import, # consistent results require running with --jobs 1 and testing all files
@@ -6,6 +6,7 @@ load-plugins=
6
6
  pylint.extensions.docstyle,
7
7
 
8
8
  disable=
9
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
9
10
  docstring-first-line-empty,
10
11
  consider-using-f-string, # many occurrences
11
12
  cyclic-import, # consistent results require running with --jobs 1 and testing all files
@@ -11,6 +11,7 @@ disable=
11
11
  attribute-defined-outside-init,
12
12
  bad-indentation,
13
13
  bad-mcs-classmethod-argument,
14
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
14
15
  broad-exception-caught,
15
16
  broad-exception-raised,
16
17
  c-extension-no-member,
@@ -16,6 +16,7 @@ disable=
16
16
  attribute-defined-outside-init,
17
17
  bad-indentation,
18
18
  bad-mcs-classmethod-argument,
19
+ bad-super-call, # flakey test, can report false positives due to inference issue when using deprecate_calls plugin
19
20
  broad-exception-caught,
20
21
  broad-exception-raised,
21
22
  c-extension-no-member,
@@ -39,6 +39,10 @@ class DeprecationCallArgs:
39
39
  removed: object = None # only on Display.deprecated
40
40
  value: object = None # only on deprecate_value
41
41
 
42
+ def all_args_dynamic(self) -> bool:
43
+ """True if all args are dynamic or None, otherwise False."""
44
+ return all(arg is None or isinstance(arg, astroid.NodeNG) for arg in dataclasses.asdict(self).values())
45
+
42
46
 
43
47
  class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
44
48
  """Checks for deprecated calls to ensure proper usage."""
@@ -172,7 +176,6 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
172
176
  def __init__(self, *args, **kwargs) -> None:
173
177
  super().__init__(*args, **kwargs)
174
178
 
175
- self.inference_context = astroid.context.InferenceContext()
176
179
  self.module_cache: dict[str, astroid.Module] = {}
177
180
 
178
181
  @functools.cached_property
@@ -237,7 +240,7 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
237
240
  inferred: astroid.typing.InferenceResult | None = None
238
241
 
239
242
  while target:
240
- if inferred := astroid.util.safe_infer(target, self.inference_context):
243
+ if inferred := astroid.util.safe_infer(target):
241
244
  break
242
245
 
243
246
  if isinstance(target, astroid.Call):
@@ -259,6 +262,20 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
259
262
  break
260
263
 
261
264
  for name in reversed(names):
265
+ if isinstance(inferred, astroid.Instance):
266
+ try:
267
+ attr = next(iter(inferred.getattr(name)), None)
268
+ except astroid.AttributeInferenceError:
269
+ break
270
+
271
+ if isinstance(attr, astroid.AssignAttr):
272
+ inferred = self.get_ansible_module(attr)
273
+ continue
274
+
275
+ if isinstance(attr, astroid.FunctionDef):
276
+ inferred = attr
277
+ continue
278
+
262
279
  if not isinstance(inferred, (astroid.Module, astroid.ClassDef)):
263
280
  inferred = None
264
281
  break
@@ -278,25 +295,46 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
278
295
  def infer_name(self, node: astroid.Name) -> astroid.NodeNG | None:
279
296
  """Infer the node referenced by the given name, or `None` if it cannot be unambiguously inferred."""
280
297
  scope = node.scope()
281
- name = None
298
+ inferred: astroid.NodeNG | None = None
299
+ name = node.name
282
300
 
283
301
  while scope:
284
302
  try:
285
- assignment = scope[node.name]
303
+ assignment = scope[name]
286
304
  except KeyError:
287
305
  scope = scope.parent.scope() if scope.parent else None
288
306
  continue
289
307
 
290
308
  if isinstance(assignment, astroid.AssignName) and isinstance(assignment.parent, astroid.Assign):
291
- name = assignment.parent.value
309
+ inferred = assignment.parent.value
310
+ elif (
311
+ isinstance(scope, astroid.FunctionDef)
312
+ and isinstance(assignment, astroid.AssignName)
313
+ and isinstance(assignment.parent, astroid.Arguments)
314
+ and assignment.parent.annotations
315
+ ):
316
+ idx, _node = assignment.parent.find_argname(name)
317
+
318
+ if idx is not None:
319
+ try:
320
+ annotation = assignment.parent.annotations[idx]
321
+ except IndexError:
322
+ pass
323
+ else:
324
+ if isinstance(annotation, astroid.Name):
325
+ name = annotation.name
326
+ continue
327
+ elif isinstance(assignment, astroid.ClassDef):
328
+ inferred = assignment
292
329
  elif isinstance(assignment, astroid.ImportFrom):
293
330
  if module := self.get_module(assignment):
331
+ name = assignment.real_name(name)
294
332
  scope = module.scope()
295
333
  continue
296
334
 
297
335
  break
298
336
 
299
- return name
337
+ return inferred
300
338
 
301
339
  def get_module(self, node: astroid.ImportFrom) -> astroid.Module | None:
302
340
  """Import the requested module if possible and cache the result."""
@@ -346,6 +384,10 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
346
384
  self.add_message('ansible-deprecated-date-not-permitted', node=node, args=(name,))
347
385
  return
348
386
 
387
+ if call_args.all_args_dynamic():
388
+ # assume collection maintainers know what they're doing if all args are dynamic
389
+ return
390
+
349
391
  if call_args.version and call_args.date:
350
392
  self.add_message('ansible-deprecated-both-version-and-date', node=node, args=(name,))
351
393
  return
@@ -407,10 +449,13 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
407
449
  self.add_message('ansible-deprecated-unnecessary-collection-name', node=node, args=('deprecator', name,))
408
450
  return
409
451
 
452
+ if args.all_args_dynamic():
453
+ # assume collection maintainers know what they're doing if all args are dynamic
454
+ return
455
+
410
456
  expected_collection_name = 'ansible.builtin' if self.is_ansible_core else self.collection_name
411
457
 
412
458
  if args.collection_name and args.collection_name != expected_collection_name:
413
- # if collection_name is provided and a constant, report when it does not match the expected name
414
459
  self.add_message('wrong-collection-deprecated', node=node, args=(args.collection_name, name))
415
460
 
416
461
  def check_version(self, node: astroid.Call, name: str, args: DeprecationCallArgs) -> None:
@@ -469,7 +514,27 @@ class AnsibleDeprecatedChecker(pylint.checkers.BaseChecker):
469
514
 
470
515
  raise TypeError(type(value))
471
516
 
517
+ def get_ansible_module(self, node: astroid.AssignAttr) -> astroid.Instance | None:
518
+ """Infer an AnsibleModule instance node from the given assignment."""
519
+ if isinstance(node.parent, astroid.Assign) and isinstance(node.parent.type_annotation, astroid.Name):
520
+ inferred = self.infer_name(node.parent.type_annotation)
521
+ elif isinstance(node.parent, astroid.Assign) and isinstance(node.parent.parent, astroid.FunctionDef) and isinstance(node.parent.value, astroid.Name):
522
+ inferred = self.infer_name(node.parent.value)
523
+ elif isinstance(node.parent, astroid.AnnAssign) and isinstance(node.parent.annotation, astroid.Name):
524
+ inferred = self.infer_name(node.parent.annotation)
525
+ else:
526
+ inferred = None
527
+
528
+ if isinstance(inferred, astroid.ClassDef) and inferred.name == 'AnsibleModule':
529
+ return inferred.instantiate_class()
530
+
531
+ return None
532
+
533
+ def register(self) -> None:
534
+ """Register this plugin."""
535
+ self.linter.register_checker(self)
536
+
472
537
 
473
538
  def register(linter: pylint.lint.PyLinter) -> None:
474
539
  """Required method to auto-register this checker."""
475
- linter.register_checker(AnsibleDeprecatedChecker(linter))
540
+ AnsibleDeprecatedChecker(linter).register()
@@ -281,10 +281,41 @@ bootstrap_remote_rhel_9()
281
281
  done
282
282
  }
283
283
 
284
+ bootstrap_remote_rhel_10()
285
+ {
286
+ py_pkg_prefix="python3"
287
+
288
+ packages="
289
+ gcc
290
+ ${py_pkg_prefix}-devel
291
+ ${py_pkg_prefix}-pip
292
+ "
293
+
294
+ if [ "${controller}" ]; then
295
+ packages="
296
+ ${packages}
297
+ ${py_pkg_prefix}-cryptography
298
+ ${py_pkg_prefix}-jinja2
299
+ ${py_pkg_prefix}-packaging
300
+ ${py_pkg_prefix}-pyyaml
301
+ ${py_pkg_prefix}-resolvelib
302
+ "
303
+ fi
304
+
305
+ while true; do
306
+ # shellcheck disable=SC2086
307
+ dnf install -q -y ${packages} \
308
+ && break
309
+ echo "Failed to install packages. Sleeping before trying again..."
310
+ sleep 10
311
+ done
312
+ }
313
+
284
314
  bootstrap_remote_rhel()
285
315
  {
286
316
  case "${platform_version}" in
287
317
  9.*) bootstrap_remote_rhel_9 ;;
318
+ 10.*) bootstrap_remote_rhel_10 ;;
288
319
  esac
289
320
  }
290
321
 
@@ -1,310 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import dataclasses
4
- import itertools
5
- import pathlib
6
- import sys
7
- import textwrap
8
- import typing as t
9
-
10
- from ansible.module_utils.common.messages import Detail, ErrorSummary
11
- from ansible._internal._datatag._tags import Origin
12
- from ansible.module_utils._internal import _ambient_context, _traceback
13
- from ansible import errors
14
-
15
- if t.TYPE_CHECKING:
16
- from ansible.utils.display import Display
17
-
18
-
19
- class RedactAnnotatedSourceContext(_ambient_context.AmbientContextBase):
20
- """
21
- When active, this context will redact annotated source lines, showing only the origin.
22
- """
23
-
24
-
25
- def _dedupe_and_concat_message_chain(message_parts: list[str]) -> str:
26
- message_parts = list(reversed(message_parts))
27
-
28
- message = message_parts.pop(0)
29
-
30
- for message_part in message_parts:
31
- # avoid duplicate messages where the cause was already concatenated to the exception message
32
- if message_part.endswith(message):
33
- message = message_part
34
- else:
35
- message = concat_message(message_part, message)
36
-
37
- return message
38
-
39
-
40
- def _collapse_error_details(error_details: t.Sequence[Detail]) -> list[Detail]:
41
- """
42
- Return a potentially modified error chain, with redundant errors collapsed into previous error(s) in the chain.
43
- This reduces the verbosity of messages by eliminating repetition when multiple errors in the chain share the same contextual information.
44
- """
45
- previous_error = error_details[0]
46
- previous_warnings: list[str] = []
47
- collapsed_error_details: list[tuple[Detail, list[str]]] = [(previous_error, previous_warnings)]
48
-
49
- for error in error_details[1:]:
50
- details_present = error.formatted_source_context or error.help_text
51
- details_changed = error.formatted_source_context != previous_error.formatted_source_context or error.help_text != previous_error.help_text
52
-
53
- if details_present and details_changed:
54
- previous_error = error
55
- previous_warnings = []
56
- collapsed_error_details.append((previous_error, previous_warnings))
57
- else:
58
- previous_warnings.append(error.msg)
59
-
60
- final_error_details: list[Detail] = []
61
-
62
- for error, messages in collapsed_error_details:
63
- final_error_details.append(dataclasses.replace(error, msg=_dedupe_and_concat_message_chain([error.msg] + messages)))
64
-
65
- return final_error_details
66
-
67
-
68
- def _get_cause(exception: BaseException) -> BaseException | None:
69
- # deprecated: description='remove support for orig_exc (deprecated in 2.23)' core_version='2.27'
70
-
71
- if not isinstance(exception, errors.AnsibleError):
72
- return exception.__cause__
73
-
74
- if exception.__cause__:
75
- if exception.orig_exc and exception.orig_exc is not exception.__cause__:
76
- _get_display().warning(
77
- msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given, but differed from the cause given by `raise ... from`.",
78
- )
79
-
80
- return exception.__cause__
81
-
82
- if exception.orig_exc:
83
- # encourage the use of `raise ... from` before deprecating `orig_exc`
84
- _get_display().warning(msg=f"The `orig_exc` argument to `{type(exception).__name__}` was given without using `raise ... from orig_exc`.")
85
-
86
- return exception.orig_exc
87
-
88
- return None
89
-
90
-
91
- class _TemporaryDisplay:
92
- # DTFIX-FUTURE: generalize this and hide it in the display module so all users of Display can benefit
93
-
94
- @staticmethod
95
- def warning(*args, **kwargs):
96
- print(f'FALLBACK WARNING: {args} {kwargs}', file=sys.stderr)
97
-
98
- @staticmethod
99
- def deprecated(*args, **kwargs):
100
- print(f'FALLBACK DEPRECATION: {args} {kwargs}', file=sys.stderr)
101
-
102
-
103
- def _get_display() -> Display | _TemporaryDisplay:
104
- try:
105
- from ansible.utils.display import Display
106
- except ImportError:
107
- return _TemporaryDisplay()
108
-
109
- return Display()
110
-
111
-
112
- def _create_error_summary(exception: BaseException, event: _traceback.TracebackEvent | None = None) -> ErrorSummary:
113
- from . import _captured # avoid circular import due to AnsibleError import
114
-
115
- current_exception: BaseException | None = exception
116
- error_details: list[Detail] = []
117
-
118
- if event:
119
- formatted_traceback = _traceback.maybe_extract_traceback(exception, event)
120
- else:
121
- formatted_traceback = None
122
-
123
- while current_exception:
124
- if isinstance(current_exception, errors.AnsibleError):
125
- include_cause_message = current_exception._include_cause_message
126
- edc = Detail(
127
- msg=current_exception._original_message.strip(),
128
- formatted_source_context=current_exception._formatted_source_context,
129
- help_text=current_exception._help_text,
130
- )
131
- else:
132
- include_cause_message = True
133
- edc = Detail(
134
- msg=str(current_exception).strip(),
135
- )
136
-
137
- error_details.append(edc)
138
-
139
- if isinstance(current_exception, _captured.AnsibleCapturedError):
140
- detail = current_exception.error_summary
141
- error_details.extend(detail.details)
142
-
143
- if formatted_traceback and detail.formatted_traceback:
144
- formatted_traceback = (
145
- f'{detail.formatted_traceback}\n'
146
- f'The {current_exception.context} exception above was the direct cause of the following controller exception:\n\n'
147
- f'{formatted_traceback}'
148
- )
149
-
150
- if not include_cause_message:
151
- break
152
-
153
- current_exception = _get_cause(current_exception)
154
-
155
- return ErrorSummary(details=tuple(error_details), formatted_traceback=formatted_traceback)
156
-
157
-
158
- def concat_message(left: str, right: str) -> str:
159
- """Normalize `left` by removing trailing punctuation and spaces before appending new punctuation and `right`."""
160
- return f'{left.rstrip(". ")}: {right}'
161
-
162
-
163
- def get_chained_message(exception: BaseException) -> str:
164
- """
165
- Return the full chain of exception messages by concatenating the cause(s) until all are exhausted.
166
- """
167
- error_summary = _create_error_summary(exception)
168
- message_parts = [edc.msg for edc in error_summary.details]
169
-
170
- return _dedupe_and_concat_message_chain(message_parts)
171
-
172
-
173
- @dataclasses.dataclass(kw_only=True, frozen=True)
174
- class SourceContext:
175
- origin: Origin
176
- annotated_source_lines: list[str]
177
- target_line: str | None
178
-
179
- def __str__(self) -> str:
180
- msg_lines = [f'Origin: {self.origin}']
181
-
182
- if self.annotated_source_lines:
183
- msg_lines.append('')
184
- msg_lines.extend(self.annotated_source_lines)
185
-
186
- return '\n'.join(msg_lines)
187
-
188
- @classmethod
189
- def from_value(cls, value: t.Any) -> SourceContext | None:
190
- """Attempt to retrieve source and render a contextual indicator from the value's origin (if any)."""
191
- if value is None:
192
- return None
193
-
194
- if isinstance(value, Origin):
195
- origin = value
196
- value = None
197
- else:
198
- origin = Origin.get_tag(value)
199
-
200
- if RedactAnnotatedSourceContext.current(optional=True):
201
- return cls.error('content redacted')
202
-
203
- if origin and origin.path:
204
- return cls.from_origin(origin)
205
-
206
- # DTFIX-RELEASE: redaction context may not be sufficient to avoid secret disclosure without SensitiveData and other enhancements
207
- if value is None:
208
- truncated_value = None
209
- annotated_source_lines = []
210
- else:
211
- # DTFIX-FUTURE: cleanup/share width
212
- try:
213
- value = str(value)
214
- except Exception as ex:
215
- value = f'<< context unavailable: {ex} >>'
216
-
217
- truncated_value = textwrap.shorten(value, width=120)
218
- annotated_source_lines = [truncated_value]
219
-
220
- return SourceContext(
221
- origin=origin or Origin.UNKNOWN,
222
- annotated_source_lines=annotated_source_lines,
223
- target_line=truncated_value,
224
- )
225
-
226
- @staticmethod
227
- def error(message: str | None, origin: Origin | None = None) -> SourceContext:
228
- return SourceContext(
229
- origin=origin,
230
- annotated_source_lines=[f'(source not shown: {message})'] if message else [],
231
- target_line=None,
232
- )
233
-
234
- @classmethod
235
- def from_origin(cls, origin: Origin) -> SourceContext:
236
- """Attempt to retrieve source and render a contextual indicator of an error location."""
237
- from ansible.parsing.vault import is_encrypted # avoid circular import
238
-
239
- # DTFIX-FUTURE: support referencing the column after the end of the target line, so we can indicate where a missing character (quote) needs to be added
240
- # this is also useful for cases like end-of-stream reported by the YAML parser
241
-
242
- # DTFIX-FUTURE: Implement line wrapping and match annotated line width to the terminal display width.
243
-
244
- context_line_count: t.Final = 2
245
- max_annotated_line_width: t.Final = 120
246
- truncation_marker: t.Final = '...'
247
-
248
- target_line_num = origin.line_num
249
-
250
- if RedactAnnotatedSourceContext.current(optional=True):
251
- return cls.error('content redacted', origin)
252
-
253
- if not target_line_num or target_line_num < 1:
254
- return cls.error(None, origin) # message omitted since lack of line number is obvious from pos
255
-
256
- start_line_idx = max(0, (target_line_num - 1) - context_line_count) # if near start of file
257
- target_col_num = origin.col_num
258
-
259
- try:
260
- with pathlib.Path(origin.path).open() as src:
261
- first_line = src.readline()
262
- lines = list(itertools.islice(itertools.chain((first_line,), src), start_line_idx, target_line_num))
263
- except Exception as ex:
264
- return cls.error(type(ex).__name__, origin)
265
-
266
- if is_encrypted(first_line):
267
- return cls.error('content encrypted', origin)
268
-
269
- if len(lines) != target_line_num - start_line_idx:
270
- return cls.error('file truncated', origin)
271
-
272
- annotated_source_lines = []
273
-
274
- line_label_width = len(str(target_line_num))
275
- max_src_line_len = max_annotated_line_width - line_label_width - 1
276
-
277
- usable_line_len = max_src_line_len
278
-
279
- for line_num, line in enumerate(lines, start_line_idx + 1):
280
- line = line.rstrip('\n') # universal newline default mode on `open` ensures we'll never see anything but \n
281
- line = line.replace('\t', ' ') # mixed tab/space handling is intentionally disabled since we're both format and display config agnostic
282
-
283
- if len(line) > max_src_line_len:
284
- line = line[: max_src_line_len - len(truncation_marker)] + truncation_marker
285
- usable_line_len = max_src_line_len - len(truncation_marker)
286
-
287
- annotated_source_lines.append(f'{str(line_num).rjust(line_label_width)}{" " if line else ""}{line}')
288
-
289
- if target_col_num and usable_line_len >= target_col_num >= 1:
290
- column_marker = f'column {target_col_num}'
291
-
292
- target_col_idx = target_col_num - 1
293
-
294
- if target_col_idx + 2 + len(column_marker) > max_src_line_len:
295
- column_marker = f'{" " * (target_col_idx - len(column_marker) - 1)}{column_marker} ^'
296
- else:
297
- column_marker = f'{" " * target_col_idx}^ {column_marker}'
298
-
299
- column_marker = f'{" " * line_label_width} {column_marker}'
300
-
301
- annotated_source_lines.append(column_marker)
302
- elif target_col_num is None:
303
- underline_length = len(annotated_source_lines[-1]) - line_label_width - 1
304
- annotated_source_lines.append(f'{" " * line_label_width} {"^" * underline_length}')
305
-
306
- return SourceContext(
307
- origin=origin,
308
- annotated_source_lines=annotated_source_lines,
309
- target_line=lines[-1].rstrip('\n'), # universal newline default mode on `open` ensures we'll never see anything but \n
310
- )