ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b6__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 (225) hide show
  1. ansible/_internal/__init__.py +1 -1
  2. ansible/_internal/_ansiballz/__init__.py +0 -0
  3. ansible/_internal/_ansiballz/_builder.py +101 -0
  4. ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
  5. ansible/_internal/_collection_proxy.py +1 -1
  6. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  7. ansible/_internal/_errors/_captured.py +25 -30
  8. ansible/_internal/_errors/_error_factory.py +89 -0
  9. ansible/_internal/_errors/_error_utils.py +240 -0
  10. ansible/_internal/_errors/_task_timeout.py +28 -0
  11. ansible/_internal/_event_formatting.py +127 -0
  12. ansible/_internal/_json/__init__.py +5 -5
  13. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  14. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  15. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  16. ansible/_internal/_ssh/__init__.py +0 -0
  17. ansible/_internal/_ssh/_agent_launch.py +91 -0
  18. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  19. ansible/_internal/_templating/__init__.py +5 -3
  20. ansible/_internal/_templating/_datatag.py +2 -1
  21. ansible/_internal/_templating/_engine.py +3 -4
  22. ansible/_internal/_templating/_jinja_bits.py +28 -20
  23. ansible/_internal/_templating/_jinja_common.py +18 -27
  24. ansible/_internal/_templating/_jinja_plugins.py +36 -5
  25. ansible/_internal/_templating/_lazy_containers.py +5 -5
  26. ansible/_internal/_templating/_template_vars.py +72 -0
  27. ansible/_internal/_templating/_transform.py +26 -19
  28. ansible/_internal/_templating/_utils.py +1 -1
  29. ansible/_internal/_yaml/_constructor.py +4 -4
  30. ansible/_internal/_yaml/_dumper.py +26 -18
  31. ansible/_internal/_yaml/_errors.py +7 -7
  32. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  33. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  34. ansible/cli/__init__.py +11 -93
  35. ansible/cli/arguments/option_helpers.py +3 -4
  36. ansible/cli/console.py +1 -1
  37. ansible/cli/doc.py +86 -30
  38. ansible/cli/inventory.py +5 -7
  39. ansible/compat/importlib_resources.py +9 -12
  40. ansible/config/base.yml +46 -0
  41. ansible/errors/__init__.py +98 -50
  42. ansible/executor/module_common.py +75 -49
  43. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  44. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  45. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  46. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  47. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  48. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  49. ansible/executor/powershell/module_manifest.py +52 -0
  50. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  51. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  52. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  53. ansible/executor/process/worker.py +40 -115
  54. ansible/executor/task_executor.py +26 -61
  55. ansible/executor/task_result.py +2 -4
  56. ansible/galaxy/api.py +1 -4
  57. ansible/galaxy/collection/__init__.py +2 -10
  58. ansible/galaxy/collection/concrete_artifact_manager.py +2 -8
  59. ansible/galaxy/role.py +2 -2
  60. ansible/inventory/manager.py +1 -1
  61. ansible/module_utils/_internal/__init__.py +7 -7
  62. ansible/module_utils/_internal/_ambient_context.py +3 -3
  63. ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
  64. ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
  65. ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
  66. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
  67. ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +13 -39
  68. ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
  69. ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
  70. ansible/module_utils/_internal/_datatag/__init__.py +43 -15
  71. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  72. ansible/module_utils/_internal/_deprecator.py +67 -55
  73. ansible/module_utils/_internal/_errors.py +88 -17
  74. ansible/module_utils/_internal/_event_utils.py +61 -0
  75. ansible/module_utils/_internal/_json/_profiles/__init__.py +22 -4
  76. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  77. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  78. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  79. ansible/module_utils/{common/messages.py → _internal/_messages.py} +54 -49
  80. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  81. ansible/module_utils/_internal/_plugin_info.py +15 -2
  82. ansible/module_utils/_internal/_stack.py +22 -0
  83. ansible/module_utils/_internal/_text_utils.py +6 -0
  84. ansible/module_utils/_internal/_traceback.py +11 -8
  85. ansible/module_utils/ansible_release.py +1 -1
  86. ansible/module_utils/basic.py +95 -71
  87. ansible/module_utils/common/arg_spec.py +2 -2
  88. ansible/module_utils/common/collections.py +6 -0
  89. ansible/module_utils/common/json.py +2 -2
  90. ansible/module_utils/common/respawn.py +4 -41
  91. ansible/module_utils/common/text/converters.py +3 -3
  92. ansible/module_utils/common/validation.py +1 -1
  93. ansible/module_utils/common/warnings.py +80 -23
  94. ansible/module_utils/common/yaml.py +1 -1
  95. ansible/module_utils/connection.py +8 -11
  96. ansible/module_utils/datatag.py +5 -2
  97. ansible/module_utils/facts/hardware/linux.py +1 -1
  98. ansible/module_utils/facts/sysctl.py +4 -6
  99. ansible/module_utils/facts/system/caps.py +2 -2
  100. ansible/module_utils/facts/system/distribution.py +16 -3
  101. ansible/module_utils/facts/system/local.py +1 -1
  102. ansible/module_utils/facts/virtual/linux.py +2 -2
  103. ansible/module_utils/service.py +3 -10
  104. ansible/module_utils/urls.py +4 -4
  105. ansible/modules/apt_repository.py +17 -39
  106. ansible/modules/assemble.py +2 -2
  107. ansible/modules/async_status.py +13 -11
  108. ansible/modules/async_wrapper.py +12 -22
  109. ansible/modules/command.py +3 -3
  110. ansible/modules/copy.py +4 -4
  111. ansible/modules/cron.py +1 -1
  112. ansible/modules/dnf5.py +14 -22
  113. ansible/modules/file.py +16 -17
  114. ansible/modules/find.py +3 -3
  115. ansible/modules/get_url.py +17 -0
  116. ansible/modules/git.py +9 -7
  117. ansible/modules/hostname.py +0 -1
  118. ansible/modules/known_hosts.py +12 -14
  119. ansible/modules/package.py +6 -0
  120. ansible/modules/replace.py +2 -2
  121. ansible/modules/service.py +3 -9
  122. ansible/modules/slurp.py +10 -13
  123. ansible/modules/stat.py +5 -7
  124. ansible/modules/unarchive.py +6 -6
  125. ansible/modules/user.py +1 -1
  126. ansible/modules/wait_for.py +28 -30
  127. ansible/modules/yum_repository.py +4 -3
  128. ansible/parsing/ajson.py +3 -5
  129. ansible/parsing/dataloader.py +6 -6
  130. ansible/parsing/mod_args.py +1 -1
  131. ansible/parsing/plugin_docs.py +2 -2
  132. ansible/parsing/utils/yaml.py +3 -3
  133. ansible/parsing/vault/__init__.py +10 -14
  134. ansible/playbook/base.py +7 -2
  135. ansible/playbook/included_file.py +3 -1
  136. ansible/playbook/play_context.py +2 -0
  137. ansible/playbook/playbook_include.py +1 -1
  138. ansible/playbook/taggable.py +19 -8
  139. ansible/playbook/task.py +2 -0
  140. ansible/plugins/__init__.py +0 -25
  141. ansible/plugins/action/__init__.py +8 -31
  142. ansible/plugins/action/add_host.py +1 -1
  143. ansible/plugins/action/assemble.py +8 -16
  144. ansible/plugins/action/async_status.py +7 -2
  145. ansible/plugins/action/copy.py +8 -7
  146. ansible/plugins/action/fetch.py +3 -3
  147. ansible/plugins/action/gather_facts.py +8 -8
  148. ansible/plugins/action/package.py +5 -8
  149. ansible/plugins/action/script.py +8 -15
  150. ansible/plugins/action/service.py +3 -7
  151. ansible/plugins/action/template.py +11 -10
  152. ansible/plugins/action/unarchive.py +5 -15
  153. ansible/plugins/action/uri.py +9 -20
  154. ansible/plugins/cache/__init__.py +17 -19
  155. ansible/plugins/callback/__init__.py +4 -6
  156. ansible/plugins/callback/junit.py +4 -2
  157. ansible/plugins/callback/tree.py +5 -5
  158. ansible/plugins/connection/local.py +6 -6
  159. ansible/plugins/connection/paramiko_ssh.py +5 -5
  160. ansible/plugins/connection/ssh.py +25 -15
  161. ansible/plugins/connection/winrm.py +6 -3
  162. ansible/plugins/doc_fragments/constructed.py +2 -2
  163. ansible/plugins/filter/core.py +32 -27
  164. ansible/plugins/filter/encryption.py +14 -6
  165. ansible/plugins/inventory/__init__.py +11 -10
  166. ansible/plugins/inventory/script.py +1 -1
  167. ansible/plugins/list.py +73 -19
  168. ansible/plugins/loader.py +7 -7
  169. ansible/plugins/lookup/csvfile.py +16 -71
  170. ansible/plugins/lookup/first_found.py +2 -1
  171. ansible/plugins/lookup/template.py +9 -4
  172. ansible/plugins/shell/__init__.py +56 -2
  173. ansible/plugins/shell/powershell.py +67 -9
  174. ansible/plugins/shell/sh.py +10 -5
  175. ansible/plugins/strategy/__init__.py +3 -3
  176. ansible/plugins/test/core.py +22 -16
  177. ansible/plugins/test/finished.yml +1 -1
  178. ansible/plugins/test/uri.py +2 -5
  179. ansible/release.py +1 -1
  180. ansible/template/__init__.py +38 -54
  181. ansible/utils/collection_loader/_collection_finder.py +3 -3
  182. ansible/utils/display.py +124 -138
  183. ansible/utils/galaxy.py +2 -2
  184. ansible/utils/hashing.py +6 -8
  185. ansible/utils/listify.py +6 -4
  186. ansible/utils/path.py +5 -7
  187. ansible/utils/py3compat.py +2 -1
  188. ansible/utils/ssh_functions.py +3 -2
  189. ansible/utils/unsafe_proxy.py +1 -1
  190. ansible/vars/hostvars.py +1 -1
  191. ansible/vars/plugins.py +3 -3
  192. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/METADATA +1 -1
  193. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/RECORD +224 -204
  194. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/WHEEL +1 -1
  195. ansible_test/_data/completion/docker.txt +3 -3
  196. ansible_test/_data/completion/remote.txt +1 -0
  197. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  198. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  199. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  200. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  201. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  202. ansible_test/_internal/commands/integration/coverage.py +7 -2
  203. ansible_test/_internal/host_profiles.py +62 -10
  204. ansible_test/_internal/provisioning.py +10 -4
  205. ansible_test/_internal/ssh.py +1 -5
  206. ansible_test/_internal/thread.py +2 -1
  207. ansible_test/_internal/timeout.py +1 -1
  208. ansible_test/_internal/util.py +40 -12
  209. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  210. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  211. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  212. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  213. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  214. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
  215. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  216. ansible_test/_util/target/setup/requirements.py +3 -9
  217. ansible/_internal/_errors/_utils.py +0 -310
  218. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/entry_points.txt +0 -0
  219. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/COPYING +0 -0
  220. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  221. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  222. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  223. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  224. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  225. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/top_level.txt +0 -0
