ansible-core 2.19.0b1__py3-none-any.whl → 2.19.0b2__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 (48) hide show
  1. ansible/_internal/_collection_proxy.py +47 -0
  2. ansible/_internal/_errors/_handler.py +4 -4
  3. ansible/_internal/_json/__init__.py +47 -4
  4. ansible/_internal/_json/_profiles/_legacy.py +2 -3
  5. ansible/_internal/_templating/_jinja_bits.py +4 -4
  6. ansible/_internal/_templating/_jinja_plugins.py +5 -11
  7. ansible/cli/__init__.py +9 -3
  8. ansible/cli/doc.py +14 -7
  9. ansible/config/base.yml +17 -20
  10. ansible/executor/process/worker.py +31 -26
  11. ansible/executor/task_executor.py +32 -23
  12. ansible/executor/task_queue_manager.py +62 -52
  13. ansible/executor/task_result.py +168 -72
  14. ansible/inventory/manager.py +2 -1
  15. ansible/module_utils/ansible_release.py +1 -1
  16. ansible/module_utils/basic.py +4 -6
  17. ansible/module_utils/common/warnings.py +1 -1
  18. ansible/parsing/utils/jsonify.py +40 -0
  19. ansible/parsing/yaml/objects.py +16 -5
  20. ansible/playbook/included_file.py +25 -12
  21. ansible/plugins/callback/__init__.py +173 -86
  22. ansible/plugins/callback/default.py +79 -79
  23. ansible/plugins/callback/junit.py +20 -19
  24. ansible/plugins/callback/minimal.py +17 -17
  25. ansible/plugins/callback/oneline.py +16 -15
  26. ansible/plugins/callback/tree.py +6 -5
  27. ansible/plugins/filter/core.py +8 -1
  28. ansible/plugins/strategy/__init__.py +70 -76
  29. ansible/plugins/strategy/free.py +4 -4
  30. ansible/plugins/strategy/linear.py +11 -9
  31. ansible/plugins/test/core.py +1 -1
  32. ansible/release.py +1 -1
  33. ansible/template/__init__.py +7 -5
  34. ansible/utils/display.py +10 -10
  35. ansible/utils/vars.py +23 -0
  36. ansible/vars/clean.py +1 -1
  37. ansible/vars/manager.py +2 -19
  38. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/METADATA +1 -1
  39. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/RECORD +48 -46
  40. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/Apache-License.txt +0 -0
  41. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/BSD-3-Clause.txt +0 -0
  42. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/COPYING +0 -0
  43. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/MIT-license.txt +0 -0
  44. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/PSF-license.txt +0 -0
  45. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/WHEEL +0 -0
  46. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/entry_points.txt +0 -0
  47. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/simplified_bsd.txt +0 -0
  48. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b2.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import dataclasses
20
21
  import os
21
22
  import sys
22
23
  import tempfile
@@ -31,7 +32,7 @@ from ansible.errors import AnsibleError, ExitCode, AnsibleCallbackError
31
32
  from ansible._internal._errors._handler import ErrorHandler
32
33
  from ansible.executor.play_iterator import PlayIterator
33
34
  from ansible.executor.stats import AggregateStats
34
- from ansible.executor.task_result import TaskResult
35
+ from ansible.executor.task_result import _RawTaskResult, _WireTaskResult
35
36
  from ansible.inventory.data import InventoryData
36
37
  from ansible.module_utils.six import string_types
37
38
  from ansible.module_utils.common.text.converters import to_native
@@ -47,7 +48,8 @@ from ansible.utils.display import Display
47
48
  from ansible.utils.lock import lock_decorator
48
49
  from ansible.utils.multiprocessing import context as multiprocessing_context
49
50
 
50
- from dataclasses import dataclass
51
+ if t.TYPE_CHECKING:
52
+ from ansible.executor.process.worker import WorkerProcess
51
53
 
52
54
  __all__ = ['TaskQueueManager']
53
55
 
