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
@@ -280,8 +280,6 @@ class CallbackBase(AnsiblePlugin):
280
280
  # that want to further modify the result, or use custom serialization
281
281
  return abridged_result
282
282
 
283
- # DTFIX-RELEASE: Switch to stock json/yaml serializers here? We should always have a transformed plain-types result.
284
-
285
283
  if result_format == 'json':
286
284
  return json.dumps(abridged_result, cls=_fallback_to_str.Encoder, indent=indent, ensure_ascii=False, sort_keys=sort_keys)
287
285
 
@@ -310,7 +308,7 @@ class CallbackBase(AnsiblePlugin):
310
308
  ' ' * (indent or 4)
311
309
  )
312
310
 
313
- # DTFIX-RELEASE: add test to exercise this case
311
+ # DTFIX5: add test to exercise this case
314
312
  raise ValueError(f'Unsupported result_format {result_format!r}.')
315
313
 
316
314
  def _handle_warnings(self, res: _c.MutableMapping[str, t.Any]) -> None:
@@ -318,7 +316,7 @@ class CallbackBase(AnsiblePlugin):
318
316
  if res.pop('warnings', None) and self._current_task_result and (warnings := self._current_task_result.warnings):
319
317
  # display warnings from the current task result if `warnings` was not removed from `result` (or made falsey)
320
318
  for warning in warnings:
321
- # DTFIX-RELEASE: what to do about propagating wrap_text from the original display.warning call?
319
+ # DTFIX3: what to do about propagating wrap_text from the original display.warning call?
322
320
  self._display._warning(warning, wrap_text=False)
323
321
 
324
322
  if res.pop('deprecations', None) and self._current_task_result and (deprecations := self._current_task_result.deprecations):
@@ -333,7 +331,7 @@ class CallbackBase(AnsiblePlugin):
333
331
 
334
332
  def _handle_warnings_and_exception(self, result: CallbackTaskResult) -> None:
335
333
  """Standardized handling of warnings/deprecations and exceptions from a task/item result."""
336
- # DTFIX-RELEASE: make/doc/porting-guide a public version of this method?
334
+ # DTFIX5: make/doc/porting-guide a public version of this method?
337
335
  try:
338
336
  use_stderr = self.get_option('display_failed_stderr')
339
337
  except KeyError:
@@ -374,7 +372,7 @@ class CallbackBase(AnsiblePlugin):
374
372
  ' '
375
373
  )
376
374
 
377
- # DTFIX-RELEASE: add test to exercise this case
375
+ # DTFIX5: add test to exercise this case
378
376
  raise ValueError(f'Unsupported result_format {result_format!r}.')
379
377
 
380
378
  def _get_diff(self, difflist):
@@ -90,6 +90,8 @@ import typing as t
90
90
 
91
91
  from ansible import constants
92
92
  from ansible.module_utils.common.text.converters import to_bytes, to_text
93
+ from ansible.module_utils._internal import _event_utils
94
+ from ansible._internal import _event_formatting
93
95
  from ansible.playbook.task import Task
94
96
  from ansible.plugins.callback import CallbackBase
95
97
  from ansible.executor.task_result import CallbackTaskResult
@@ -248,8 +250,8 @@ class CallbackModule(CallbackBase):
248
250
 
249
251
  if host_data.status == 'failed':
250
252
  if error_summary := task_result.exception:
251
- message = error_summary._format()
252
- output = error_summary.formatted_traceback
253
+ message = _event_utils.format_event_brief_message(error_summary.event)
254
+ output = _event_formatting.format_event_traceback(error_summary.event)
253
255
  test_case.errors.append(TestError(message=message, output=output))
254
256
  elif 'msg' in res:
255
257
  message = res['msg']
@@ -101,9 +101,9 @@ class Connection(ConnectionBase):
101
101
  display.debug("opening command with Popen()")
102
102
 
103
103
  if isinstance(cmd, (text_type, binary_type)):
104
- cmd = to_bytes(cmd)
104
+ cmd = to_text(cmd)
105
105
  else:
106
- cmd = map(to_bytes, cmd)
106
+ cmd = map(to_text, cmd)
107
107
 
108
108
  pty_primary = None
109
109
  stdin = subprocess.PIPE
