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
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ import typing as _t
4
+
5
+ from ansible.module_utils._internal import _text_utils, _messages
6
+
7
+
8
+ def deduplicate_message_parts(message_parts: list[str]) -> str:
9
+ """Format the given list of messages into a brief message, while deduplicating repeated elements."""
10
+ message_parts = list(reversed(message_parts))
11
+
12
+ message = message_parts.pop(0)
13
+
14
+ for message_part in message_parts:
15
+ # avoid duplicate messages where the cause was already concatenated to the exception message
16
+ if message_part.endswith(message):
17
+ message = message_part
18
+ else:
19
+ message = _text_utils.concat_message(message_part, message)
20
+
21
+ return message
22
+
23
+
24
+ def format_event_brief_message(event: _messages.Event) -> str:
25
+ """
26
+ Format an event into a brief message.
27
+ Help text, contextual information and sub-events will be omitted.
28
+ """
29
+ message_parts: list[str] = []
30
+
31
+ while True:
32
+ message_parts.append(event.msg)
33
+
34
+ if not event.chain or not event.chain.follow:
35
+ break
36
+
37
+ event = event.chain.event
38
+
39
+ return deduplicate_message_parts(message_parts)
40
+
41
+
42
+ def deprecation_as_dict(deprecation: _messages.DeprecationSummary) -> _t.Dict[str, _t.Any]:
43
+ """Returns a dictionary representation of the deprecation object in the format exposed to playbooks."""
44
+ from ansible.module_utils._internal._deprecator import INDETERMINATE_DEPRECATOR # circular import from messages
45
+
46
+ if deprecation.deprecator and deprecation.deprecator != INDETERMINATE_DEPRECATOR:
47
+ collection_name = '.'.join(deprecation.deprecator.resolved_name.split('.')[:2])
48
+ else:
49
+ collection_name = None
50
+
51
+ result = dict(
52
+ msg=format_event_brief_message(deprecation.event),
53
+ collection_name=collection_name,
54
+ )
55
+
56
+ if deprecation.date:
57
+ result.update(date=deprecation.date)
58
+ else:
59
+ result.update(version=deprecation.version)
60
+
61
+ return result
@@ -6,7 +6,7 @@ import json
6
6
  import typing as t
7
7
 
8
8
  from ansible.module_utils import _internal