@@ -57,12 +59,13 @@ STDERR_FILENO = 2
57
59
 
58
60
  display = Display()
59
61
 
62
+ _T = t.TypeVar('_T')
60
63
 
64
+
65
+ @dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
61
66
  class CallbackSend:
62
- def __init__(self, method_name, *args, **kwargs):
63
- self.method_name = method_name
64
- self.args = args
65
- self.kwargs = kwargs
67
+ method_name: str
68
+ wire_task_result: _WireTaskResult
66
69
 
67
70
 
68
71
  class DisplaySend:
@@ -72,7 +75,7 @@ class DisplaySend:
72
75
  self.kwargs = kwargs
73
76
 
74
77
 
75
- @dataclass
78
+ @dataclasses.dataclass
76
79
  class PromptSend:
77
80
  worker_id: int
78
81
  prompt: str
@@ -87,19 +90,11 @@ class FinalQueue(multiprocessing.queues.SimpleQueue):
87
90
  kwargs['ctx'] = multiprocessing_context
88
91
  super().__init__(*args, **kwargs)
89
92
 
90
- def send_callback(self, method_name, *args, **kwargs):
91
- self.put(
92
- CallbackSend(method_name, *args, **kwargs),
93
- )
93
+ def send_callback(self, method_name: str, task_result: _RawTaskResult) -> None:
94
+ self.put(CallbackSend(method_name=method_name, wire_task_result=task_result.as_wire_task_result()))
94
95
 
95
- def send_task_result(self, *args, **kwargs):
96
- if isinstance(args[0], TaskResult):
97
- tr = args[0]
98
- else:
99
- tr = TaskResult(*args, **kwargs)
100
- self.put(
101
- tr,
102
- )
96
+ def send_task_result(self, task_result: _RawTaskResult) -> None:
97
+ self.put(task_result.as_wire_task_result())
103
98
 
104
99
  def send_display(self, method, *args, **kwargs):