@@ -398,6 +398,17 @@ DOCUMENTATION = """
398
398
  - {key: pkcs11_provider, section: ssh_connection}
399
399
  vars:
400
400
  - name: ansible_ssh_pkcs11_provider
401
+ verbosity:
402
+ version_added: '2.19'
403
+ default: 0
404
+ type: int
405
+ description:
406
+ - Requested verbosity level for the SSH CLI.
407
+ env: [{name: ANSIBLE_SSH_VERBOSITY}]
408
+ ini:
409
+ - {key: verbosity, section: ssh_connection}
410
+ vars:
411
+ - name: ansible_ssh_verbosity
401
412
  """
402
413
 
403
414
  import collections.abc as c
@@ -436,7 +447,7 @@ from ansible.plugins.connection import ConnectionBase, BUFSIZE
436
447
  from ansible.plugins.shell.powershell import _replace_stderr_clixml
437
448
  from ansible.utils.display import Display
438
449
  from ansible.utils.path import unfrackpath, makedirs_safe
439
- from ansible.utils._ssh_agent import SshAgentClient, _key_data_into_crypto_objects
450
+ from ansible._internal._ssh import _ssh_agent
440
451
 
441
452
  try:
442
453
  from cryptography.hazmat.primitives import serialization
@@ -755,12 +766,12 @@ class Connection(ConnectionBase):
755
766
  key_data = self.get_option('private_key')
756
767
  passphrase = self.get_option('private_key_passphrase')
757
768
 
