ansible-core 2.19.0b4__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 (163) hide show
  1. ansible/_internal/__init__.py +1 -1
  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 +5 -5
  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/_yaml/_dumper.py +1 -1
  26. ansible/_internal/_yaml/_errors.py +7 -7
  27. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  29. ansible/cli/__init__.py +5 -82
  30. ansible/cli/arguments/option_helpers.py +2 -3
  31. ansible/cli/doc.py +84 -28
  32. ansible/cli/inventory.py +1 -1
  33. ansible/compat/importlib_resources.py +9 -12
  34. ansible/config/base.yml +22 -0
  35. ansible/errors/__init__.py +96 -49
  36. ansible/executor/module_common.py +8 -10
  37. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  38. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  39. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  40. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  41. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  42. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  43. ansible/executor/powershell/module_manifest.py +52 -0
  44. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  45. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  46. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  47. ansible/executor/process/worker.py +38 -113
  48. ansible/executor/task_executor.py +26 -61
  49. ansible/executor/task_result.py +2 -4
  50. ansible/galaxy/collection/__init__.py +1 -4
  51. ansible/inventory/manager.py +1 -1
  52. ansible/module_utils/_internal/__init__.py +0 -3
  53. ansible/module_utils/_internal/_ambient_context.py +3 -3
  54. ansible/module_utils/_internal/_ansiballz.py +4 -2
  55. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  56. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  57. ansible/module_utils/_internal/_deprecator.py +66 -48
  58. ansible/module_utils/_internal/_errors.py +88 -17
  59. ansible/module_utils/_internal/_event_utils.py +61 -0
  60. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  61. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  62. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  63. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  64. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  65. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  66. ansible/module_utils/_internal/_plugin_info.py +1 -1
  67. ansible/module_utils/_internal/_stack.py +22 -0
  68. ansible/module_utils/_internal/_text_utils.py +6 -0
  69. ansible/module_utils/_internal/_traceback.py +11 -8
  70. ansible/module_utils/ansible_release.py +1 -1
  71. ansible/module_utils/basic.py +49 -15
  72. ansible/module_utils/common/arg_spec.py +2 -2
  73. ansible/module_utils/common/collections.py +6 -0
  74. ansible/module_utils/common/json.py +2 -2
  75. ansible/module_utils/common/text/converters.py +3 -3
  76. ansible/module_utils/common/validation.py +1 -1
  77. ansible/module_utils/common/warnings.py +80 -23
  78. ansible/module_utils/common/yaml.py +1 -1
  79. ansible/module_utils/datatag.py +5 -2
  80. ansible/module_utils/facts/system/distribution.py +16 -3
  81. ansible/module_utils/facts/virtual/linux.py +1 -1
  82. ansible/module_utils/service.py +2 -9
  83. ansible/modules/apt_repository.py +7 -29
  84. ansible/modules/async_status.py +13 -11
  85. ansible/modules/async_wrapper.py +5 -5
  86. ansible/modules/dnf5.py +14 -22
  87. ansible/modules/hostname.py +0 -1
  88. ansible/modules/service.py +3 -9
  89. ansible/parsing/ajson.py +3 -5
  90. ansible/parsing/dataloader.py +4 -4
  91. ansible/parsing/mod_args.py +1 -1
  92. ansible/parsing/plugin_docs.py +2 -2
  93. ansible/parsing/utils/yaml.py +3 -3
  94. ansible/parsing/vault/__init__.py +4 -4
  95. ansible/playbook/playbook_include.py +1 -1
  96. ansible/playbook/taggable.py +0 -3
  97. ansible/plugins/__init__.py +0 -25
  98. ansible/plugins/action/__init__.py +8 -31
  99. ansible/plugins/action/add_host.py +1 -1
  100. ansible/plugins/action/assemble.py +8 -16
  101. ansible/plugins/action/async_status.py +7 -2
  102. ansible/plugins/action/copy.py +8 -7
  103. ansible/plugins/action/gather_facts.py +8 -8
  104. ansible/plugins/action/package.py +5 -8
  105. ansible/plugins/action/script.py +8 -15
  106. ansible/plugins/action/service.py +3 -7
  107. ansible/plugins/action/template.py +3 -8
  108. ansible/plugins/action/unarchive.py +5 -15
  109. ansible/plugins/action/uri.py +9 -20
  110. ansible/plugins/callback/__init__.py +4 -6
  111. ansible/plugins/callback/junit.py +4 -2
  112. ansible/plugins/connection/local.py +2 -2
  113. ansible/plugins/connection/ssh.py +17 -9
  114. ansible/plugins/connection/winrm.py +5 -2
  115. ansible/plugins/doc_fragments/constructed.py +2 -2
  116. ansible/plugins/filter/core.py +13 -6
  117. ansible/plugins/filter/encryption.py +4 -4
  118. ansible/plugins/inventory/__init__.py +11 -10
  119. ansible/plugins/inventory/script.py +1 -1
  120. ansible/plugins/list.py +69 -16
  121. ansible/plugins/loader.py +7 -7
  122. ansible/plugins/lookup/csvfile.py +16 -71
  123. ansible/plugins/lookup/first_found.py +2 -1
  124. ansible/plugins/shell/__init__.py +56 -2
  125. ansible/plugins/shell/powershell.py +66 -9
  126. ansible/plugins/shell/sh.py +9 -5
  127. ansible/plugins/test/core.py +21 -15
  128. ansible/plugins/test/finished.yml +1 -1
  129. ansible/plugins/test/uri.py +2 -5
  130. ansible/release.py +1 -1
  131. ansible/template/__init__.py +30 -2
  132. ansible/utils/display.py +103 -128
  133. ansible/utils/hashing.py +0 -1
  134. ansible/utils/listify.py +6 -4
  135. ansible/utils/unsafe_proxy.py +1 -1
  136. ansible/vars/hostvars.py +1 -1
  137. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +1 -1
  138. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +162 -151
  139. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  140. ansible_test/_data/completion/docker.txt +3 -3
  141. ansible_test/_data/completion/remote.txt +1 -0
  142. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  143. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  144. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  145. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  146. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  147. ansible_test/_internal/util.py +20 -0
  148. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  149. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  150. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  151. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  152. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  153. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
  154. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  155. ansible/_internal/_errors/_utils.py +0 -310
  156. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  157. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/COPYING +0 -0
  158. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  159. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  160. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  161. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  162. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  163. {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -1,79 +1,52 @@
1
1
  from __future__ import annotations
2
2
 
3
- import inspect
4
3
  import re
5
4
  import pathlib
6
5
  import sys
7
6
  import typing as t
8
7
 
9
- from ansible.module_utils.common.messages import PluginInfo
8
+ from ansible.module_utils._internal import _stack, _messages, _validation
10
9
 
11
- _ansible_module_base_path: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
12
- """Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
13
10
 
14
- ANSIBLE_CORE_DEPRECATOR: t.Final = PluginInfo._from_collection_name('ansible.builtin')
15
- """Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
11
+ def deprecator_from_collection_name(collection_name: str | None) -> _messages.PluginInfo | None:
12
+ """Returns an instance with the special `collection` type to refer to a non-plugin or ambiguous caller within a collection."""
13
+ # CAUTION: This function is exposed in public API as ansible.module_utils.datatag.deprecator_from_collection_name.
16
14
 
17
- INDETERMINATE_DEPRECATOR: t.Final = PluginInfo(resolved_name='indeterminate', type='indeterminate')
18
- """Singleton `PluginInfo` instance for indeterminate deprecator."""
15
+ if not collection_name:
16
+ return None
19
17
 
20
- _DEPRECATOR_PLUGIN_TYPES = frozenset(
21
- {
22
- 'action',
23
- 'become',
24
- 'cache',
25
- 'callback',
26
- 'cliconf',
27
- 'connection',
28
- # doc_fragments - no code execution
29
- # filter - basename inadequate to identify plugin
30
- 'httpapi',
31
- 'inventory',
32
- 'lookup',
33
- 'module', # only for collections
34
- 'netconf',
35
- 'shell',
36
- 'strategy',
37
- 'terminal',
38
- # test - basename inadequate to identify plugin
39
- 'vars',
40
- }
41
- )
42
- """Plugin types which are valid for identifying a deprecator for deprecation purposes."""
18
+ _validation.validate_collection_name(collection_name)
43
19
 
44
- _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES = frozenset(
45
- {
46
- 'filter',
47
- 'test',
48
- }
49
- )
50
- """Plugin types for which basename cannot be used to identify the plugin name."""
20
+ return _messages.PluginInfo(
21
+ resolved_name=collection_name,
22
+ type=_COLLECTION_ONLY_TYPE,
23
+ )
51
24
 
52
25
 
53
- def get_best_deprecator(*, deprecator: PluginInfo | None = None, collection_name: str | None = None) -> PluginInfo:
26
+ def get_best_deprecator(*, deprecator: _messages.PluginInfo | None = None, collection_name: str | None = None) -> _messages.PluginInfo:
54
27
  """Return the best-available `PluginInfo` for the caller of this method."""
55
28
  _skip_stackwalk = True
56
29
 
57
30
  if deprecator and collection_name:
58
31
  raise ValueError('Specify only one of `deprecator` or `collection_name`.')
59
32
 
60
- return deprecator or PluginInfo._from_collection_name(collection_name) or get_caller_plugin_info() or INDETERMINATE_DEPRECATOR
33
+ return deprecator or deprecator_from_collection_name(collection_name) or get_caller_plugin_info() or INDETERMINATE_DEPRECATOR
61
34
 
62
35
 
63
- def get_caller_plugin_info() -> PluginInfo | None:
36
+ def get_caller_plugin_info() -> _messages.PluginInfo | None:
64
37
  """Try to get `PluginInfo` for the caller of this method, ignoring marked infrastructure stack frames."""
65
38
  _skip_stackwalk = True
66
39
 
67
- if frame_info := next((frame_info for frame_info in inspect.stack() if '_skip_stackwalk' not in frame_info.frame.f_locals), None):
40
+ if frame_info := _stack.caller_frame():
68
41
  return _path_as_core_plugininfo(frame_info.filename) or _path_as_collection_plugininfo(frame_info.filename)
69
42
 
70
43
  return None # pragma: nocover
71
44
 
72
45
 
73
- def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
46
+ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
74
47
  """Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
75
48
  try:
76
- relpath = str(pathlib.Path(path).relative_to(_ansible_module_base_path))
49
+ relpath = str(pathlib.Path(path).relative_to(_ANSIBLE_MODULE_BASE_PATH))
77
50
  except ValueError:
78
51
  return None # not ansible-core
79
52
 
@@ -104,10 +77,10 @@ def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
104
77
 
105
78
  name = f'{namespace}.{plugin_name}'
106
79
 
107
- return PluginInfo(resolved_name=name, type=plugin_type)
80
+ return _messages.PluginInfo(resolved_name=name, type=plugin_type)
108
81
 
109
82
 
110
- def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
83
+ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
111
84
  """Return a `PluginInfo` instance if the provided `path` refers to a collection plugin."""
112
85
  if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
113
86
  return None
@@ -118,7 +91,7 @@ def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
118
91
  # We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
119
92
  # To keep things simple we'll fall back to just identifying the namespace and collection.
120
93
  # In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
121
- return PluginInfo._from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
94
+ return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
122
95
 
123
96
  if plugin_type == 'modules':
124
97
  plugin_type = 'module'
@@ -131,4 +104,49 @@ def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
131
104
 
132
105
  name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
133
106
 
134
- return PluginInfo(resolved_name=name, type=plugin_type)
107
+ return _messages.PluginInfo(resolved_name=name, type=plugin_type)
108
+
109
+
110
+ _COLLECTION_ONLY_TYPE: t.Final = 'collection'
111
+ """Ersatz placeholder plugin type for use by a `PluginInfo` instance that references only a collection."""
112
+
113
+ _ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
114
+ """Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
115
+
116
+ ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin')
117
+ """Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
118
+
119
+ INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name='indeterminate', type='indeterminate')
120
+ """Singleton `PluginInfo` instance for indeterminate deprecator."""
121
+
122
+ _DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
123
+ {
124
+ 'action',
125
+ 'become',
126
+ 'cache',
127
+ 'callback',
128
+ 'cliconf',
129
+ 'connection',
130
+ # doc_fragments - no code execution
131
+ # filter - basename inadequate to identify plugin
132
+ 'httpapi',
133
+ 'inventory',
134
+ 'lookup',
135
+ 'module', # only for collections
136
+ 'netconf',
137
+ 'shell',
138
+ 'strategy',
139
+ 'terminal',
140
+ # test - basename inadequate to identify plugin
141
+ 'vars',
142
+ }
143
+ )
144
+ """Plugin types which are valid for identifying a deprecator for deprecation purposes."""
145
+
146
+ _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
147
+ {
148
+ 'filter',
149
+ 'test',
150
+ }
151
+ )
152
+ """Plugin types for which basename cannot be used to identify the plugin name."""
@@ -3,28 +3,99 @@
3
3
 
4
4
  """Internal error handling logic for targets. Not for use on the controller."""
5
5
 
6
- from __future__ import annotations
6
+ from __future__ import annotations as _annotations
7
7
 
8
- from . import _traceback
9
- from ..common.messages import Detail, ErrorSummary
8
+ import traceback as _sys_traceback
9
+ import typing as _t
10
10
 
11
+ from . import _messages
11
12
 
12
- def create_error_summary(exception: BaseException) -> ErrorSummary:
13
- """Return an `ErrorDetail` created from the given exception."""
14
- return ErrorSummary(
15
- details=_create_error_details(exception),
16
- formatted_traceback=_traceback.maybe_extract_traceback(exception, _traceback.TracebackEvent.ERROR),
17
- )
13
+ MSG_REASON_DIRECT_CAUSE: _t.Final[str] = '<<< caused by >>>'
14
+ MSG_REASON_HANDLING_CAUSE: _t.Final[str] = '<<< while handling >>>'
18
15
 
16
+ TRACEBACK_REASON_EXCEPTION_DIRECT_WARNING: _t.Final[str] = 'The above exception was the direct cause of the following warning:'
19
17
 
20
- def _create_error_details(exception: BaseException) -> tuple[Detail, ...]:
21
- """Return an `ErrorMessage` tuple created from the given exception."""
22
- target_exception: BaseException | None = exception
23
- error_details: list[Detail] = []
24
18
 
25
- while target_exception:
26
- error_details.append(Detail(msg=str(target_exception).strip()))
19
+ class EventFactory:
20
+ """Factory for creating `Event` instances from `BaseException` instances on targets."""
27
21
 
28
- target_exception = target_exception.__cause__
22
+ _MAX_DEPTH = 10
23
+ """Maximum exception chain depth. Exceptions beyond this depth will be omitted."""
29
24
 
30
- return tuple(error_details)
25
+ @classmethod
26
+ def from_exception(cls, exception: BaseException, include_traceback: bool) -> _messages.Event:
27
+ return cls(include_traceback)._convert_exception(exception)
28
+
29
+ def __init__(self, include_traceback: bool) -> None:
30
+ self._include_traceback = include_traceback
31
+ self._depth = 0
32
+
33
+ def _convert_exception(self, exception: BaseException) -> _messages.Event:
34
+ if self._depth > self._MAX_DEPTH:
35
+ return _messages.Event(
36
+ msg="Maximum depth exceeded, omitting further events.",
37
+ )
38
+
39
+ self._depth += 1
40
+
41
+ try:
42
+ return _messages.Event(
43
+ msg=self._get_msg(exception),
44
+ formatted_traceback=self._get_formatted_traceback(exception),
45
+ formatted_source_context=self._get_formatted_source_context(exception),
46
+ help_text=self._get_help_text(exception),
47
+ chain=self._get_chain(exception),
48
+ events=self._get_events(exception),
49
+ )
50
+ finally:
51
+ self._depth -= 1
52
+
53
+ def _get_msg(self, exception: BaseException) -> str | None:
54
+ return str(exception).strip()
55
+
56
+ def _get_formatted_traceback(self, exception: BaseException) -> str | None:
57
+ if self._include_traceback:
58
+ return ''.join(_sys_traceback.format_exception(type(exception), exception, exception.__traceback__, chain=False))
59
+
60
+ return None
61
+
62
+ def _get_formatted_source_context(self, exception: BaseException) -> str | None:
63
+ return None
64
+
65
+ def _get_help_text(self, exception: BaseException) -> str | None:
66
+ return None
67
+
68
+ def _get_chain(self, exception: BaseException) -> _messages.EventChain | None:
69
+ if cause := self._get_cause(exception):
70
+ return _messages.EventChain(
71
+ msg_reason=MSG_REASON_DIRECT_CAUSE,
72
+ traceback_reason='The above exception was the direct cause of the following exception:',
73
+ event=self._convert_exception(cause),
74
+ follow=self._follow_cause(exception),
75
+ )
76
+
77
+ if context := self._get_context(exception):
78
+ return _messages.EventChain(
79
+ msg_reason=MSG_REASON_HANDLING_CAUSE,
80
+ traceback_reason='During handling of the above exception, another exception occurred:',
81
+ event=self._convert_exception(context),
82
+ follow=False,
83
+ )
84
+
85
+ return None
86
+
87
+ def _follow_cause(self, exception: BaseException) -> bool:
88
+ return True
89
+
90
+ def _get_cause(self, exception: BaseException) -> BaseException | None:
91
+ return exception.__cause__
92
+
93
+ def _get_context(self, exception: BaseException) -> BaseException | None:
94
+ if exception.__suppress_context__:
95
+ return None
96
+
97
+ return exception.__context__
98
+
99
+ def _get_events(self, exception: BaseException) -> tuple[_messages.Event, ...] | None:
100
+ # deprecated: description='move BaseExceptionGroup support here from ControllerEventFactory' python_version='3.10'
101
+ return None
@@ -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,8 @@ 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.Event,
91
+ _messages.EventChain,
91
92
  _messages.ErrorSummary,
92
93
  _messages.WarningSummary,
93
94
  _messages.DeprecationSummary,
@@ -203,11 +204,27 @@ class _JSONSerializationProfile(t.Generic[_T_encoder, _T_decoder]):
203
204
 
204
205
  @classmethod
205
206
  def handle_key(cls, k: t.Any) -> t.Any:
207
+ """Validation/conversion hook before a dict key is serialized. The default implementation only accepts str-typed keys."""
208
+ # NOTE: Since JSON requires string keys, there is no support for preserving tags on dictionary keys during serialization.
209
+
206
210
  if not isinstance(k, str): # DTFIX-FUTURE: optimize this to use all known str-derived types in type map / allowed types
207
211
  raise TypeError(f'Key of type {type(k).__name__!r} is not JSON serializable by the {cls.profile_name!r} profile.')
208
212
 
209
213
  return k
210
214
 
215
+ @classmethod
216
+ def _handle_key_str_fallback(cls, k: t.Any) -> t.Any:
217
+ """Legacy implementations should use this key handler for backward compatibility with stdlib JSON key conversion quirks."""
218
+ # DTFIX-FUTURE: optimized exact-type table lookup first
219
+
220
+ if isinstance(k, str):
221
+ return k
222
+
223
+ if k is None or isinstance(k, (int, float)):
224
+ return json.dumps(k)
225
+
226
+ raise TypeError(f'Key of type {type(k).__name__!r} is not JSON serializable by the {cls.profile_name!r} profile.')
227
+
211
228
  @classmethod
212
229
  def default(cls, o: t.Any) -> t.Any:
213
230
  # Preserve the built-in JSON encoder support for subclasses of scalar types.
@@ -373,8 +390,8 @@ Future code changes should further restrict bytes to string conversions to elimi
373
390
  Additional warnings at other boundaries may be needed to give users an opportunity to resolve the issues before they become errors.
374
391
  """
375
392
  # 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
393
+ # DTFIX3: the surrogateescape note above isn't quite right, for encoding use surrogatepass, which does work
394
+ # DTFIX-FUTURE: this config setting should probably be deprecated
378
395
 
379
396
 
380
397
  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,11 @@ 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 sys as _sys
12
+ import typing as _t
12
13
 
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
14
+ from ansible.module_utils._internal import _datatag, _dataclass_validation
17
15
 
18
16
  if _sys.version_info >= (3, 10):
19
17
  # Using slots for reduced memory usage and improved performance.
@@ -29,50 +27,50 @@ class PluginInfo(_datatag.AnsibleSerializableDataclass):
29
27
 
30
28
  resolved_name: str
31
29
  """The resolved canonical plugin name; always fully-qualified for collection plugins."""
30
+
32
31
  type: str
33
32
  """The plugin type."""
34
33
 
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
34
 
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
35
+ @_dataclasses.dataclass(**_dataclass_kwargs)
36
+ class EventChain(_datatag.AnsibleSerializableDataclass):
37
+ """A chain used to link one event to another."""
43
38
 
44
- _validation.validate_collection_name(collection_name)
39
+ _validation_auto_enabled = False
45
40
 
46
- return cls(
47
- resolved_name=collection_name,
48
- type=cls._COLLECTION_ONLY_TYPE,
49
- )
41
+ def __post_init__(self): ... # required for deferred dataclass validation
42
+
43
+ msg_reason: str
44
+ traceback_reason: str
45
+ event: Event
46
+ follow: bool = True
50
47
 
51
48
 
52
49
  @_dataclasses.dataclass(**_dataclass_kwargs)
53
- class Detail(_datatag.AnsibleSerializableDataclass):
54
- """Message detail with optional source context and help text."""
50
+ class Event(_datatag.AnsibleSerializableDataclass):
51
+ """Base class for an error/warning/deprecation event with optional chain (from an exception __cause__ chain) and an optional traceback."""
52
+
53
+ _validation_auto_enabled = False
54
+
55
+ def __post_init__(self): ... # required for deferred dataclass validation
55
56
 
56
57
  msg: str
57
58
  formatted_source_context: _t.Optional[str] = None
59
+ formatted_traceback: _t.Optional[str] = None
58
60
  help_text: _t.Optional[str] = None
61
+ chain: _t.Optional[EventChain] = None
62
+ events: _t.Optional[_t.Tuple[Event, ...]] = None
63
+
64
+
65
+ _dataclass_validation.inject_post_init_validation(EventChain, EventChain._validation_allow_subclasses)
66
+ _dataclass_validation.inject_post_init_validation(Event, Event._validation_allow_subclasses)
59
67
 
60
68
 
61
69
  @_dataclasses.dataclass(**_dataclass_kwargs)
62
70
  class SummaryBase(_datatag.AnsibleSerializableDataclass):
63
71
  """Base class for an error/warning/deprecation summary with details (possibly derived from an exception __cause__ chain) and an optional traceback."""
64
72
 
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')
73
+ event: Event
76
74
 
77
75
 
78
76
  @_dataclasses.dataclass(**_dataclass_kwargs)
@@ -106,20 +104,3 @@ class DeprecationSummary(WarningSummary):
106
104
  Ignored if `deprecator` is not provided.
107
105
  Ignored if `date` is provided.
108
106
  """
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):
@@ -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}'