@@ -47,13 +47,13 @@ options:
47
47
  - The key from input dictionary used to generate groups.
48
48
  default_value:
49
49
  description:
50
- - The default value when the host variable's value is an empty string.
50
+ - The default value when the host variable's value is V(None) or an empty string.
51
51
  - This option is mutually exclusive with O(keyed_groups[].trailing_separator).
52
52
  type: str
53
53
  version_added: '2.12'
54
54
  trailing_separator:
55
55
  description:
56
- - Set this option to V(false) to omit the O(keyed_groups[].separator) after the host variable when the value is an empty string.
56
+ - Set this option to V(false) to omit the O(keyed_groups[].separator) after the host variable when the value is V(None) or an empty string.
57
57
  - This option is mutually exclusive with O(keyed_groups[].default_value).
58
58
  type: bool
59
59
  default: true
@@ -32,10 +32,10 @@ from ansible.module_utils.common.json import get_encoder, get_decoder
32
32
  from ansible.module_utils.six import string_types, integer_types, text_type
33
33
  from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
34
34
  from ansible.module_utils.common.collections import is_sequence
35
- from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
36
35
  from ansible.parsing.yaml.dumper import AnsibleDumper
37
- from ansible.plugins import accept_args_markers, accept_lazy_markers
36
+ from ansible.template import accept_args_markers, accept_lazy_markers
38
37
  from ansible._internal._templating._jinja_common import MarkerError, UndefinedMarker, validate_arg_type