758
- private_key, public_key, fingerprint = _key_data_into_crypto_objects(
769
+ private_key, public_key, fingerprint = _ssh_agent.key_data_into_crypto_objects(
759
770
  to_bytes(key_data),
760
771
  to_bytes(passphrase) if passphrase else None,
761
772
  )
762
773
 
763
- with SshAgentClient(auth_sock) as client:
774
+ with _ssh_agent.SshAgentClient(auth_sock) as client:
764
775
  if public_key not in client:
765
776
  display.vvv(f'SSH: SSH_AGENT adding {fingerprint} to agent', host=self.host)
766
777
  client.add(
@@ -855,8 +866,8 @@ class Connection(ConnectionBase):
855
866
  self._add_args(b_command, b_args, u'disable batch mode for password auth')
856
867
  b_command += [b'-b', b'-']
857
868
 
858
- if display.verbosity:
859
- b_command.append(b'-' + (b'v' * display.verbosity))
869
+ if (verbosity := self.get_option('verbosity')) > 0:
870
+ b_command.append(b'-' + (b'v' * verbosity))
860
871
 
861
872
  # Next, we add ssh_args
862
873
  ssh_args = self.get_option('ssh_args')
@@ -879,10 +890,7 @@ class Connection(ConnectionBase):
879
890
  try:
880
891
  key = self._populate_agent()
881
892
  except Exception as e:
882
- raise AnsibleAuthenticationFailure(
883
- 'Failed to add configured private key into ssh-agent',
884
- orig_exc=e,
885
- )
893
+ raise AnsibleAuthenticationFailure('Failed to add configured private key into ssh-agent.') from e
886
894
  b_args = (b'-o', b'IdentitiesOnly=yes', b'-o', to_bytes(f'IdentityFile="{key}"', errors='surrogate_or_strict'))
887
895
  self._add_args(b_command, b_args, "ANSIBLE_PRIVATE_KEY/private_key set")
888
896
  elif key := self.get_option('private_key_file'):
@@ -723,8 +723,11 @@ class Connection(ConnectionBase):
723
723
  super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
724
724
 
725
725
  encoded_prefix = self._shell._encode_script('', as_list=False, strict_mode=False, preserve_rc=False)
726
- if cmd.startswith(encoded_prefix):
727
- # Avoid double encoding the script
726
+ if cmd.startswith(encoded_prefix) or cmd.startswith("type "):
727
+ # Avoid double encoding the script, the first means we are already
728
+ # running the standard PowerShell command, the latter is used for
729
+ # the no pipeline case where it uses type to pipe the script into
730
+ # powershell which is known to work without re-encoding as pwsh.
728
731
  cmd_parts = cmd.split(" ")
729
732
  else:
730
733
  cmd_parts = self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)
@@ -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
@@ -34,7 +34,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
34
34
  from ansible.module_utils.common.collections import is_sequence
35
35
  from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
36
36
  from ansible.parsing.yaml.dumper import AnsibleDumper
37
- from ansible.plugins import accept_args_markers, accept_lazy_markers
37
+ from ansible.template import accept_args_markers, accept_lazy_markers
38
38
  from ansible._internal._templating._jinja_common import MarkerError, UndefinedMarker, validate_arg_type
39
39
  from ansible.utils.display import Display
40
40
  from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
@@ -247,20 +247,30 @@ def regex_escape(string, re_type='python'):
247
247
 
248
248
 
249
249
  def from_yaml(data):
250
+ if data is None:
251
+ return None
252
+
250
253
  if isinstance(data, string_types):
251
254
  # The ``text_type`` call here strips any custom
252
255
  # string wrapper class, so that CSafeLoader can
253
256
  # read the data
254
257
  return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
258
+
259
+ display.deprecated(f"The from_yaml filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
255
260
  return data
256
261
 
257
262
 
258
263
  def from_yaml_all(data):
264
+ if data is None:
265
+ return [] # backward compatibility; ensure consistent result between classic/native Jinja for None/empty string input
266
+
259
267
  if isinstance(data, string_types):
260
268
  # The ``text_type`` call here strips any custom
261
269
  # string wrapper class, so that CSafeLoader can
262
270
  # read the data
263
271
  return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
272
+
273
+ 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
274
  return data
265
275
 
266
276
 
@@ -338,7 +348,7 @@ def to_uuid(string, namespace=UUID_NAMESPACE_ANSIBLE):
338
348
  @accept_args_markers
339
349
  def mandatory(a: object, msg: str | None = None) -> object:
340
350
  """Make a variable mandatory."""
341
- # DTFIX-RELEASE: deprecate this filter; there are much better ways via undef, etc...
351
+ # DTFIX-FUTURE: deprecate this filter; there are much better ways via undef, etc...
342
352
  # also remember to remove unit test checking for _undefined_name
343
353
  if isinstance(a, UndefinedMarker):
344
354
  if msg is not None:
@@ -654,7 +664,7 @@ def _cleansed_groupby(*args, **kwargs):
654
664
 
655
665
  return res
656
666
 
657
- # DTFIX-RELEASE: make these dumb wrappers more dynamic
667
+ # DTFIX-FUTURE: make these dumb wrappers more dynamic
658
668
 
659
669
 
660
670
  @accept_args_markers
@@ -806,7 +816,6 @@ class FilterModule(object):
806
816
  'groupby': _cleansed_groupby,
807
817
 
808
818
  # 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
819
  'd': ansible_default, # replaces the implementation instead of wrapping it
811
820
  'default': ansible_default, # replaces the implementation instead of wrapping it
812
821
  'map': wrapped_map,
@@ -815,5 +824,3 @@ class FilterModule(object):
815
824
  'reject': wrapped_reject,
816
825
  'rejectattr': wrapped_rejectattr,
817
826
  }
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()
@@ -43,12 +43,12 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
43
43
  return vault
44
44
 
45
45
 
46
- @accept_args_markers
46
+ @_template.accept_args_markers
47
47
  def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
48
48
  if isinstance(vault, VaultExceptionMarker):
49
49
  vault = vault._disarm()
50
50
 
51
- if (first_marker := get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
51
+ if (first_marker := _template.get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
52
52
  return first_marker
53
53
 
54
54
  if not isinstance(secret, (str, bytes)):
@@ -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:
@@ -116,21 +132,37 @@ def _list_j2_plugins_from_file(collection, plugin_path, ptype, plugin_name):
116
132
  return file_plugins
117
133
 
118
134
 
119
- def list_collection_plugins(ptype, collections, search_paths=None):
135
+ def list_collection_plugins(ptype: str, collections: dict[str, bytes], search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
136
+ # Kept for backwards compatibility.
137
+ return {
138
+ name: (info.path, info.plugin_obj)
139
+ for name, info in _list_collection_plugins_with_info(ptype, collections).items()
140
+ }
141
+
142
+
143
+ def _list_collection_plugins_with_info(
144
+ ptype: str,
145
+ collections: dict[str, bytes],
146
+ ) -> dict[str, _PluginDocMetadata]:
120
147
  # TODO: update to use importlib.resources
121
148
 
122
- # starts at {plugin_name: filepath, ...}, but changes at the end
123
- plugins = {}
124
149
  try:
125
150
  ploader = getattr(loader, '{0}_loader'.format(ptype))
126
151
  except AttributeError:
127
152
  raise AnsibleError(f"Cannot list plugins, incorrect plugin type {ptype!r} supplied.") from None
128
153
 
154
+ builtin_jinja_plugins = {}
155
+ plugin_paths = {}
156
+
129
157
  # get plugins for each collection
130
- for collection in collections.keys():
158
+ for collection, path in collections.items():
131
159
  if collection == 'ansible.builtin':
132
160
  # dirs from ansible install, but not configured paths
133
161
  dirs = [d.path for d in ploader._get_paths_with_context() if d.internal]
162
+
163
+ if ptype in ('filter', 'test'):
164
+ builtin_jinja_plugins = get_jinja_builtin_plugin_descriptions(ptype)
165
+
134
166
  elif collection == 'ansible.legacy':
135
167
  # configured paths + search paths (should include basedirs/-M)
136
168
  dirs = [d.path for d in ploader._get_paths_with_context() if not d.internal]
@@ -139,7 +171,7 @@ def list_collection_plugins(ptype, collections, search_paths=None):
139
171
  else:
140
172
  # search path in this case is for locating collection itselfA
141
173
  b_ptype = to_bytes(C.COLLECTION_PTYPE_COMPAT.get(ptype, ptype))
142
- dirs = [to_native(os.path.join(collections[collection], b'plugins', b_ptype))]
174
+ dirs = [to_native(os.path.join(path, b'plugins', b_ptype))]
143
175
  # acr = AnsibleCollectionRef.try_parse_fqcr(collection, ptype)
144
176
  # if acr:
145
177
  # dirs = acr.subdirs
@@ -147,30 +179,51 @@ def list_collection_plugins(ptype, collections, search_paths=None):
147
179
 
148
180
  # raise Exception('bad acr for %s, %s' % (collection, ptype))
149
181
 
150
- plugins.update(_list_plugins_from_paths(ptype, dirs, collection))
182
+ plugin_paths.update(_list_plugins_from_paths(ptype, dirs, collection))
151
183
 
152
- # return plugin and it's class object, None for those not verifiable or failing
184
+ plugins = {}
153
185
  if ptype in ('module',):
154
186
  # no 'invalid' tests for modules
155
- for plugin in plugins.keys():
156
- plugins[plugin] = (plugins[plugin], None)
187
+ for plugin, plugin_path in plugin_paths.items():
188
+ plugins[plugin] = _PluginDocMetadata(name=plugin, path=plugin_path)
157
189
  else:
158
190
  # detect invalid plugin candidates AND add loaded object to return data
159
- for plugin in list(plugins.keys()):
191
+ for plugin, plugin_path in plugin_paths.items():
160
192
  pobj = None
161
193
  try:
162
194
  pobj = ploader.get(plugin, class_only=True)
163
195
  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)))
196
+ display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype, plugin_path, to_native(e)))
165
197
 