105
100
  self.put(
@@ -194,11 +189,8 @@ class TaskQueueManager:
194
189
  # plugins for inter-process locking.
195
190
  self._connection_lockfile = tempfile.TemporaryFile()
196
191
 
197
- def _initialize_processes(self, num):
198
- self._workers = []
199
-
200
- for i in range(num):
201
- self._workers.append(None)
192
+ def _initialize_processes(self, num: int) -> None:
193
+ self._workers: list[WorkerProcess | None] = [None] * num
202
194
 
203
195
  def load_callbacks(self):
204
196
  """
@@ -438,54 +430,72 @@ class TaskQueueManager:
438
430
  defunct = True
439
431
  return defunct
440
432
 
433
+ @staticmethod
434
+ def _first_arg_of_type(value_type: t.Type[_T], args: t.Sequence) -> _T | None:
435
+ return next((arg for arg in args if isinstance(arg, value_type)), None)
436
+
441
437
  @lock_decorator(attr='_callback_lock')
442
438
  def send_callback(self, method_name, *args, **kwargs):
443
439
  # We always send events to stdout callback first, rest should follow config order
444
440
  for callback_plugin in [self._stdout_callback] + self._callback_plugins:
445
441
  # a plugin that set self.disabled to True will not be called
446
442
  # see osx_say.py example for such a plugin
447
- if getattr(callback_plugin, 'disabled', False):
443
+ if callback_plugin.disabled:
448
444
  continue
449
445
 
450
446
  # a plugin can opt in to implicit tasks (such as meta). It does this
451
447
  # by declaring self.wants_implicit_tasks = True.
452
- wants_implicit_tasks = getattr(callback_plugin, 'wants_implicit_tasks', False)
448
+ if not callback_plugin.wants_implicit_tasks and (task_arg := self._first_arg_of_type(Task, args)) and task_arg.implicit:
449
+ continue
453
450
 
454
451
  # try to find v2 method, fallback to v1 method, ignore callback if no method found
455
452
  methods = []
453
+
456
454
  for possible in [method_name, 'v2_on_any']:
457
- gotit = getattr(callback_plugin, possible, None)
458
- if gotit is None:
459
- gotit = getattr(callback_plugin, possible.removeprefix('v2_'), None)
460
- if gotit is not None:
461
- methods.append(gotit)
462
-
463
- # send clean copies
464
- new_args = []
465
-
466
- # If we end up being given an implicit task, we'll set this flag in
467
- # the loop below. If the plugin doesn't care about those, then we
468
- # check and continue to the next iteration of the outer loop.
469
- is_implicit_task = False
470
-
471
- for arg in args:
472
- # FIXME: add play/task cleaners
473
- if isinstance(arg, TaskResult):
474
- new_args.append(arg.clean_copy())
475
- # elif isinstance(arg, Play):
476
- # elif isinstance(arg, Task):
477
- else:
478
- new_args.append(arg)
455
+ method = getattr(callback_plugin, possible, None)
479
456
 
480
- if isinstance(arg, Task) and arg.implicit:
481
- is_implicit_task = True
457
+ if method is None:
458
+ method = getattr(callback_plugin, possible.removeprefix('v2_'), None)
482
459
 
483
- if is_implicit_task and not wants_implicit_tasks:
484
- continue
460
+ if method is not None:
461
+ display.deprecated(
462
+ msg='The v1 callback API is deprecated.',
463
+ version='2.23',
464
+ help_text='Use `v2_` prefixed callback methods instead.',
465
+ )
466
+
467
+ if method is not None and not getattr(method, '_base_impl', False): # don't bother dispatching to the base impls
468
+ if possible == 'v2_on_any':
469
+ display.deprecated(
470
+ msg='The `v2_on_any` callback method is deprecated.',
471
+ version='2.23',
472
+ help_text='Use event-specific callback methods instead.',
473
+ )
474
+
475
+ methods.append(method)
485
476
 
486
477
  for method in methods:
478
+ # send clean copies
479
+ new_args = []
480
+
481
+ for arg in args:
482
+ # FIXME: add play/task cleaners
483
+ if isinstance(arg, _RawTaskResult):
484
+ copied_tr = arg.as_callback_task_result()
485
+ new_args.append(copied_tr)
486
+ # this state hack requires that no callback ever accepts > 1 TaskResult object
487
+ callback_plugin._current_task_result = copied_tr
488
+ else:
489
+ new_args.append(arg)
490
+
487
491
  with self._callback_dispatch_error_handler.handle(AnsibleCallbackError):
488
492
  try:
489
493
  method(*new_args, **kwargs)
494
+ except AssertionError:
495
+ # Using an `assert` in integration tests is useful.
496
+ # Production code should never use `assert` or raise `AssertionError`.
497
+ raise
490
498
  except Exception as ex:
491
499
  raise AnsibleCallbackError(f"Callback dispatch {method_name!r} failed for plugin {callback_plugin._load_name!r}.") from ex
500
+
501
+ callback_plugin._current_task_result = None
@@ -4,15 +4,24 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import collections.abc as _c
8
+ import dataclasses
9
+ import functools
7
10
  import typing as t
8
11
 
9
- from ansible import constants as C
10
- from ansible.parsing.dataloader import DataLoader
12
+ from ansible import constants
13
+ from ansible.utils import vars as _vars
11
14
  from ansible.vars.clean import module_response_deepcopy, strip_internal_keys
15
+ from ansible.module_utils.common import messages as _messages
16
+ from ansible._internal import _collection_proxy
17
+
18
+ if t.TYPE_CHECKING:
19
+ from ansible.inventory.host import Host
20
+ from ansible.playbook.task import Task
12
21
 
13
22
  _IGNORE = ('failed', 'skipped')
14
- _PRESERVE = ('attempts', 'changed', 'retries', '_ansible_no_log')
15
- _SUB_PRESERVE = {'_ansible_delegated_vars': ('ansible_host', 'ansible_port', 'ansible_user', 'ansible_connection')}
23
+ _PRESERVE = {'attempts', 'changed', 'retries', '_ansible_no_log'}
24
+ _SUB_PRESERVE = {'_ansible_delegated_vars': {'ansible_host', 'ansible_port', 'ansible_user', 'ansible_connection'}}
16
25
 
17
26
  # stuff callbacks need
18
27
  CLEAN_EXCEPTIONS = (
@@ -23,61 +32,120 @@ CLEAN_EXCEPTIONS = (
23
32
  )
24
33
 
25
34
 
26
- class TaskResult:
35
+ @t.final
36
+ @dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
37
+ class _WireTaskResult:
38
+ """A thin version of `_RawTaskResult` which can be sent over the worker queue."""
39
+
40
+ host_name: str
41
+ task_uuid: str
42
+ return_data: _c.MutableMapping[str, object]
43
+ task_fields: _c.Mapping[str, object]
44
+
45
+
46
+ class _BaseTaskResult:
27
47
  """
28
48
  This class is responsible for interpreting the resulting data
29
49
  from an executed task, and provides helper methods for determining
30
50
  the result of a given task.
31
51
  """
32
52
 
33
- def __init__(self, host, task, return_data, task_fields=None):
34
- self._host = host
35
- self._task = task
53
+ def __init__(self, host: Host, task: Task, return_data: _c.MutableMapping[str, t.Any], task_fields: _c.Mapping[str, t.Any]) -> None:
54
+ self.__host = host
55
+ self.__task = task
56
+ self._return_data = return_data # FIXME: this should be immutable, but strategy result processing mutates it in some corner cases
57
+ self.__task_fields = task_fields
36
58
 
37
- if isinstance(return_data, dict):
38
- self._result = return_data.copy()
39
- else:
40
- self._result = DataLoader().load(return_data)
59
+ @property
60
+ def host(self) -> Host:
61
+ """The host associated with this result."""
62
+ return self.__host
41
63
 
42
- if task_fields is None:
43
- self._task_fields = dict()
44
- else:
45
- self._task_fields = task_fields
64
+ @property
65
+ def _host(self) -> Host:
66
+ """Use the `host` property when supporting only ansible-core 2.19 or later."""
67
+ # deprecated: description='Deprecate `_host` in favor of `host`' core_version='2.23'
68
+ return self.__host
69
+
70
+ @property
71
+ def task(self) -> Task:
72
+ """The task associated with this result."""
73
+ return self.__task
74
+
75
+ @property
76
+ def _task(self) -> Task:
77
+ """Use the `task` property when supporting only ansible-core 2.19 or later."""
78
+ # deprecated: description='Deprecate `_task` in favor of `task`' core_version='2.23'
79
+ return self.__task
80
+
81
+ @property
82
+ def task_fields(self) -> _c.Mapping[str, t.Any]:
83
+ """The task fields associated with this result."""
84
+ return self.__task_fields
85
+
86
+ @property
87
+ def _task_fields(self) -> _c.Mapping[str, t.Any]:
88
+ """Use the `task_fields` property when supporting only ansible-core 2.19 or later."""
89
+ # deprecated: description='Deprecate `_task_fields` in favor of `task`' core_version='2.23'
90
+ return self.__task_fields
91
+
92
+ @property
93
+ def exception(self) -> _messages.ErrorSummary | None:
94
+ """The error from this task result, if any."""
95
+ return self._return_data.get('exception')
96
+
97
+ @property
98
+ def warnings(self) -> _c.Sequence[_messages.WarningSummary]:
99
+ """The warnings for this task, if any."""
100
+ return _collection_proxy.SequenceProxy(self._return_data.get('warnings') or [])
101
+
102
+ @property
103
+ def deprecations(self) -> _c.Sequence[_messages.DeprecationSummary]:
104
+ """The deprecation warnings for this task, if any."""
105
+ return _collection_proxy.SequenceProxy(self._return_data.get('deprecations') or [])
106
+
107
+ @property
108
+ def _loop_results(self) -> list[_c.MutableMapping[str, t.Any]]:
109
+ """Return a list of loop results. If no loop results are present, an empty list is returned."""
110
+ results = self._return_data.get('results')
111
+
112
+ if not isinstance(results, list):
113
+ return []
114
+
115
+ return results
46
116
 
47
117
  @property
48
- def task_name(self):
49
- return self._task_fields.get('name', None) or self._task.get_name()
118
+ def task_name(self) -> str:
119
+ return str(self.task_fields.get('name', '')) or self.task.get_name()
50
120
 
51
- def is_changed(self):
121
+ def is_changed(self) -> bool:
52
122
  return self._check_key('changed')
53
123
 
54
- def is_skipped(self):
55
- # loop results
56
- if 'results' in self._result:
57
- results = self._result['results']
124
+ def is_skipped(self) -> bool:
125
+ if self._loop_results:
58
126
  # Loop tasks are only considered skipped if all items were skipped.
59
127
  # some squashed results (eg, dnf) are not dicts and can't be skipped individually
60
- if results and all(isinstance(res, dict) and res.get('skipped', False) for res in results):
128
+ if all(isinstance(loop_res, dict) and loop_res.get('skipped', False) for loop_res in self._loop_results):
61
129
  return True
62
130
 
63
131
  # regular tasks and squashed non-dict results
64
- return self._result.get('skipped', False)
132
+ return bool(self._return_data.get('skipped', False))
65
133
 
66
- def is_failed(self):
67
- if 'failed_when_result' in self._result or \
68
- 'results' in self._result and True in [True for x in self._result['results'] if 'failed_when_result' in x]:
134
+ def is_failed(self) -> bool:
135
+ if 'failed_when_result' in self._return_data or any(isinstance(loop_res, dict) and 'failed_when_result' in loop_res for loop_res in self._loop_results):
69
136
  return self._check_key('failed_when_result')
70
- else:
71
- return self._check_key('failed')
72
137
 
73
- def is_unreachable(self):
138
+ return self._check_key('failed')
139
+
140
+ def is_unreachable(self) -> bool:
74
141
  return self._check_key('unreachable')
75
142
 
76
- def needs_debugger(self, globally_enabled=False):
77
- _debugger = self._task_fields.get('debugger')
78
- _ignore_errors = C.TASK_DEBUGGER_IGNORE_ERRORS and self._task_fields.get('ignore_errors')
143
+ def needs_debugger(self, globally_enabled: bool = False) -> bool:
144
+ _debugger = self.task_fields.get('debugger')
145
+ _ignore_errors = constants.TASK_DEBUGGER_IGNORE_ERRORS and self.task_fields.get('ignore_errors')
79
146
 
80
147
  ret = False
148
+
81
149
  if globally_enabled and ((self.is_failed() and not _ignore_errors) or self.is_unreachable()):
82
150
  ret = True
83
151
 
@@ -94,68 +162,96 @@ class TaskResult:
94
162
 
95
163
  return ret
96
164
 
97
- def _check_key(self, key):
98
- """get a specific key from the result or its items"""
165
+ def _check_key(self, key: str) -> bool:
166
+ """Fetch a specific named boolean value from the result; if missing, a logical OR of the value from nested loop results; False for non-loop results."""
167
+ if (value := self._return_data.get(key, ...)) is not ...:
168
+ return bool(value)
99
169
 
100
- if isinstance(self._result, dict) and key in self._result:
101
- return self._result.get(key, False)
102
- else:
103
- flag = False
104
- for res in self._result.get('results', []):
105
- if isinstance(res, dict):
106
- flag |= res.get(key, False)
107
- return flag
170
+ return any(isinstance(result, dict) and result.get(key) for result in self._loop_results)
108
171
 
109
- def clean_copy(self):
110
172
 
111
- """ returns 'clean' taskresult object """
173
+ @t.final
174
+ class _RawTaskResult(_BaseTaskResult):
175
+ def as_wire_task_result(self) -> _WireTaskResult:
176
+ """Return a `_WireTaskResult` from this instance."""
177
+ return _WireTaskResult(
178
+ host_name=self.host.name,
179
+ task_uuid=self.task._uuid,
180
+ return_data=self._return_data,
181
+ task_fields=self.task_fields,
182
+ )
112
183
 
113
- # FIXME: clean task_fields, _task and _host copies
114
- result = TaskResult(self._host, self._task, {}, self._task_fields)
184
+ def as_callback_task_result(self) -> CallbackTaskResult:
185
+ """Return a `CallbackTaskResult` from this instance."""
186
+ ignore: tuple[str, ...]
115
187
 
116
188
  # statuses are already reflected on the event type
117
- if result._task and result._task.action in C._ACTION_DEBUG:
189
+ if self.task and self.task.action in constants._ACTION_DEBUG:
118
190
  # debug is verbose by default to display vars, no need to add invocation
119
191
  ignore = _IGNORE + ('invocation',)
120
192
  else:
121
193
  ignore = _IGNORE
122
194
 
123
- subset = {}
195
+ subset: dict[str, dict[str, object]] = {}
196
+
124
197
  # preserve subset for later
125
- for sub in _SUB_PRESERVE:
126
- if sub in self._result:
127
- subset[sub] = {}
128
- for key in _SUB_PRESERVE[sub]:
129
- if key in self._result[sub]:
130
- subset[sub][key] = self._result[sub][key]
198
+ for sub, sub_keys in _SUB_PRESERVE.items():
199
+ sub_data = self._return_data.get(sub)
200
+
201
+ if isinstance(sub_data, dict):
202
+ subset[sub] = {key: value for key, value in sub_data.items() if key in sub_keys}
131
203
 
132
204
  # DTFIX-FUTURE: is checking no_log here redundant now that we use _ansible_no_log everywhere?
133
- if isinstance(self._task.no_log, bool) and self._task.no_log or self._result.get('_ansible_no_log'):
134
- censored_result = censor_result(self._result)
205
+ if isinstance(self.task.no_log, bool) and self.task.no_log or self._return_data.get('_ansible_no_log'):
206
+ censored_result = censor_result(self._return_data)
135
207
 
136
- if results := self._result.get('results'):
208
+ if self._loop_results:
137
209
  # maintain shape for loop results so callback behavior recognizes a loop was performed
138
- censored_result.update(results=[censor_result(item) if item.get('_ansible_no_log') else item for item in results])
210
+ censored_result.update(results=[
211
+ censor_result(loop_res) if isinstance(loop_res, dict) and loop_res.get('_ansible_no_log') else loop_res for loop_res in self._loop_results
212
+ ])
139
213
 
140
- result._result = censored_result
141
- elif self._result:
142
- result._result = module_response_deepcopy(self._result)
143
-
144
- # actually remove
145
- for remove_key in ignore:
146
- if remove_key in result._result:
147
- del result._result[remove_key]
214
+ return_data = censored_result
215
+ elif self._return_data:
216
+ return_data = {k: v for k, v in module_response_deepcopy(self._return_data).items() if k not in ignore}
148
217
 
149
218
  # remove almost ALL internal keys, keep ones relevant to callback
150
- strip_internal_keys(result._result, exceptions=CLEAN_EXCEPTIONS)
219
+ strip_internal_keys(return_data, exceptions=CLEAN_EXCEPTIONS)
220
+ else:
221
+ return_data = {}
151
222
 
152
223
  # keep subset
153
- result._result.update(subset)
224
+ return_data.update(subset)
225
+
226
+ return CallbackTaskResult(self.host, self.task, return_data, self.task_fields)
227
+
228
+
229
+ @t.final
230
+ class CallbackTaskResult(_BaseTaskResult):
231
+ """Public contract of TaskResult """
232
+
233
+ # DTFIX-RELEASE: find a better home for this since it's public API
234
+
235
+ @property
236
+ def _result(self) -> _c.MutableMapping[str, t.Any]:
237
+ """Use the `result` property when supporting only ansible-core 2.19 or later."""
238
+ # deprecated: description='Deprecate `_result` in favor of `result`' core_version='2.23'
239
+ return self.result
240
+
241
+ @functools.cached_property
242
+ def result(self) -> _c.MutableMapping[str, t.Any]:
243
+ """
244
+ Returns a cached copy of the task result dictionary for consumption by callbacks.
245
+ Internal custom types are transformed to native Python types to facilitate access and serialization.
246
+ """
247
+ return t.cast(_c.MutableMapping[str, t.Any], _vars.transform_to_native_types(self._return_data))
248
+
154
249
 
155
- return result
250
+ TaskResult = CallbackTaskResult
251
+ """Compatibility name for the pre-2.19 callback-shaped TaskResult passed to callbacks."""
156
252
 
157
253
 
158
- def censor_result(result: dict[str, t.Any]) -> dict[str, t.Any]:
254
+ def censor_result(result: _c.Mapping[str, t.Any]) -> dict[str, t.Any]:
159
255
  censored_result = {key: value for key in _PRESERVE if (value := result.get(key, ...)) is not ...}
160
256
  censored_result.update(censored="the output has been hidden due to the fact that 'no_log: true' was specified for this result")
161
257
 
@@ -30,6 +30,7 @@ from random import shuffle
30
30
 
31
31
  from ansible import constants as C
32
32
  from ansible._internal import _json, _wrapt
33
+ from ansible._internal._json import EncryptedStringBehavior
33
34
  from ansible.errors import AnsibleError, AnsibleOptionsError
34
35
  from ansible.inventory.data import InventoryData
35
36
  from ansible.module_utils.six import string_types
@@ -787,7 +788,7 @@ class _InventoryDataWrapper(_wrapt.ObjectProxy):
787
788
  return _json.AnsibleVariableVisitor(
788
789
  trusted_as_template=self._target_plugin.trusted_by_default,
789
790
  origin=self._default_origin,
790
- allow_encrypted_string=True,
791
+ encrypted_string_behavior=EncryptedStringBehavior.PRESERVE,
791
792
  )
792
793
 
793
794
  def set_variable(self, entity: str, varname: str, value: t.Any) -> None:
@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.19.0b1'
20
+ __version__ = '2.19.0b2'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "What Is and What Should Never Be"
@@ -53,9 +53,7 @@ try:
53
53
  except ImportError:
54
54
  HAS_SYSLOG = False
55
55
 
56
- # deprecated: description='types.EllipsisType is available in Python 3.10+' python_version='3.9'
57
- if t.TYPE_CHECKING:
58
- from builtins import ellipsis
56
+ _UNSET = t.cast(t.Any, object())
59
57
 
60
58
  try:
61
59
  from systemd import journal, daemon as systemd_daemon
@@ -341,7 +339,7 @@ def _load_params():
341
339
  except Exception as ex:
342
340
  raise Exception("Failed to decode JSON module parameters.") from ex
343
341
 
344
- if (ansible_module_args := params.get('ANSIBLE_MODULE_ARGS', ...)) is ...:
342
+ if (ansible_module_args := params.get('ANSIBLE_MODULE_ARGS', _UNSET)) is _UNSET:
345
343
  raise Exception("ANSIBLE_MODULE_ARGS not provided.")
346
344
 
347
345
  global _PARSED_MODULE_ARGS
@@ -1459,7 +1457,7 @@ class AnsibleModule(object):
1459
1457
  self._return_formatted(kwargs)
1460
1458
  sys.exit(0)
1461
1459
 
1462
- def fail_json(self, msg: str, *, exception: BaseException | str | ellipsis | None = ..., **kwargs) -> t.NoReturn:
1460
+ def fail_json(self, msg: str, *, exception: BaseException | str | None = _UNSET, **kwargs) -> t.NoReturn:
1463
1461
  """
1464
1462
  Return from the module with an error message and optional exception/traceback detail.
1465
1463
  A traceback will only be included in the result if error traceback capturing has been enabled.
@@ -1498,7 +1496,7 @@ class AnsibleModule(object):
1498
1496
 
1499
1497
  if isinstance(exception, str):
1500
1498
  formatted_traceback = exception
1501
- elif exception is ... and (current_exception := t.cast(t.Optional[BaseException], sys.exc_info()[1])):
1499
+ elif exception is _UNSET and (current_exception := t.cast(t.Optional[BaseException], sys.exc_info()[1])):
1502
1500
  formatted_traceback = _traceback.maybe_extract_traceback(current_exception, _traceback.TracebackEvent.ERROR)
1503
1501
  else:
1504
1502
  formatted_traceback = _traceback.maybe_capture_traceback(_traceback.TracebackEvent.ERROR)
@@ -11,7 +11,7 @@ from ansible.module_utils._internal import _traceback, _plugin_exec_context
11
11
  from ansible.module_utils.common import messages as _messages
12
12
  from ansible.module_utils import _internal
13
13
 
14
- _UNSET = _t.cast(_t.Any, ...)
14
+ _UNSET = _t.cast(_t.Any, object())
15
15
 
16
16
 
17
17
  def warn(warning: str) -> None:
@@ -0,0 +1,40 @@
1
+ # (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
2
+ #
3
+ # This file is part of Ansible
4
+ #
5
+ # Ansible is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+ #
10
+ # Ansible is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+ #
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+
22
+ from ansible.utils.display import Display
23
+
24
+ Display().deprecated(f'{__name__!r} is deprecated.', version='2.23', help_text='Call `json.dumps` directly instead.')
25
+
26
+
27
+ def jsonify(result, format=False):
28
+ """Format JSON output."""
29
+
30
+ if result is None:
31
+ return "{}"
32
+
33
+ indent = None
34
+ if format:
35
+ indent = 4
36
+
37
+ try:
38
+ return json.dumps(result, sort_keys=True, indent=indent, ensure_ascii=False)
39
+ except UnicodeDecodeError:
40
+ return json.dumps(result, sort_keys=True, indent=indent)
@@ -8,25 +8,36 @@ from ansible.module_utils._internal import _datatag
8
8
  from ansible.module_utils.common.text import converters as _converters
9
9
  from ansible.parsing import vault as _vault
10
10
 
11
+ _UNSET = _t.cast(_t.Any, object())
12
+
11
13
 
12
14
  class _AnsibleMapping(dict):
13
15
  """Backwards compatibility type."""
14
16
 
15
- def __new__(cls, value):
16
- return _datatag.AnsibleTagHelper.tag_copy(value, dict(value))
17
+ def __new__(cls, value=_UNSET, /, **kwargs):
18
+ if value is _UNSET:
19
+ return dict(**kwargs)
20
+
21
+ return _datatag.AnsibleTagHelper.tag_copy(value, dict(value, **kwargs))
17
22
 
18
23
 
19
24
  class _AnsibleUnicode(str):
20
25
  """Backwards compatibility type."""
21
26
 
22
- def __new__(cls, value):
23
- return _datatag.AnsibleTagHelper.tag_copy(value, str(value))
27
+ def __new__(cls, object=_UNSET, **kwargs):
28
+ if object is _UNSET:
29
+ return str(**kwargs)
30
+
31
+ return _datatag.AnsibleTagHelper.tag_copy(object, str(object, **kwargs))
24
32
 
25
33
 
26
34
  class _AnsibleSequence(list):
27
35
  """Backwards compatibility type."""
28
36
 
29
- def __new__(cls, value):
37
+ def __new__(cls, value=_UNSET, /):
38
+ if value is _UNSET:
39
+ return list()
40
+
30
41
  return _datatag.AnsibleTagHelper.tag_copy(value, list(value))
31
42
 
32
43