38
+ from ansible._internal._yaml import _loader as _yaml_loader
39
39
  from ansible.utils.display import Display
40
40
  from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
41
41
  from ansible.utils.hashing import md5s, checksum_s
@@ -47,13 +47,13 @@ display = Display()
47
47
  UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')
48
48
 
49
49
 
50
- def to_yaml(a, *_args, default_flow_style: bool | None = None, dump_vault_tags: bool | None = None, **kwargs) -> str:
50
+ @accept_lazy_markers
51
+ def to_yaml(a, *_args, default_flow_style: bool | None = None, **kwargs) -> str:
51
52
  """Serialize input as terse flow-style YAML."""
52
- dumper = partial(AnsibleDumper, dump_vault_tags=dump_vault_tags)
53
-
54
- return yaml.dump(a, Dumper=dumper, allow_unicode=True, default_flow_style=default_flow_style, **kwargs)
53
+ return yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kwargs)
55
54
 
56
55
 
56
+ @accept_lazy_markers
57
57
  def to_nice_yaml(a, indent=4, *_args, default_flow_style=False, **kwargs) -> str:
58
58
  """Serialize input as verbose multi-line YAML."""
59
59
  return to_yaml(a, indent=indent, default_flow_style=default_flow_style, **kwargs)
@@ -98,6 +98,7 @@ _valid_bool_false = {'no', 'off', 'false', '0'}
98
98
  def to_bool(value: object) -> bool:
99
99
  """Convert well-known input values to a boolean value."""
100
100
  value_to_check: object
101
+
101
102
  if isinstance(value, str):
102
103
  value_to_check = value.lower() # accept mixed case variants
103
104
  elif isinstance(value, int): # bool is also an int
@@ -105,14 +106,17 @@ def to_bool(value: object) -> bool:
105
106
  else:
106
107
  value_to_check = value
107
108
 
108
- if value_to_check in _valid_bool_true:
109
- return True
109
+ try:
110
+ if value_to_check in _valid_bool_true:
111
+ return True
110
112
 
111
- if value_to_check in _valid_bool_false:
112
- return False
113
+ if value_to_check in _valid_bool_false:
114
+ return False
113
115
 
114
- # if we're still here, the value is unsupported- always fire a deprecation warning
115
- result = value_to_check == 1 # backwards compatibility with the old code which checked: value in ('yes', 'on', '1', 'true', 1)
116
+ # if we're still here, the value is unsupported- always fire a deprecation warning
117
+ result = value_to_check == 1 # backwards compatibility with the old code which checked: value in ('yes', 'on', '1', 'true', 1)
118
+ except TypeError:
119
+ result = False
116
120
 
117
121
  # NB: update the doc string to reflect reality once this fallback is removed