166
- # sets final {plugin_name: (filepath, class|NONE if not loaded), ...}
167
- plugins[plugin] = (plugins[plugin], pobj)
198
+ plugins[plugin] = _PluginDocMetadata(
199
+ name=plugin,
200
+ path=plugin_path,
201
+ plugin_obj=pobj,
202
+ jinja_builtin_short_description=builtin_jinja_plugins.get(plugin),
203
+ )
204
+
205
+ # Add in any builtin Jinja2 plugins that have not been shadowed in Ansible.
206
+ plugins.update(
207
+ (plugin_name, _PluginDocMetadata(name=plugin_name, jinja_builtin_short_description=plugin_description))
208
+ for plugin_name, plugin_description in builtin_jinja_plugins.items() if plugin_name not in plugins
209
+ )
168
210
 
169
- # {plugin_name: (filepath, class), ...}
170
211
  return plugins
171
212
 
172
213
 
173
- def list_plugins(ptype, collections=None, search_paths=None):
214
+ def list_plugins(ptype: str, collections: list[str] | None = None, search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
215
+ # Kept for backwards compatibility.
216
+ return {
217
+ name: (info.path, info.plugin_obj)
218
+ for name, info in _list_plugins_with_info(ptype, collections, search_paths).items()
219
+ }
220
+
221
+
222
+ def _list_plugins_with_info(
223
+ ptype: str,
224
+ collections: list[str] = None,
225
+ search_paths: list[str] | None = None,
226
+ ) -> dict[str, _PluginDocMetadata]:
174
227
  if isinstance(collections, str):
175
228
  collections = [collections]
176
229
 
@@ -195,7 +248,7 @@ def list_plugins(ptype, collections=None, search_paths=None):
195
248
  raise AnsibleError(f"Cannot use supplied collection {collection!r}.") from ex
196
249
 
197
250
  if plugin_collections:
198
- plugins.update(list_collection_plugins(ptype, plugin_collections))
251
+ plugins.update(_list_collection_plugins_with_info(ptype, plugin_collections))
199
252
 
200
253
  return plugins
201
254
 
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
@@ -505,7 +505,8 @@ class PluginLoader:
505
505
 
506
506
  # if type name != 'module_doc_fragment':
507
507
  if type_name in C.CONFIGURABLE_PLUGINS and not C.config.has_configuration_definition(type_name, name):
508
- documentation_source = getattr(module, 'DOCUMENTATION', '')
508
+ # trust-tagged source propagates to loaded values; expressions and templates in config require trust
509
+ documentation_source = _tags.TrustedAsTemplate().tag(getattr(module, 'DOCUMENTATION', ''))
509
510
  try:
510
511
  dstring = yaml.load(_tags.Origin(path=path).tag(documentation_source), Loader=AnsibleLoader)
511
512
  except ParserError as e:
@@ -610,7 +611,7 @@ class PluginLoader:
610
611
  version=removal_version,
611
612
  date=removal_date,
612
613
  removed=True,
613
- deprecator=PluginInfo._from_collection_name(acr.collection),
614
+ deprecator=deprecator_from_collection_name(acr.collection),
614
615
  )
