ansible-core 2.19.0b1__py3-none-any.whl → 2.19.0b3__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 (92) hide show
  1. ansible/_internal/_ansiballz.py +1 -4
  2. ansible/_internal/_collection_proxy.py +47 -0
  3. ansible/_internal/_errors/_handler.py +4 -4
  4. ansible/_internal/_json/__init__.py +47 -4
  5. ansible/_internal/_json/_profiles/_legacy.py +2 -3
  6. ansible/_internal/_templating/_datatag.py +3 -4
  7. ansible/_internal/_templating/_engine.py +6 -1
  8. ansible/_internal/_templating/_jinja_bits.py +4 -4
  9. ansible/_internal/_templating/_jinja_plugins.py +7 -17
  10. ansible/cli/__init__.py +12 -5
  11. ansible/cli/arguments/option_helpers.py +4 -1
  12. ansible/cli/doc.py +14 -8
  13. ansible/config/base.yml +17 -20
  14. ansible/config/manager.py +2 -2
  15. ansible/constants.py +0 -62
  16. ansible/errors/__init__.py +6 -2
  17. ansible/executor/module_common.py +11 -7
  18. ansible/executor/process/worker.py +31 -26
  19. ansible/executor/task_executor.py +38 -31
  20. ansible/executor/task_queue_manager.py +62 -52
  21. ansible/executor/task_result.py +168 -72
  22. ansible/galaxy/api.py +1 -1
  23. ansible/galaxy/collection/__init__.py +3 -3
  24. ansible/inventory/manager.py +2 -1
  25. ansible/module_utils/_internal/_ansiballz.py +4 -30
  26. ansible/module_utils/_internal/_datatag/_tags.py +3 -25
  27. ansible/module_utils/_internal/_deprecator.py +134 -0
  28. ansible/module_utils/_internal/_plugin_info.py +25 -0
  29. ansible/module_utils/_internal/_validation.py +14 -0
  30. ansible/module_utils/ansible_release.py +1 -1
  31. ansible/module_utils/basic.py +68 -23
  32. ansible/module_utils/common/arg_spec.py +8 -3
  33. ansible/module_utils/common/messages.py +40 -23
  34. ansible/module_utils/common/process.py +0 -1
  35. ansible/module_utils/common/respawn.py +0 -7
  36. ansible/module_utils/common/warnings.py +13 -13
  37. ansible/module_utils/datatag.py +13 -13
  38. ansible/modules/async_status.py +1 -1
  39. ansible/modules/dnf5.py +1 -1
  40. ansible/modules/get_url.py +1 -1
  41. ansible/parsing/utils/jsonify.py +40 -0
  42. ansible/parsing/yaml/objects.py +16 -5
  43. ansible/playbook/included_file.py +25 -12
  44. ansible/playbook/task.py +0 -2
  45. ansible/plugins/__init__.py +18 -8
  46. ansible/plugins/action/__init__.py +6 -14
  47. ansible/plugins/action/gather_facts.py +2 -4
  48. ansible/plugins/callback/__init__.py +173 -86
  49. ansible/plugins/callback/default.py +79 -79
  50. ansible/plugins/callback/junit.py +20 -19
  51. ansible/plugins/callback/minimal.py +17 -17
  52. ansible/plugins/callback/oneline.py +23 -16
  53. ansible/plugins/callback/tree.py +13 -6
  54. ansible/plugins/connection/local.py +1 -1
  55. ansible/plugins/connection/paramiko_ssh.py +9 -2
  56. ansible/plugins/doc_fragments/action_core.py +1 -1
  57. ansible/plugins/filter/core.py +12 -2
  58. ansible/plugins/inventory/__init__.py +2 -2
  59. ansible/plugins/loader.py +194 -130
  60. ansible/plugins/lookup/url.py +2 -2
  61. ansible/plugins/strategy/__init__.py +76 -82
  62. ansible/plugins/strategy/free.py +4 -4
  63. ansible/plugins/strategy/linear.py +11 -9
  64. ansible/plugins/test/core.py +1 -1
  65. ansible/release.py +1 -1
  66. ansible/template/__init__.py +8 -6
  67. ansible/utils/collection_loader/_collection_meta.py +5 -3
  68. ansible/utils/display.py +141 -79
  69. ansible/utils/py3compat.py +1 -7
  70. ansible/utils/ssh_functions.py +4 -1
  71. ansible/utils/vars.py +23 -0
  72. ansible/vars/clean.py +1 -1
  73. ansible/vars/manager.py +18 -27
  74. ansible/vars/plugins.py +4 -4
  75. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/METADATA +1 -1
  76. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/RECORD +89 -85
  77. ansible_test/_internal/commands/sanity/pylint.py +1 -0
  78. ansible_test/_internal/docker_util.py +4 -3
  79. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +475 -0
  80. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
  81. ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
  82. ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
  83. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
  84. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/Apache-License.txt +0 -0
  85. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/BSD-3-Clause.txt +0 -0
  86. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/COPYING +0 -0
  87. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/MIT-license.txt +0 -0
  88. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/PSF-license.txt +0 -0
  89. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/WHEEL +0 -0
  90. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/entry_points.txt +0 -0
  91. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/simplified_bsd.txt +0 -0
  92. {ansible_core-2.19.0b1.dist-info → ansible_core-2.19.0b3.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,9 @@ from ..module_utils.datatag import native_type_name
18
18
  from ansible._internal._datatag import _tags
19
19
  from .._internal._errors import _utils
20
20
 
21
+ if t.TYPE_CHECKING:
22
+ from ansible.plugins import loader as _t_loader
23
+
21
24
 
22
25
  class ExitCode(enum.IntEnum):
23
26
  SUCCESS = 0 # used by TQM, must be bit-flag safe
@@ -374,8 +377,9 @@ class _AnsibleActionDone(AnsibleAction):
374
377
  class AnsiblePluginError(AnsibleError):
375
378
  """Base class for Ansible plugin-related errors that do not need AnsibleError contextual data."""
376
379
 
377
- def __init__(self, message=None, plugin_load_context=None):
378
- super(AnsiblePluginError, self).__init__(message)
380
+ def __init__(self, message: str | None = None, plugin_load_context: _t_loader.PluginLoadContext | None = None, help_text: str | None = None) -> None:
381
+ super(AnsiblePluginError, self).__init__(message, help_text=help_text)
382
+
379
383
  self.plugin_load_context = plugin_load_context
380
384
 
381
385
 
@@ -39,7 +39,6 @@ from io import BytesIO
39
39
  from ansible._internal import _locking
40
40
  from ansible._internal._datatag import _utils
41
41
  from ansible.module_utils._internal import _dataclass_validation
42
- from ansible.module_utils.common.messages import PluginInfo
43
42
  from ansible.module_utils.common.yaml import yaml_load
44
43
  from ansible._internal._datatag._tags import Origin
45
44
  from ansible.module_utils.common.json import Direction, get_module_encoder
@@ -56,6 +55,7 @@ from ansible.template import Templar
56
55
  from ansible.utils.collection_loader._collection_finder import _get_collection_metadata, _nested_dict_get
57
56
  from ansible.module_utils._internal import _json, _ansiballz
58
57
  from ansible.module_utils import basic as _basic
58
+ from ansible.module_utils.common import messages as _messages
59
59
 
60
60
  if t.TYPE_CHECKING:
61
61
  from ansible import template as _template
@@ -434,7 +434,13 @@ class ModuleUtilLocatorBase:
434
434
  else:
435
435
  msg += '.'
436
436
 
437
- display.deprecated(msg, removal_version, removed, removal_date, self._collection_name)
437
+ display.deprecated( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
438
+ msg=msg,
439
+ version=removal_version,
440
+ removed=removed,
441
+ date=removal_date,
442
+ deprecator=_messages.PluginInfo._from_collection_name(self._collection_name),
443
+ )
438
444
  if 'redirect' in routing_entry:
439
445
  self.redirected = True
440
446
  source_pkg = '.'.join(name_parts)
@@ -944,7 +950,6 @@ class _CachedModule:
944
950
  def _find_module_utils(
945
951
  *,
946
952
  module_name: str,
947
- plugin: PluginInfo,
948
953
  b_module_data: bytes,
949
954
  module_path: str,
950
955
  module_args: dict[object, object],
@@ -1020,7 +1025,9 @@ def _find_module_utils(
1020
1025
  # People should start writing collections instead of modules in roles so we
1021
1026
  # may never fix this
1022
1027
  display.debug('ANSIBALLZ: Could not determine module FQN')
1023
- remote_module_fqn = 'ansible.modules.%s' % module_name
1028
+ # FIXME: add integration test to validate that builtins and legacy modules with the same name are tracked separately by the caching mechanism
1029
+ # FIXME: surrogate FQN should be unique per source path- role-packaged modules with name collisions can still be aliased
1030
+ remote_module_fqn = 'ansible.legacy.%s' % module_name
1024
1031
 
1025
1032
  if module_substyle == 'python':
1026
1033
  date_time = datetime.datetime.now(datetime.timezone.utc)
@@ -1126,7 +1133,6 @@ def _find_module_utils(
1126
1133
  module_fqn=remote_module_fqn,
1127
1134
  params=encoded_params,
1128
1135
  profile=module_metadata.serialization_profile,
1129
- plugin_info_dict=dataclasses.asdict(plugin),
1130
1136
  date_time=date_time,
1131
1137
  coverage_config=coverage_config,
1132
1138
  coverage_output=coverage_output,
@@ -1236,7 +1242,6 @@ def _extract_interpreter(b_module_data):
1236
1242
  def modify_module(
1237
1243
  *,
1238
1244
  module_name: str,
1239
- plugin: PluginInfo,
1240
1245
  module_path,
1241
1246
  module_args,
1242
1247
  templar,
@@ -1277,7 +1282,6 @@ def modify_module(
1277
1282
 
1278
1283
  module_bits = _find_module_utils(
1279
1284
  module_name=module_name,
1280
- plugin=plugin,
1281
1285
  b_module_data=b_module_data,
1282
1286
  module_path=module_path,
1283
1287
  module_args=module_args,
@@ -32,6 +32,7 @@ from ansible._internal import _task
32
32
  from ansible.errors import AnsibleConnectionFailure, AnsibleError
33
33
  from ansible.executor.task_executor import TaskExecutor
34
34
  from ansible.executor.task_queue_manager import FinalQueue, STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO
35
+ from ansible.executor.task_result import _RawTaskResult
35
36
  from ansible.inventory.host import Host
36
37
  from ansible.module_utils.common.collections import is_sequence
37
38
  from ansible.module_utils.common.text.converters import to_text
@@ -226,7 +227,7 @@ class WorkerProcess(multiprocessing_context.Process): # type: ignore[name-defin
226
227
  init_plugin_loader(cli_collections_path)
227
228
 
228
229
  try:
229
- # execute the task and build a TaskResult from the result
230
+ # execute the task and build a _RawTaskResult from the result
230
231
  display.debug("running TaskExecutor() for %s/%s" % (self._host, self._task))
231
232
  executor_result = TaskExecutor(
232
233
  self._host,
@@ -256,48 +257,52 @@ class WorkerProcess(multiprocessing_context.Process): # type: ignore[name-defin
256
257
  # put the result on the result queue
257
258
  display.debug("sending task result for task %s" % self._task._uuid)
258
259
  try:
259
- self._final_q.send_task_result(
260
- self._host.name,
261
- self._task._uuid,
262
- executor_result,
260
+ self._final_q.send_task_result(_RawTaskResult(
261
+ host=self._host,
262
+ task=self._task,
263
+ return_data=executor_result,
263
264
  task_fields=self._task.dump_attrs(),
264
- )
265
+ ))
265
266
  except Exception as ex:
266
267
  try:
267
268
  raise AnsibleError("Task result omitted due to queue send failure.") from ex
268
269
  except Exception as ex_wrapper:
269
- self._final_q.send_task_result(
270
- self._host.name,
271
- self._task._uuid,
272
- ActionBase.result_dict_from_exception(ex_wrapper), # Overriding the task result, to represent the failure
273
- {}, # The failure pickling may have been caused by the task attrs, omit for safety
274
- )
270
+ self._final_q.send_task_result(_RawTaskResult(
271
+ host=self._host,
272
+ task=self._task,
273
+ return_data=ActionBase.result_dict_from_exception(ex_wrapper), # Overriding the task result, to represent the failure
274
+ task_fields={}, # The failure pickling may have been caused by the task attrs, omit for safety
275
+ ))
275
276
 
276
277
  display.debug("done sending task result for task %s" % self._task._uuid)
277
278
 
278
- except AnsibleConnectionFailure:
279
+ except AnsibleConnectionFailure as ex:
280
+ return_data = ActionBase.result_dict_from_exception(ex)
281
+ return_data.pop('failed')
282
+ return_data.update(unreachable=True)
283
+
279
284
  self._host.vars = dict()
280
285
  self._host.groups = []
281
- self._final_q.send_task_result(
282
- self._host.name,
283
- self._task._uuid,
284
- dict(unreachable=True),
286
+ self._final_q.send_task_result(_RawTaskResult(
287
+ host=self._host,
288
+ task=self._task,
289
+ return_data=return_data,
285
290
  task_fields=self._task.dump_attrs(),
286
- )
291
+ ))
287
292
 
288
- except Exception as e:
289
- if not isinstance(e, (IOError, EOFError, KeyboardInterrupt, SystemExit)) or isinstance(e, TemplateNotFound):
293
+ except Exception as ex:
294
+ if not isinstance(ex, (IOError, EOFError, KeyboardInterrupt, SystemExit)) or isinstance(ex, TemplateNotFound):
290
295
  try:
291
296
  self._host.vars = dict()
292
297
  self._host.groups = []
293
- self._final_q.send_task_result(
294
- self._host.name,
295
- self._task._uuid,
296
- dict(failed=True, exception=to_text(traceback.format_exc()), stdout=''),
298
+ self._final_q.send_task_result(_RawTaskResult(
299
+ host=self._host,
300
+ task=self._task,
301
+ return_data=ActionBase.result_dict_from_exception(ex),
297
302
  task_fields=self._task.dump_attrs(),
298
- )
303
+ ))
299
304
  except Exception:
300
- display.debug(u"WORKER EXCEPTION: %s" % to_text(e))
305
+ display.debug(u"WORKER EXCEPTION: %s" % to_text(ex))
301
306
  display.debug(u"WORKER TRACEBACK: %s" % to_text(traceback.format_exc()))
302
307
  finally:
303
308
  self._clean_up()
@@ -20,10 +20,9 @@ from ansible.errors import (
20
20
  AnsibleError, AnsibleParserError, AnsibleUndefinedVariable, AnsibleConnectionFailure, AnsibleActionFail, AnsibleActionSkip, AnsibleTaskError,
21
21
  AnsibleValueOmittedError,
22
22
  )
23
- from ansible.executor.task_result import TaskResult
23
+ from ansible.executor.task_result import _RawTaskResult
24
24
  from ansible._internal._datatag import _utils
25
- from ansible.module_utils._internal._plugin_exec_context import PluginExecContext
26
- from ansible.module_utils.common.messages import Detail, WarningSummary, DeprecationSummary
25
+ from ansible.module_utils.common.messages import Detail, WarningSummary, DeprecationSummary, PluginInfo
27
26
  from ansible.module_utils.datatag import native_type_name
28
27
  from ansible._internal._datatag._tags import TrustedAsTemplate
29
28
  from ansible.module_utils.parsing.convert_bool import boolean
@@ -44,6 +43,9 @@ from ansible.vars.clean import namespace_facts, clean_facts
44
43
  from ansible.vars.manager import _deprecate_top_level_fact
45
44
  from ansible._internal._errors import _captured
46
45
 
46
+ if t.TYPE_CHECKING:
47
+ from ansible.executor.task_queue_manager import FinalQueue
48
+
47
49
  display = Display()
48
50
 
49
51
 
@@ -79,7 +81,7 @@ class TaskExecutor:
79
81
  class.
80
82
  """
81
83
 
82
- def __init__(self, host, task: Task, job_vars, play_context, loader, shared_loader_obj, final_q, variable_manager):
84
+ def __init__(self, host, task: Task, job_vars, play_context, loader, shared_loader_obj, final_q: FinalQueue, variable_manager):
83
85
  self._host = host
84
86
  self._task = task
85
87
  self._job_vars = job_vars
@@ -361,10 +363,10 @@ class TaskExecutor:
361
363
  if self._connection and not isinstance(self._connection, string_types):
362
364
  task_fields['connection'] = getattr(self._connection, 'ansible_name')
363
365
 
364
- tr = TaskResult(
365
- self._host.name,
366
- self._task._uuid,
367
- res,
366
+ tr = _RawTaskResult(
367
+ host=self._host,
368
+ task=self._task,
369
+ return_data=res,
368
370
  task_fields=task_fields,
369
371
  )
370
372
 
@@ -637,8 +639,8 @@ class TaskExecutor:
637
639
  if self._task.timeout:
638
640
  old_sig = signal.signal(signal.SIGALRM, task_timeout)
639
641
  signal.alarm(self._task.timeout)
640
- with PluginExecContext(self._handler):
641
- result = self._handler.run(task_vars=vars_copy)
642
+
643
+ result = self._handler.run(task_vars=vars_copy)
642
644
 
643
645
  # DTFIX-RELEASE: nuke this, it hides a lot of error detail- remove the active exception propagation hack from AnsibleActionFail at the same time
644
646
  except (AnsibleActionFail, AnsibleActionSkip) as e:
@@ -666,17 +668,23 @@ class TaskExecutor:
666
668
  if result.get('failed'):
667
669
  self._final_q.send_callback(
668
670
  'v2_runner_on_async_failed',
669
- TaskResult(self._host.name,
670
- self._task._uuid,
671
- result,
672
- task_fields=self._task.dump_attrs()))
671
+ _RawTaskResult(
672
+ host=self._host,
673
+ task=self._task,
674
+ return_data=result,
675
+ task_fields=self._task.dump_attrs(),
676
+ ),
677
+ )
673
678
  else:
674
679
  self._final_q.send_callback(
675
680
  'v2_runner_on_async_ok',
676
- TaskResult(self._host.name,
677
- self._task._uuid,
678
- result,
679
- task_fields=self._task.dump_attrs()))
681
+ _RawTaskResult(
682
+ host=self._host,
683
+ task=self._task,
684
+ return_data=result,
685
+ task_fields=self._task.dump_attrs(),
686
+ ),
687
+ )
680
688
 
681
689
  if 'ansible_facts' in result and self._task.action not in C._ACTION_DEBUG:
682
690
  if self._task.action in C._ACTION_WITH_CLEAN_FACTS:
@@ -756,12 +764,12 @@ class TaskExecutor:
756
764
  display.debug('Retrying task, attempt %d of %d' % (attempt, retries))
757
765
  self._final_q.send_callback(
758
766
  'v2_runner_retry',
759
- TaskResult(
760
- self._host.name,
761
- self._task._uuid,
762
- result,
767
+ _RawTaskResult(
768
+ host=self._host,
769
+ task=self._task,
770
+ return_data=result,
763
771
  task_fields=self._task.dump_attrs()
764
- )
772
+ ),
765
773
  )
766
774
  time.sleep(delay)
767
775
  self._handler = self._get_action_handler(templar=templar)
@@ -835,13 +843,12 @@ class TaskExecutor:
835
843
  if not isinstance(deprecation, DeprecationSummary):
836
844
  # translate non-DeprecationMessageDetail message dicts
837
845
  try:
838
- if deprecation.pop('collection_name', ...) is not ...:
846
+ if (collection_name := deprecation.pop('collection_name', ...)) is not ...:
839
847
  # deprecated: description='enable the deprecation message for collection_name' core_version='2.23'
848
+ # CAUTION: This deprecation cannot be enabled until the replacement (deprecator) has been documented, and the schema finalized.
840
849
  # self.deprecated('The `collection_name` key in the `deprecations` dictionary is deprecated.', version='2.27')
841
- pass
850
+ deprecation.update(deprecator=PluginInfo._from_collection_name(collection_name))
842
851
 
843
- # DTFIX-RELEASE: when plugin isn't set, do it at the boundary where we receive the module/action results
844
- # that may even allow us to never set it in modules/actions directly and to populate it at the boundary
845
852
  deprecation = DeprecationSummary(
846
853
  details=(
847
854
  Detail(msg=deprecation.pop('msg')),
@@ -926,10 +933,10 @@ class TaskExecutor:
926
933
  time_left -= self._task.poll
927
934
  self._final_q.send_callback(
928
935
  'v2_runner_on_async_poll',
929
- TaskResult(
930
- self._host.name,
931
- async_task._uuid,
932
- async_result,
936
+ _RawTaskResult(
937
+ host=self._host,
938
+ task=async_task,
939
+ return_data=async_result,
933
940
  task_fields=async_task.dump_attrs(),
934
941
  ),
935
942
  )
@@ -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