118
122
  display.deprecated(
@@ -247,20 +251,24 @@ def regex_escape(string, re_type='python'):
247
251
 
248
252
 
249
253
  def from_yaml(data):
250
- if isinstance(data, string_types):
251
- # The ``text_type`` call here strips any custom
252
- # string wrapper class, so that CSafeLoader can
253
- # read the data
254
- return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
254
+ if data is None:
255
+ return None
256
+
257
+ if isinstance(data, str):
258
+ return yaml.load(data, Loader=_yaml_loader.AnsibleInstrumentedLoader) # type: ignore[arg-type]
259
+
260
+ display.deprecated(f"The from_yaml filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
255
261
  return data
256
262
 
257
263
 
258
264
  def from_yaml_all(data):
259
- if isinstance(data, string_types):
260
- # The ``text_type`` call here strips any custom
261
- # string wrapper class, so that CSafeLoader can
262
- # read the data
263
- return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
265
+ if data is None:
266
+ return [] # backward compatibility; ensure consistent result between classic/native Jinja for None/empty string input
267
+
268
+ if isinstance(data, str):
269
+ return yaml.load_all(data, Loader=_yaml_loader.AnsibleInstrumentedLoader) # type: ignore[arg-type]
270
+
271
+ display.deprecated(f"The from_yaml_all filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
264
272
  return data
265
273
 
266
274
 
@@ -338,7 +346,7 @@ def to_uuid(string, namespace=UUID_NAMESPACE_ANSIBLE):
338
346
  @accept_args_markers
339
347
  def mandatory(a: object, msg: str | None = None) -> object:
340
348
  """Make a variable mandatory."""
341
- # DTFIX-RELEASE: deprecate this filter; there are much better ways via undef, etc...
349
+ # DTFIX-FUTURE: deprecate this filter; there are much better ways via undef, etc...
342
350
  # also remember to remove unit test checking for _undefined_name
343
351
  if isinstance(a, UndefinedMarker):
344
352
  if msg is not None:
@@ -654,7 +662,7 @@ def _cleansed_groupby(*args, **kwargs):
654
662
 
655
663
  return res
656
664
 
657
- # DTFIX-RELEASE: make these dumb wrappers more dynamic
665
+ # DTFIX-FUTURE: make these dumb wrappers more dynamic
658
666
 
659
667
 
660
668
  @accept_args_markers
@@ -806,7 +814,6 @@ class FilterModule(object):
806
814
  'groupby': _cleansed_groupby,
807
815
 
808
816
  # Jinja builtins that need special arg handling
809
- # DTFIX-RELEASE: document these now that they're overridden, or hide them so they don't show up as undocumented
810
817
  'd': ansible_default, # replaces the implementation instead of wrapping it
811
818
  'default': ansible_default, # replaces the implementation instead of wrapping it
812
819
  'map': wrapped_map,
@@ -815,5 +822,3 @@ class FilterModule(object):
815
822
  'reject': wrapped_reject,
816
823
  'rejectattr': wrapped_rejectattr,
817
824
  }
818
-
819
- # DTFIX-RELEASE: document protomatter plugins, or hide them from ansible-doc/galaxy (not related to this code, but needed some place to put this comment)
@@ -4,10 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  from ansible.errors import AnsibleError
6
6
  from ansible.module_utils.common.text.converters import to_native, to_bytes
7
- from ansible.plugins import accept_args_markers
8
- from ansible._internal._templating._jinja_common import get_first_marker_arg, VaultExceptionMarker
7
+ from ansible._internal._templating._jinja_common import VaultExceptionMarker
9
8
  from ansible._internal._datatag._tags import VaultedValue
10
9
  from ansible.parsing.vault import is_encrypted, VaultSecret, VaultLib, VaultHelper
10
+ from ansible import template as _template
11
11
  from ansible.utils.display import Display
12
12
 
13
13
  display = Display()
@@ -21,7 +21,11 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
21
21
  raise TypeError(f"Can only vault strings, instead we got {type(data)}.")
22
22
 
23
23
  if vaultid is not None:
24
- display.deprecated("Use of undocumented 'vaultid', use 'vault_id' instead", version='2.20')
24
+ display.deprecated(
25
+ msg="Use of undocumented `vaultid`.",
26
+ version="2.20",
27
+ help_text="Use `vault_id` instead.",
28
+ )
25
29
 
26
30
  if vault_id == 'filter_default':
27
31
  vault_id = vaultid
@@ -43,12 +47,12 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
43
47
  return vault
44
48
 
45
49
 
46
- @accept_args_markers
50
+ @_template.accept_args_markers
47
51
  def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
48
52
  if isinstance(vault, VaultExceptionMarker):
49
53
  vault = vault._disarm()
50
54
 
51
- if (first_marker := get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
55
+ if (first_marker := _template.get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
52
56
  return first_marker
53
57
 
54
58
  if not isinstance(secret, (str, bytes)):
@@ -58,7 +62,11 @@ def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
58
62
  raise TypeError(f"Vault should be in the form of a string, instead we got {type(vault)}.")
59
63
 
60
64
  if vaultid is not None:
61
- display.deprecated("Use of undocumented 'vaultid', use 'vault_id' instead", version='2.20')
65
+ display.deprecated(
66
+ msg="Use of undocumented `vaultid`.",
67
+ version="2.20",
68
+ help_text="Use `vault_id` instead.",
69
+ )
62
70
 
63
71
  if vault_id == 'filter_default':
64
72
  vault_id = vaultid
@@ -334,7 +334,6 @@ class Cacheable(_plugin_info.HasPluginInfo, _ConfigurablePlugin):
334
334
 
335
335
  def _get_cache_prefix(self, path: str) -> str:
336
336
  """Return a predictable unique key based on the given path."""
337
- # DTFIX-RELEASE: choose a better hashing approach
338
337
  return 'k' + hashlib.sha256(f'{self.ansible_name}{path}'.encode(), usedforsecurity=False).hexdigest()[:6]
339
338
 
340
339
  def clear_cache(self) -> None:
@@ -402,6 +401,8 @@ class Constructable(_BaseInventoryPlugin):
402
401
 
403
402
  def _add_host_to_keyed_groups(self, keys, variables, host, strict=False, fetch_hostvars=True):
404
403
  """ helper to create groups for plugins based on variable values and add the corresponding hosts to it"""
404
+ should_default_value = (None, '')
405
+
405
406
  if keys and isinstance(keys, list):
406
407
  for keyed in keys:
407
408
  if keyed and isinstance(keyed, dict):
@@ -418,7 +419,9 @@ class Constructable(_BaseInventoryPlugin):
418
419
  trailing_separator = keyed.get('trailing_separator')
419
420
  if trailing_separator is not None and default_value_name is not None:
420
421
  raise AnsibleParserError("parameters are mutually exclusive for keyed groups: default_value|trailing_separator")
421
- if key or (key == '' and default_value_name is not None):
422
+
423
+ use_default = key in should_default_value and default_value_name is not None
424
+ if key or use_default:
422
425
  prefix = keyed.get('prefix', '')
423
426
  sep = keyed.get('separator', '_')
424
427
  raw_parent_name = keyed.get('parent_group', None)
@@ -434,23 +437,21 @@ class Constructable(_BaseInventoryPlugin):
434
437
  continue
435
438
 
436
439
  new_raw_group_names = []
437
- if isinstance(key, string_types):
438
- # if key is empty, 'default_value' will be used as group name
439
- if key == '' and default_value_name is not None:
440
- new_raw_group_names.append(default_value_name)
441
- else:
442
- new_raw_group_names.append(key)
440
+ if use_default:
441
+ new_raw_group_names.append(default_value_name)
442
+ elif isinstance(key, string_types):
443
+ new_raw_group_names.append(key)
443
444
  elif isinstance(key, list):
444
445
  for name in key:
445
446
  # if list item is empty, 'default_value' will be used as group name
446
- if name == '' and default_value_name is not None:
447
+ if name in should_default_value and default_value_name is not None:
447
448
  new_raw_group_names.append(default_value_name)
448
449
  else:
449
450
  new_raw_group_names.append(name)
450
451
  elif isinstance(key, Mapping):
451
452
  for (gname, gval) in key.items():
452
453
  bare_name = '%s%s%s' % (gname, sep, gval)
453
- if gval == '':
454
+ if gval in should_default_value:
454
455
  # key's value is empty
455
456
  if default_value_name is not None:
456
457
  bare_name = '%s%s%s' % (gname, sep, default_value_name)
@@ -367,7 +367,7 @@ def run_command(path: str, options: list[str], origin: Origin) -> tuple[str, str
367
367
  if stderr and not stderr.endswith('\n'):
368
368
  stderr += '\n'
369
369
 
370
- # DTFIX-RELEASE: another use case for the "not quite help text, definitely not message" diagnostic output on errors
370
+ # DTFIX-FUTURE: another use case for the "not quite help text, definitely not message" diagnostic output on errors
371
371
  stderr_help_text = f'Standard error from inventory script:\n{stderr}' if stderr.strip() else None
372
372
 
373
373
  if sp.returncode != 0:
ansible/plugins/list.py CHANGED
@@ -4,6 +4,7 @@
4
4
  from __future__ import annotations
5
5
 
6
6
 
7
+ import dataclasses
7
8
  import os
8
9
 
9
10
  from ansible import context
@@ -14,6 +15,7 @@ from ansible.module_utils.common.text.converters import to_native, to_bytes
14
15
  from ansible.plugins import loader
15
16
  from ansible.utils.display import Display
16
17
  from ansible.utils.collection_loader._collection_finder import _get_collection_path
18
+ from ansible._internal._templating._jinja_plugins import get_jinja_builtin_plugin_descriptions
17
19
 
18
20
  display = Display()
19
21
 
@@ -25,6 +27,20 @@ IGNORE = {
25
27
  }
26
28
 
27
29
 
30
+ @dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
31
+ class _PluginDocMetadata:
32
+ """Information about a plugin."""
33
+
34
+ name: str
35
+ """The fully qualified name of the plugin."""
36
+ path: bytes | None = None
37
+ """The path to the plugin file, or None if not available."""
38
+ plugin_obj: object | None = None
39
+ """The loaded plugin object, or None if not loaded."""
40
+ jinja_builtin_short_description: str | None = None
41
+ """The short description of the plugin if it is a Jinja builtin, otherwise None."""
42
+
43
+
28
44
  def get_composite_name(collection, name, path, depth):
29
45
  resolved_collection = collection
30
46
  if '.' not in name:
@@ -42,7 +58,7 @@ def get_composite_name(collection, name, path, depth):
42
58
  return '.'.join(composite)
43
59
 
44
60
 
45
- def _list_plugins_from_paths(ptype, dirs, collection, depth=0):
61
+ def _list_plugins_from_paths(ptype, dirs, collection, depth=0, docs=False):
46
62
  # TODO: update to use importlib.resources
47
63
 
48
64
  plugins = {}
@@ -77,14 +93,15 @@ def _list_plugins_from_paths(ptype, dirs, collection, depth=0):
77
93
  continue
78
94
 
79
95
  # actually recurse dirs
80
- plugins.update(_list_plugins_from_paths(ptype, [to_native(full_path)], collection, depth=depth + 1))
96
+ plugins.update(_list_plugins_from_paths(ptype, [to_native(full_path)], collection, depth=depth + 1, docs=docs))
81
97
  else:
82
98
  if any([
83
99
  plugin in C.IGNORE_FILES, # general files to ignore
84
100
  to_native(b_ext) in C.REJECT_EXTS, # general extensions to ignore
85
- b_ext in (b'.yml', b'.yaml', b'.json'), # ignore docs files TODO: constant!
101
+ b_ext in (b'.yml', b'.yaml', b'.json'), # ignore docs files
86
102
  plugin in IGNORE.get(bkey, ()), # plugin in reject list
87
103
  os.path.islink(full_path), # skip aliases, author should document in 'aliases' field
104
+ not docs and b_ext in (b''), # ignore no ext when looking for docs files
88
105
  ]):
89
106
  continue
90
107
 
@@ -116,21 +133,37 @@ def _list_j2_plugins_from_file(collection, plugin_path, ptype, plugin_name):
116
133
  return file_plugins
117
134
 
118
135
 
119
- def list_collection_plugins(ptype, collections, search_paths=None):
136
+ def list_collection_plugins(ptype: str, collections: dict[str, bytes], search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
137
+ # Kept for backwards compatibility.
138
+ return {
139
+ name: (info.path, info.plugin_obj)
140
+ for name, info in _list_collection_plugins_with_info(ptype, collections).items()
141
+ }
142
+
143
+
144
+ def _list_collection_plugins_with_info(
145
+ ptype: str,
146
+ collections: dict[str, bytes],
147
+ ) -> dict[str, _PluginDocMetadata]:
120
148
  # TODO: update to use importlib.resources
121
149
 
122
- # starts at {plugin_name: filepath, ...}, but changes at the end
123
- plugins = {}
124
150
  try:
125
151
  ploader = getattr(loader, '{0}_loader'.format(ptype))
126
152
  except AttributeError:
127
153
  raise AnsibleError(f"Cannot list plugins, incorrect plugin type {ptype!r} supplied.") from None
128
154
 
155
+ builtin_jinja_plugins = {}
156
+ plugin_paths = {}
157
+
129
158
  # get plugins for each collection
130
- for collection in collections.keys():
159
+ for collection, path in collections.items():
131
160
  if collection == 'ansible.builtin':
132
161
  # dirs from ansible install, but not configured paths
133
162
  dirs = [d.path for d in ploader._get_paths_with_context() if d.internal]
163
+
164
+ if ptype in ('filter', 'test'):
165
+ builtin_jinja_plugins = get_jinja_builtin_plugin_descriptions(ptype)
166
+
134
167
  elif collection == 'ansible.legacy':
135
168
  # configured paths + search paths (should include basedirs/-M)
136
169
  dirs = [d.path for d in ploader._get_paths_with_context() if not d.internal]
@@ -139,7 +172,7 @@ def list_collection_plugins(ptype, collections, search_paths=None):
139
172
  else:
140
173
  # search path in this case is for locating collection itselfA
141
174
  b_ptype = to_bytes(C.COLLECTION_PTYPE_COMPAT.get(ptype, ptype))
142
- dirs = [to_native(os.path.join(collections[collection], b'plugins', b_ptype))]
175
+ dirs = [to_native(os.path.join(path, b'plugins', b_ptype))]
143
176
  # acr = AnsibleCollectionRef.try_parse_fqcr(collection, ptype)
144
177
  # if acr:
145
178
  # dirs = acr.subdirs
@@ -147,30 +180,51 @@ def list_collection_plugins(ptype, collections, search_paths=None):
147
180
 
148
181
  # raise Exception('bad acr for %s, %s' % (collection, ptype))
149
182
 
150
- plugins.update(_list_plugins_from_paths(ptype, dirs, collection))
183
+ plugin_paths.update(_list_plugins_from_paths(ptype, dirs, collection, docs=True))
151
184
 
152
- # return plugin and it's class object, None for those not verifiable or failing
185
+ plugins = {}
153
186
  if ptype in ('module',):
154
187
  # no 'invalid' tests for modules
155
- for plugin in plugins.keys():
156
- plugins[plugin] = (plugins[plugin], None)
188
+ for plugin, plugin_path in plugin_paths.items():
189
+ plugins[plugin] = _PluginDocMetadata(name=plugin, path=plugin_path)
157
190
  else:
158
191
  # detect invalid plugin candidates AND add loaded object to return data
159
- for plugin in list(plugins.keys()):
192
+ for plugin, plugin_path in plugin_paths.items():
160
193
  pobj = None
161
194
  try:
162
195
  pobj = ploader.get(plugin, class_only=True)
163
196
  except Exception as e:
164
- display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype, plugins[plugin], to_native(e)))
197
+ display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype, plugin_path, to_native(e)))
165
198
 
166
- # sets final {plugin_name: (filepath, class|NONE if not loaded), ...}
167
- plugins[plugin] = (plugins[plugin], pobj)
199
+ plugins[plugin] = _PluginDocMetadata(
200
+ name=plugin,
201
+ path=plugin_path,
202
+ plugin_obj=pobj,
203
+ jinja_builtin_short_description=builtin_jinja_plugins.get(plugin),
204
+ )
205
+
206
+ # Add in any builtin Jinja2 plugins that have not been shadowed in Ansible.
207
+ plugins.update(
208
+ (plugin_name, _PluginDocMetadata(name=plugin_name, jinja_builtin_short_description=plugin_description))
209
+ for plugin_name, plugin_description in builtin_jinja_plugins.items() if plugin_name not in plugins
210
+ )
168
211
 
169
- # {plugin_name: (filepath, class), ...}
170
212
  return plugins
171
213
 
172
214
 
173
- def list_plugins(ptype, collections=None, search_paths=None):
215
+ def list_plugins(ptype: str, collections: list[str] | None = None, search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
216
+ # Kept for backwards compatibility.
217
+ return {
218
+ name: (info.path, info.plugin_obj)
219
+ for name, info in _list_plugins_with_info(ptype, collections, search_paths).items()
220
+ }
221
+
222
+
223
+ def _list_plugins_with_info(
224
+ ptype: str,
225
+ collections: list[str] = None,
226
+ search_paths: list[str] | None = None,
227
+ ) -> dict[str, _PluginDocMetadata]:
174
228
  if isinstance(collections, str):
175
229
  collections = [collections]
176
230
 
@@ -195,7 +249,7 @@ def list_plugins(ptype, collections=None, search_paths=None):
195
249
  raise AnsibleError(f"Cannot use supplied collection {collection!r}.") from ex
196
250
 
197
251
  if plugin_collections:
198
- plugins.update(list_collection_plugins(ptype, plugin_collections))
252
+ plugins.update(_list_collection_plugins_with_info(ptype, plugin_collections))
199
253
 
200
254
  return plugins
201
255
 
ansible/plugins/loader.py CHANGED
@@ -26,6 +26,7 @@ from ansible import __version__ as ansible_version
26
26
  from ansible import _internal, constants as C
27
27
  from ansible.errors import AnsibleError, AnsiblePluginCircularRedirect, AnsiblePluginRemovedError, AnsibleCollectionUnsupportedVersionError
28
28
  from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
29
+ from ansible.module_utils.datatag import deprecator_from_collection_name
29
30
  from ansible.module_utils.six import string_types
30
31
  from ansible.parsing.yaml.loader import AnsibleLoader
31
32
  from ansible._internal._yaml._loader import AnsibleInstrumentedLoader
@@ -40,7 +41,6 @@ from . import _AnsiblePluginInfoMixin
40
41
  from .filter import AnsibleJinja2Filter
41
42
  from .test import AnsibleJinja2Test
42
43
  from .._internal._plugins import _cache
43
- from ..module_utils.common.messages import PluginInfo
44
44
 
45
45
  # TODO: take the packaging dep, or vendor SpecifierSet?
46
46
 
@@ -202,7 +202,7 @@ class PluginLoadContext(object):
202
202
  msg=warning_text,
203
203
  date=removal_date,
204
204
  version=removal_version,
205
- deprecator=PluginInfo._from_collection_name(collection_name),
205
+ deprecator=deprecator_from_collection_name(collection_name),
206
206
  )
207
207
 
208
208
  self.deprecated = True
@@ -611,7 +611,7 @@ class PluginLoader:
611
611
  version=removal_version,
612
612
  date=removal_date,
613
613
  removed=True,
614
- deprecator=PluginInfo._from_collection_name(acr.collection),
614
+ deprecator=deprecator_from_collection_name(acr.collection),
615
615
  )
616
616
  plugin_load_context.date = removal_date
617
617
  plugin_load_context.version = removal_version
@@ -795,7 +795,7 @@ class PluginLoader:
795
795
  except Exception as ex:
796
796
  plugin_load_context.raw_error_list.append(ex)
797
797
 
798
- # DTFIX-RELEASE: can we deprecate/remove these stringified versions?
798
+ # DTFIX-FUTURE: can we deprecate/remove these stringified versions?
799
799
  if isinstance(ex, ImportError):
800
800
  plugin_load_context.import_error_list.append(ex)
801
801
  else:
@@ -955,7 +955,7 @@ class PluginLoader:
955
955
  redirected_names: list[str] | None = None,
956
956
  resolved: str | None = None,
957
957
  ) -> None:
958
- # DTFIX-RELEASE: clean this up- standardize types, document, split/remove redundant bits
958
+ # DTFIX-FUTURE: clean this up- standardize types, document, split/remove redundant bits
959
959
 
960
960
  # set extra info on the module, in case we want it later
961
961
  obj._original_path = path
@@ -1396,7 +1396,7 @@ class Jinja2Loader(PluginLoader):
1396
1396
  msg=warning_text,
1397
1397
  version=removal_version,
1398
1398
  date=removal_date,
1399
- deprecator=PluginInfo._from_collection_name(acr.collection),
1399
+ deprecator=deprecator_from_collection_name(acr.collection),
1400
1400
  )
1401
1401
 
1402
1402
  # check removal
@@ -1412,7 +1412,7 @@ class Jinja2Loader(PluginLoader):
1412
1412
  version=removal_version,
1413
1413
  date=removal_date,
1414
1414
  removed=True,
1415
- deprecator=PluginInfo._from_collection_name(acr.collection),
1415
+ deprecator=deprecator_from_collection_name(acr.collection),
1416
1416
  )
1417
1417
 
1418
1418
  raise AnsiblePluginRemovedError(exc_msg)
@@ -103,76 +103,26 @@ RETURN = """
103
103
  elements: str
104
104
  """