9
- from ansible.module_utils.common import messages as _messages
9
+ from ansible.module_utils._internal import _messages
10
10
  from ansible.module_utils._internal._datatag import (
11
11
  AnsibleSerializable,
12
12
  AnsibleSerializableWrapper,
@@ -87,7 +87,9 @@ For controller-to-module, type behavior is profile dependent.
87
87
  _common_module_response_types: frozenset[type[AnsibleSerializable]] = frozenset(
88
88
  {
89
89
  _messages.PluginInfo,
90
- _messages.Detail,
90
+ _messages.PluginType,
91
+ _messages.Event,
92
+ _messages.EventChain,
91
93
  _messages.ErrorSummary,
92
94
  _messages.WarningSummary,
93
95
  _messages.DeprecationSummary,
@@ -203,11 +205,27 @@ class _JSONSerializationProfile(t.Generic[_T_encoder, _T_decoder]):
203
205
 
204
206
  @classmethod
205
207
  def handle_key(cls, k: t.Any) -> t.Any:
208
+ """Validation/conversion hook before a dict key is serialized. The default implementation only accepts str-typed keys."""
209
+ # NOTE: Since JSON requires string keys, there is no support for preserving tags on dictionary keys during serialization.
210
+
206
211
  if not isinstance(k, str): # DTFIX-FUTURE: optimize this to use all known str-derived types in type map / allowed types
207
212
  raise TypeError(f'Key of type {type(k).__name__!r} is not JSON serializable by the {cls.profile_name!r} profile.')
208
213
 
209
214
  return k
210
215
 
216
+ @classmethod
217
+ def _handle_key_str_fallback(cls, k: t.Any) -> t.Any:
218
+ """Legacy implementations should use this key handler for backward compatibility with stdlib JSON key conversion quirks."""
219
+ # DTFIX-FUTURE: optimized exact-type table lookup first
220
+
221
+ if isinstance(k, str):
222
+ return k
223
+
224
+ if k is None or isinstance(k, (int, float)):
225
+ return json.dumps(k)
226
+
227
+ raise TypeError(f'Key of type {type(k).__name__!r} is not JSON serializable by the {cls.profile_name!r} profile.')
228
+
211
229
  @classmethod
212
230
  def default(cls, o: t.Any) -> t.Any:
213
231
  # Preserve the built-in JSON encoder support for subclasses of scalar types.
@@ -373,8 +391,8 @@ Future code changes should further restrict bytes to string conversions to elimi
373
391
  Additional warnings at other boundaries may be needed to give users an opportunity to resolve the issues before they become errors.
374
392
  """
375
393
  # DTFIX-FUTURE: add strict UTF8 string encoding checking to serialization profiles (to match the checks performed during deserialization)
376
- # DTFIX-RELEASE: the surrogateescape note above isn't quite right, for encoding use surrogatepass, which does work
377
- # DTFIX-RELEASE: this config setting should probably be deprecated
394
+ # DTFIX3: the surrogateescape note above isn't quite right, for encoding use surrogatepass, which does work
395
+ # DTFIX-FUTURE: this config setting should probably be deprecated
378
396
 
379
397
 
380
398
  def _create_encoding_check_error() -> Exception:
@@ -22,6 +22,8 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
22
22
  }
23
23
  )
24
24
 
25
+ cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
26
+
25
27
 
26
28
  class Encoder(_profiles.AnsibleProfileJSONEncoder):
27
29
  _profile = _Profile
@@ -26,6 +26,8 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
26
26
  _datetime.datetime: cls.serialize_as_isoformat, # legacy _json_encode_fallback behavior *and* legacy parameters.py does this before serialization
27
27
  }
28
28
 
29
+ cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
30
+
29
31
 
30
32
  class Encoder(_profiles.AnsibleProfileJSONEncoder):
31
33
  _profile = _Profile
@@ -17,7 +17,7 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
17
17
  @classmethod
18
18
  def post_init(cls) -> None:
19
19
  cls.serialize_map = {
20
- # DTFIX-RELEASE: support serialization of every type that is supported in the Ansible variable type system
20
+ # DTFIX5: support serialization of every type that is supported in the Ansible variable type system
21
21
  set: cls.serialize_as_list,
22
22
  tuple: cls.serialize_as_list,
23
23
  _datetime.date: cls.serialize_as_isoformat,
@@ -41,6 +41,8 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
41
41
  '__ansible_vault': _functools.partial(cls.unsupported_target_type_error, '__ansible_vault'),
42
42
  }
43
43
 
44
+ cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
45
+
44
46
 
45
47
  class Encoder(_profiles.AnsibleProfileJSONEncoder):
46
48
  _profile = _Profile
@@ -7,13 +7,12 @@ A future release will remove the provisional status.
7
7
 
8
8
  from __future__ import annotations as _annotations
9
9
 
10
- import sys as _sys
11
10
  import dataclasses as _dataclasses
11
+ import enum as _enum
12
+ import sys as _sys
13
+ import typing as _t
12
14
 
13
- # deprecated: description='typing.Self exists in Python 3.11+' python_version='3.10'
14
- from ..compat import typing as _t
15
-
16
- from ansible.module_utils._internal import _datatag, _validation
15
+ from ansible.module_utils._internal import _datatag, _dataclass_validation
17
16
 
18
17
  if _sys.version_info >= (3, 10):
19
18
  # Using slots for reduced memory usage and improved performance.
@@ -23,56 +22,79 @@ else:
23
22
  _dataclass_kwargs = dict(frozen=True)
24
23
 
25
24
 
25
+ class PluginType(_datatag.AnsibleSerializableEnum):
26
+ """Enum of Ansible plugin types."""
27
+
28
+ ACTION = _enum.auto()
29
+ BECOME = _enum.auto()
30
+ CACHE = _enum.auto()
31
+ CALLBACK = _enum.auto()
32
+ CLICONF = _enum.auto()
33
+ CONNECTION = _enum.auto()
34
+ DOC_FRAGMENTS = _enum.auto()
35
+ FILTER = _enum.auto()
36
+ HTTPAPI = _enum.auto()
37
+ INVENTORY = _enum.auto()
38
+ LOOKUP = _enum.auto()
39
+ MODULE = _enum.auto()
40
+ NETCONF = _enum.auto()
41
+ SHELL = _enum.auto()
42
+ STRATEGY = _enum.auto()
43
+ TERMINAL = _enum.auto()
44
+ TEST = _enum.auto()
45
+ VARS = _enum.auto()
46
+
47
+
26
48
  @_dataclasses.dataclass(**_dataclass_kwargs)
27
49
  class PluginInfo(_datatag.AnsibleSerializableDataclass):
28
50
  """Information about a loaded plugin."""
29
51
 
30
- resolved_name: str
52
+ resolved_name: _t.Optional[str]
31
53
  """The resolved canonical plugin name; always fully-qualified for collection plugins."""
32
- type: str
54
+
55
+ type: _t.Optional[PluginType]
33
56
  """The plugin type."""
34
57
 
35
- _COLLECTION_ONLY_TYPE: _t.ClassVar[str] = 'collection'
36
- """This is not a real plugin type. It's a placeholder for use by a `PluginInfo` instance which references a collection without a plugin."""
37
58
 
38
- @classmethod
39
- def _from_collection_name(cls, collection_name: str | None) -> _t.Self | None:
40
- """Returns an instance with the special `collection` type to refer to a non-plugin or ambiguous caller within a collection."""
41
- if not collection_name:
42
- return None
59
+ @_dataclasses.dataclass(**_dataclass_kwargs)
60
+ class EventChain(_datatag.AnsibleSerializableDataclass):
61
+ """A chain used to link one event to another."""
62
+
63
+ _validation_auto_enabled = False
43
64
 
44
- _validation.validate_collection_name(collection_name)
65
+ def __post_init__(self): ... # required for deferred dataclass validation
45
66
 
46
- return cls(
47
- resolved_name=collection_name,
48
- type=cls._COLLECTION_ONLY_TYPE,
49
- )
67
+ msg_reason: str
68
+ traceback_reason: str
69
+ event: Event
70
+ follow: bool = True
50
71
 
51
72
 
52
73
  @_dataclasses.dataclass(**_dataclass_kwargs)
53
- class Detail(_datatag.AnsibleSerializableDataclass):
54
- """Message detail with optional source context and help text."""
74
+ class Event(_datatag.AnsibleSerializableDataclass):
75
+ """Base class for an error/warning/deprecation event with optional chain (from an exception __cause__ chain) and an optional traceback."""
76
+
77
+ _validation_auto_enabled = False
78
+
79
+ def __post_init__(self): ... # required for deferred dataclass validation
55
80
 
56
81
  msg: str
57
82
  formatted_source_context: _t.Optional[str] = None
83
+ formatted_traceback: _t.Optional[str] = None
58
84
  help_text: _t.Optional[str] = None
85
+ chain: _t.Optional[EventChain] = None
86
+ events: _t.Optional[_t.Tuple[Event, ...]] = None
87
+
88
+
89
+ _dataclass_validation.inject_post_init_validation(EventChain, EventChain._validation_allow_subclasses)
90
+ _dataclass_validation.inject_post_init_validation(Event, Event._validation_allow_subclasses)
59
91
 
60
92
 
61
93
  @_dataclasses.dataclass(**_dataclass_kwargs)
62
94
  class SummaryBase(_datatag.AnsibleSerializableDataclass):
63
95
  """Base class for an error/warning/deprecation summary with details (possibly derived from an exception __cause__ chain) and an optional traceback."""
64
96
 
65
- details: _t.Tuple[Detail, ...]
66
- formatted_traceback: _t.Optional[str] = None
67
-
68
- def _format(self) -> str:
69
- """Returns a string representation of the details."""
70
- # DTFIX-RELEASE: eliminate this function and use a common message squashing utility such as get_chained_message on instances of this type
71
- return ': '.join(detail.msg for detail in self.details)
72
-
73
- def _post_validate(self) -> None:
74
- if not self.details:
75
- raise ValueError(f'{type(self).__name__}.details cannot be empty')
97
+ event: Event
76
98
 
77
99
 
78
100
  @_dataclasses.dataclass(**_dataclass_kwargs)
@@ -106,20 +128,3 @@ class DeprecationSummary(WarningSummary):
106
128
  Ignored if `deprecator` is not provided.
107
129
  Ignored if `date` is provided.
108
130
  """
109
-
110
- def _as_simple_dict(self) -> _t.Dict[str, _t.Any]:
111
- """Returns a dictionary representation of the deprecation object in the format exposed to playbooks."""
112
- from ansible.module_utils._internal._deprecator import INDETERMINATE_DEPRECATOR # circular import from messages
113
-
114
- if self.deprecator and self.deprecator != INDETERMINATE_DEPRECATOR:
115
- collection_name = '.'.join(self.deprecator.resolved_name.split('.')[:2])
116
- else:
117
- collection_name = None
118
-
119
- result = self._as_dict()
120
- result.update(
121
- msg=self._format(),
122
- collection_name=collection_name,
123
- )
124
-
125
- return result
@@ -1,7 +1,5 @@
1
1
  """Patches for builtin `dataclasses` module."""
2
2
 
3
- # deprecated: description='verify ClassVar support in dataclasses has been fixed in Python before removing this patching code', python_version='3.13'
4
-
5
3
  from __future__ import annotations
6
4
 
7
5
  import dataclasses
@@ -26,7 +24,7 @@ class DataclassesIsTypePatch(CallablePatch):
26
24
  @dataclasses.dataclass
27
25
  class CheckClassVar:
28
26
  # this is the broken case requiring patching: ClassVar dot-referenced from a module that is not `typing` is treated as an instance field
29
- # DTFIX-RELEASE: add link to CPython bug report to-be-filed (or update associated deprecation comments if we don't)
27
+ # DTFIX-FUTURE: file/link CPython bug report, deprecate this patch if/when it's fixed in CPython
30
28
  a_classvar: _ts.ClassVar[int] # type: ignore[name-defined]
31
29
  a_field: int
32
30
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import typing as t
4
4
 
5
- from ..common import messages as _messages
5
+ from . import _messages
6
6
 
7
7
 
8
8
  class HasPluginInfo(t.Protocol):
@@ -21,5 +21,18 @@ def get_plugin_info(value: HasPluginInfo) -> _messages.PluginInfo:
21
21
  """Utility method that returns a `PluginInfo` from an object implementing the `HasPluginInfo` protocol."""
22
22
  return _messages.PluginInfo(
23
23
  resolved_name=value.ansible_name,
24
- type=value.plugin_type,
24
+ type=normalize_plugin_type(value.plugin_type),
25
25
  )
26
+
27
+
28
+ def normalize_plugin_type(value: str) -> _messages.PluginType | None:
29
+ """Normalize value and return it as a PluginType, or None if the value does match any known plugin type."""
30
+ value = value.lower()
31
+
32
+ if value == 'modules':
33
+ value = 'module'
34
+
35
+ try:
36
+ return _messages.PluginType(value)
37
+ except ValueError:
38
+ return None
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+ import inspect as _inspect
4
+ import typing as _t
5
+
6
+
7
+ def caller_frame() -> _inspect.FrameInfo | None:
8
+ """Return the caller stack frame, skipping any marked with the `_skip_stackwalk` local."""
9
+ _skip_stackwalk = True
10
+
11
+ return next(iter_stack(), None)
12
+
13
+
14
+ def iter_stack() -> _t.Generator[_inspect.FrameInfo]:
15
+ """Iterate over stack frames, skipping any marked with the `_skip_stackwalk` local."""
16
+ _skip_stackwalk = True
17
+
18
+ for frame_info in _inspect.stack():
19
+ if '_skip_stackwalk' in frame_info.frame.f_locals:
20
+ continue
21
+
22
+ yield frame_info
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations as _annotations
2
+
3
+
4
+ def concat_message(left: str, right: str) -> str:
5
+ """Normalize `left` by removing trailing punctuation and spaces before appending new punctuation and `right`."""
6
+ return f'{left.rstrip(". ")}: {right}'
@@ -6,9 +6,10 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import enum
9
- import inspect
10
9
  import traceback
11
10
 
11
+ from . import _stack
12
+
12
13
 
13
14
  class TracebackEvent(enum.Enum):
14
15
  """The events for which tracebacks can be enabled."""
@@ -16,6 +17,7 @@ class TracebackEvent(enum.Enum):
16
17
  ERROR = enum.auto()
17
18
  WARNING = enum.auto()
18
19
  DEPRECATED = enum.auto()
20
+ DEPRECATED_VALUE = enum.auto() # implies DEPRECATED
19
21
 
20
22
 
21
23
  def traceback_for() -> list[str]:
@@ -28,24 +30,25 @@ def is_traceback_enabled(event: TracebackEvent) -> bool:
28
30
  return _is_traceback_enabled(event)
29
31
 
30
32
 
31
- def maybe_capture_traceback(event: TracebackEvent) -> str | None:
33
+ def maybe_capture_traceback(msg: str, event: TracebackEvent) -> str | None:
32
34
  """
33
35
  Optionally capture a traceback for the current call stack, formatted as a string, if the specified traceback event is enabled.
34
- The current and previous frames are omitted to mask the expected call pattern from error/warning handlers.
36
+ Frames marked with the `_skip_stackwalk` local are omitted.
35
37
  """
38
+ _skip_stackwalk = True
39
+
36
40
  if not is_traceback_enabled(event):
37
41
  return None
38
42
 
39
43
  tb_lines = []
40
44
 
41
- if current_frame := inspect.currentframe():
45
+ if frame_info := _stack.caller_frame():
42
46
  # DTFIX-FUTURE: rewrite target-side tracebacks to point at controller-side paths?
43
- frames = inspect.getouterframes(current_frame)
44
- ignore_frame_count = 2 # ignore this function and its caller
45
47
  tb_lines.append('Traceback (most recent call last):\n')
46
- tb_lines.extend(traceback.format_stack(frames[ignore_frame_count].frame))
48
+ tb_lines.extend(traceback.format_stack(frame_info.frame))
49
+ tb_lines.append(f'Message: {msg}\n')
47
50
  else:
48
- tb_lines.append('Traceback unavailable.\n')
51
+ tb_lines.append('(frame not found)\n') # pragma: nocover
49
52
 
50
53
  return ''.join(tb_lines)
51
54
 
@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.19.0b4'
20
+ __version__ = '2.19.0b6'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "What Is and What Should Never Be"