615
616
  plugin_load_context.date = removal_date
616
617
  plugin_load_context.version = removal_version
@@ -673,7 +674,7 @@ class PluginLoader:
673
674
  # look for any matching extension in the package location (sans filter)
674
675
  found_files = [f
675
676
  for f in glob.iglob(os.path.join(pkg_path, n_resource) + '.*')
676
- if os.path.isfile(f) and not f.endswith(C.MODULE_IGNORE_EXTS)]
677
+ if os.path.isfile(f) and not any(f.endswith(ext) for ext in C.MODULE_IGNORE_EXTS)]
677
678
 
678
679
  if not found_files:
679
680
  return plugin_load_context.nope('failed fuzzy extension match for {0} in {1}'.format(full_name, acr.collection))
@@ -794,7 +795,7 @@ class PluginLoader:
794
795
  except Exception as ex:
795
796
  plugin_load_context.raw_error_list.append(ex)
796
797
 
797
- # DTFIX-RELEASE: can we deprecate/remove these stringified versions?
798
+ # DTFIX-FUTURE: can we deprecate/remove these stringified versions?
798
799
  if isinstance(ex, ImportError):
799
800
  plugin_load_context.import_error_list.append(ex)
800
801
  else:
@@ -954,7 +955,7 @@ class PluginLoader:
954
955
  redirected_names: list[str] | None = None,
955
956
  resolved: str | None = None,
956
957
  ) -> None:
957
- # 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
958
959
 
959
960
  # set extra info on the module, in case we want it later
960
961
  obj._original_path = path
@@ -1395,7 +1396,7 @@ class Jinja2Loader(PluginLoader):
1395
1396
  msg=warning_text,
1396
1397
  version=removal_version,
1397
1398
  date=removal_date,
1398
- deprecator=PluginInfo._from_collection_name(acr.collection),
1399
+ deprecator=deprecator_from_collection_name(acr.collection),
1399
1400
  )
1400
1401
 
1401
1402
  # check removal
@@ -1411,7 +1412,7 @@ class Jinja2Loader(PluginLoader):
1411
1412
  version=removal_version,
1412
1413
  date=removal_date,
1413
1414
  removed=True,
1414
- deprecator=PluginInfo._from_collection_name(acr.collection),
1415
+ deprecator=deprecator_from_collection_name(acr.collection),
1415
1416
  )
1416
1417
 
1417
1418
  raise AnsiblePluginRemovedError(exc_msg)