105
105
 
106
- import codecs
107
106
  import csv
108
107
 
109
108
  from collections.abc import MutableSequence
110
109
 
111
- from ansible.errors import AnsibleError, AnsibleAssertionError
110
+ from ansible.errors import AnsibleError
112
111
  from ansible.parsing.splitter import parse_kv
113
112
  from ansible.plugins.lookup import LookupBase
114
- from ansible.module_utils.six import PY2
115
- from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
116
-
117
-
118
- class CSVRecoder:
119
- """
120
- Iterator that reads an encoded stream and encodes the input to UTF-8
121
- """
122
- def __init__(self, f, encoding='utf-8'):
123
- self.reader = codecs.getreader(encoding)(f)
124
-
125
- def __iter__(self):
126
- return self
127
-
128
- def __next__(self):
129
- return next(self.reader).encode("utf-8")
130
-
131
- next = __next__ # For Python 2
132
-
133
-
134
- class CSVReader:
135
- """
136
- A CSV reader which will iterate over lines in the CSV file "f",
137
- which is encoded in the given encoding.
138
- """
139
-
140
- def __init__(self, f, dialect=csv.excel, encoding='utf-8', **kwds):
141
- if PY2:
142
- f = CSVRecoder(f, encoding)
143
- else:
144
- f = codecs.getreader(encoding)(f)
145
-
146
- self.reader = csv.reader(f, dialect=dialect, **kwds)
147
-
148
- def __next__(self):
149
- row = next(self.reader)
150
- return [to_text(s) for s in row]
151
-
152
- next = __next__ # For Python 2
153
-
154
- def __iter__(self):
155
- return self
156
113
 
157
114
 
158
115
  class LookupModule(LookupBase):
159
116
 
160
117
  def read_csv(self, filename, key, delimiter, encoding='utf-8', dflt=None, col=1, keycol=0):
161
-
162
- try:
163
- f = open(to_bytes(filename), 'rb')
164
- creader = CSVReader(f, delimiter=to_native(delimiter), encoding=encoding)
165
-
166
- for row in creader:
167
- if len(row) and row[keycol] == key:
118
+ with open(filename, encoding=encoding) as f:
119
+ for row in csv.reader(f, dialect=csv.excel, delimiter=delimiter):
120
+ if row and row[keycol] == key:
168
121
  return row[col]
169
- except Exception as e:
170
- raise AnsibleError("csvfile: %s" % to_native(e))
171
122
 
172
123
  return dflt
173
124
 
174
125
  def run(self, terms, variables=None, **kwargs):
175
-
176
126
  ret = []
177
127
 
178
128
  self.set_options(var_options=variables, direct=kwargs)
@@ -192,23 +142,19 @@ class LookupModule(LookupBase):
192
142
  key = kv['_raw_params']
193
143
 
194
144
  # parameters override per term using k/v
195
- try:
196
- reset_params = False
197
- for name, value in kv.items():
198
- if name == '_raw_params':
199
- continue
200
- if name not in paramvals:
201
- raise AnsibleAssertionError('%s is not a valid option' % name)
202
-
203
- self._deprecate_inline_kv()
204
- self.set_option(name, value)
205
- reset_params = True
145
+ reset_params = False
146
+ for name, value in kv.items():
147
+ if name == '_raw_params':
148
+ continue
149
+ if name not in paramvals:
150
+ raise ValueError(f'{name!r} is not a valid option')
206
151
 
207
- if reset_params:
208
- paramvals = self.get_options()
152
+ self._deprecate_inline_kv()
153
+ self.set_option(name, value)
154
+ reset_params = True
209
155
 
210
- except (ValueError, AssertionError) as e:
211
- raise AnsibleError(e)
156
+ if reset_params:
157
+ paramvals = self.get_options()
212
158
 
213
159
  # default is just placeholder for real tab
214
160
  if paramvals['delimiter'] == 'TAB':
@@ -218,8 +164,7 @@ class LookupModule(LookupBase):
218
164
  var = self.read_csv(lookupfile, key, paramvals['delimiter'], paramvals['encoding'], paramvals['default'], paramvals['col'], paramvals['keycol'])
219
165
  if var is not None:
220
166
  if isinstance(var, MutableSequence):
221
- for v in var:
222
- ret.append(v)
167
+ ret.extend(var)
223
168
  else:
224
169
  ret.append(var)
225
170
 
@@ -149,6 +149,7 @@ from ansible.errors import AnsibleError
149
149
  from ansible.plugins.lookup import LookupBase
150
150
  from ansible._internal._templating import _jinja_common
151
151
  from ansible._internal._templating import _jinja_plugins
152
+ from ansible import template as _template
152
153
  from ansible.utils.path import unfrackpath
153
154
  from ansible.utils.display import Display
154
155
  from ansible.module_utils.datatag import native_type_name
@@ -222,7 +223,7 @@ class LookupModule(LookupBase):
222
223
  return total_search
223
224
 
224
225
  def run(self, terms: list, variables=None, **kwargs):
225
- if (first_marker := _jinja_common.get_first_marker_arg((), kwargs)) is not None:
226
+ if (first_marker := _template.get_first_marker_arg((), kwargs)) is not None:
226
227
  first_marker.trip()
227
228
 
228
229
  if _jinja_plugins._LookupContext.current().invoked_as_with: