ansible-core 2.19.0b2__py3-none-any.whl → 2.19.0b4__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 (85) hide show
  1. ansible/_internal/__init__.py +1 -1
  2. ansible/_internal/_ansiballz.py +1 -4
  3. ansible/_internal/_json/__init__.py +1 -1
  4. ansible/_internal/_templating/_datatag.py +3 -4
  5. ansible/_internal/_templating/_engine.py +6 -1
  6. ansible/_internal/_templating/_jinja_plugins.py +2 -6
  7. ansible/_internal/_testing.py +26 -0
  8. ansible/cli/__init__.py +3 -2
  9. ansible/cli/arguments/option_helpers.py +10 -3
  10. ansible/cli/doc.py +0 -1
  11. ansible/config/base.yml +5 -23
  12. ansible/config/manager.py +144 -103
  13. ansible/constants.py +1 -63
  14. ansible/errors/__init__.py +6 -2
  15. ansible/executor/module_common.py +11 -7
  16. ansible/executor/task_executor.py +6 -8
  17. ansible/galaxy/api.py +1 -1
  18. ansible/galaxy/collection/__init__.py +3 -3
  19. ansible/inventory/manager.py +1 -0
  20. ansible/module_utils/_internal/_ansiballz.py +4 -30
  21. ansible/module_utils/_internal/_datatag/_tags.py +3 -25
  22. ansible/module_utils/_internal/_deprecator.py +134 -0
  23. ansible/module_utils/_internal/_plugin_info.py +25 -0
  24. ansible/module_utils/_internal/_validation.py +14 -0
  25. ansible/module_utils/ansible_release.py +1 -1
  26. ansible/module_utils/basic.py +64 -17
  27. ansible/module_utils/common/arg_spec.py +8 -3
  28. ansible/module_utils/common/messages.py +40 -23
  29. ansible/module_utils/common/process.py +0 -1
  30. ansible/module_utils/common/respawn.py +0 -7
  31. ansible/module_utils/common/warnings.py +13 -13
  32. ansible/module_utils/datatag.py +13 -13
  33. ansible/module_utils/facts/virtual/linux.py +1 -1
  34. ansible/module_utils/parsing/convert_bool.py +6 -0
  35. ansible/modules/assemble.py +4 -4
  36. ansible/modules/async_status.py +1 -1
  37. ansible/modules/cron.py +3 -5
  38. ansible/modules/dnf5.py +2 -1
  39. ansible/modules/get_url.py +1 -1
  40. ansible/modules/git.py +1 -6
  41. ansible/modules/pip.py +2 -4
  42. ansible/modules/sysvinit.py +3 -3
  43. ansible/playbook/task.py +0 -2
  44. ansible/plugins/__init__.py +18 -8
  45. ansible/plugins/action/__init__.py +7 -15
  46. ansible/plugins/action/gather_facts.py +2 -4
  47. ansible/plugins/action/template.py +3 -0
  48. ansible/plugins/callback/oneline.py +7 -1
  49. ansible/plugins/callback/tree.py +7 -1
  50. ansible/plugins/connection/local.py +1 -1
  51. ansible/plugins/connection/paramiko_ssh.py +9 -2
  52. ansible/plugins/doc_fragments/action_core.py +1 -1
  53. ansible/plugins/filter/core.py +4 -1
  54. ansible/plugins/inventory/__init__.py +2 -2
  55. ansible/plugins/loader.py +197 -132
  56. ansible/plugins/lookup/url.py +2 -2
  57. ansible/plugins/strategy/__init__.py +6 -6
  58. ansible/release.py +1 -1
  59. ansible/template/__init__.py +1 -1
  60. ansible/utils/collection_loader/__init__.py +2 -0
  61. ansible/utils/collection_loader/_collection_meta.py +5 -3
  62. ansible/utils/display.py +137 -71
  63. ansible/utils/plugin_docs.py +2 -1
  64. ansible/utils/py3compat.py +1 -7
  65. ansible/utils/ssh_functions.py +4 -1
  66. ansible/vars/manager.py +18 -10
  67. ansible/vars/plugins.py +4 -4
  68. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/METADATA +3 -2
  69. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/RECORD +82 -79
  70. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/WHEEL +1 -1
  71. ansible_test/_internal/commands/sanity/pylint.py +1 -0
  72. ansible_test/_internal/docker_util.py +4 -3
  73. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +486 -0
  74. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
  75. ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
  76. ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
  77. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
  78. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/entry_points.txt +0 -0
  79. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses}/COPYING +0 -0
  80. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/Apache-License.txt +0 -0
  81. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
  82. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/MIT-license.txt +0 -0
  83. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/PSF-license.txt +0 -0
  84. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
  85. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b4.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@
6
6
  from __future__ import annotations
7
7
 
8
8
  import atexit
9
- import dataclasses
10
9
  import importlib.util
11
10
  import json
12
11
  import os
@@ -15,17 +14,14 @@ import sys
15
14
  import typing as t
16
15
 
17
16
  from . import _errors
18
- from ._plugin_exec_context import PluginExecContext, HasPluginInfo
19
17
  from .. import basic
20
18
  from ..common.json import get_module_encoder, Direction
21
- from ..common.messages import PluginInfo
22
19
 
23
20
 
24
21
  def run_module(
25
22
  *,
26
23
  json_params: bytes,
27
24
  profile: str,
28
- plugin_info_dict: dict[str, object],
29
25
  module_fqn: str,
30
26
  modlib_path: str,
31
27
  init_globals: dict[str, t.Any] | None = None,
@@ -38,7 +34,6 @@ def run_module(
38
34
  _run_module(
39
35
  json_params=json_params,
40
36
  profile=profile,
41
- plugin_info_dict=plugin_info_dict,
42
37
  module_fqn=module_fqn,
43
38
  modlib_path=modlib_path,
44
39
  init_globals=init_globals,
@@ -80,7 +75,6 @@ def _run_module(
80
75
  *,
81
76
  json_params: bytes,
82
77
  profile: str,
83
- plugin_info_dict: dict[str, object],
84
78
  module_fqn: str,
85
79
  modlib_path: str,
86
80
  init_globals: dict[str, t.Any] | None = None,
@@ -92,12 +86,11 @@ def _run_module(
92
86
  init_globals = init_globals or {}
93
87
  init_globals.update(_module_fqn=module_fqn, _modlib_path=modlib_path)
94
88
 
95
- with PluginExecContext(_ModulePluginWrapper(PluginInfo._from_dict(plugin_info_dict))):
96
- # Run the module. By importing it as '__main__', it executes as a script.
97
- runpy.run_module(mod_name=module_fqn, init_globals=init_globals, run_name='__main__', alter_sys=True)
89
+ # Run the module. By importing it as '__main__', it executes as a script.
90
+ runpy.run_module(mod_name=module_fqn, init_globals=init_globals, run_name='__main__', alter_sys=True)
98
91
 
99
- # An Ansible module must print its own results and exit. If execution reaches this point, that did not happen.
100
- raise RuntimeError('New-style module did not handle its own exit.')
92
+ # An Ansible module must print its own results and exit. If execution reaches this point, that did not happen.
93
+ raise RuntimeError('New-style module did not handle its own exit.')
101
94
 
102
95
 
103
96
  def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
@@ -112,22 +105,3 @@ def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
112
105
  print(json.dumps(result, cls=encoder)) # pylint: disable=ansible-bad-function
113
106
 
114
107
  sys.exit(1) # pylint: disable=ansible-bad-function
115
-
116
-
117
- @dataclasses.dataclass(frozen=True)
118
- class _ModulePluginWrapper(HasPluginInfo):
119
- """Modules aren't plugin instances; this adapter implements the `HasPluginInfo` protocol to allow `PluginExecContext` infra to work with modules."""
120
-
121
- plugin: PluginInfo
122
-
123
- @property
124
- def _load_name(self) -> str:
125
- return self.plugin.requested_name
126
-
127
- @property
128
- def ansible_name(self) -> str:
129
- return self.plugin.resolved_name
130
-
131
- @property
132
- def plugin_type(self) -> str:
133
- return self.plugin.type
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import dataclasses
4
- import datetime
5
4
  import typing as t
6
5
 
7
6
  from ansible.module_utils.common import messages as _messages
@@ -12,27 +11,6 @@ from ansible.module_utils._internal import _datatag
12
11
  class Deprecated(_datatag.AnsibleDatatagBase):
13
12
  msg: str
14
13
  help_text: t.Optional[str] = None
15
- removal_date: t.Optional[datetime.date] = None
16
- removal_version: t.Optional[str] = None
17
- plugin: t.Optional[_messages.PluginInfo] = None
18
-
19
- @classmethod
20
- def _from_dict(cls, d: t.Dict[str, t.Any]) -> Deprecated:
21
- source = d
22
- removal_date = source.get('removal_date')
23
-
24
- if removal_date is not None:
25
- source = source.copy()
26
- source['removal_date'] = datetime.date.fromisoformat(removal_date)
27
-
28
- return cls(**source)
29
-
30
- def _as_dict(self) -> t.Dict[str, t.Any]:
31
- # deprecated: description='no-args super() with slotted dataclass requires 3.14+' python_version='3.13'
32
- # see: https://github.com/python/cpython/pull/124455
33
- value = super(Deprecated, self)._as_dict()
34
-
35
- if self.removal_date is not None:
36
- value['removal_date'] = self.removal_date.isoformat()
37
-
38
- return value
14
+ date: t.Optional[str] = None
15
+ version: t.Optional[str] = None
16
+ deprecator: t.Optional[_messages.PluginInfo] = None
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import re
5
+ import pathlib
6
+ import sys
7
+ import typing as t
8
+
9
+ from ansible.module_utils.common.messages import PluginInfo
10
+
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
+
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."""
16
+
17
+ INDETERMINATE_DEPRECATOR: t.Final = PluginInfo(resolved_name='indeterminate', type='indeterminate')
18
+ """Singleton `PluginInfo` instance for indeterminate deprecator."""
19
+
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."""
43
+
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."""
51
+
52
+
53
+ def get_best_deprecator(*, deprecator: PluginInfo | None = None, collection_name: str | None = None) -> PluginInfo:
54
+ """Return the best-available `PluginInfo` for the caller of this method."""
55
+ _skip_stackwalk = True
56
+
57
+ if deprecator and collection_name:
58
+ raise ValueError('Specify only one of `deprecator` or `collection_name`.')
59
+
60
+ return deprecator or PluginInfo._from_collection_name(collection_name) or get_caller_plugin_info() or INDETERMINATE_DEPRECATOR
61
+
62
+
63
+ def get_caller_plugin_info() -> PluginInfo | None:
64
+ """Try to get `PluginInfo` for the caller of this method, ignoring marked infrastructure stack frames."""
65
+ _skip_stackwalk = True
66
+
67
+ if frame_info := next((frame_info for frame_info in inspect.stack() if '_skip_stackwalk' not in frame_info.frame.f_locals), None):
68
+ return _path_as_core_plugininfo(frame_info.filename) or _path_as_collection_plugininfo(frame_info.filename)
69
+
70
+ return None # pragma: nocover
71
+
72
+
73
+ def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
74
+ """Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
75
+ try:
76
+ relpath = str(pathlib.Path(path).relative_to(_ansible_module_base_path))
77
+ except ValueError:
78
+ return None # not ansible-core
79
+
80
+ namespace = 'ansible.builtin'
81
+
82
+ if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
83
+ plugin_name = match.group("plugin_name")
84
+ plugin_type = match.group("plugin_type")
85
+
86
+ if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
87
+ # The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
88
+ # We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
89
+ # Callers in this case need to identify the deprecating plugin name, otherwise only ansible-core will be reported.
90
+ # Reporting ansible-core is never wrong, it just may be missing an additional detail (plugin name) in the "on behalf of" case.
91
+ return ANSIBLE_CORE_DEPRECATOR
92
+ elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
93
+ # AnsiballZ Python package for core modules
94
+ plugin_name = match.group("module_name")
95
+ plugin_type = "module"
96
+ elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
97
+ # AnsiballZ Python package for non-core library/role modules
98
+ namespace = 'ansible.legacy'
99
+
100
+ plugin_name = match.group("module_name")
101
+ plugin_type = "module"
102
+ else:
103
+ return ANSIBLE_CORE_DEPRECATOR # non-plugin core path, safe to use ansible-core for the same reason as the non-deprecator plugin type case above
104
+
105
+ name = f'{namespace}.{plugin_name}'
106
+
107
+ return PluginInfo(resolved_name=name, type=plugin_type)
108
+
109
+
110
+ def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
111
+ """Return a `PluginInfo` instance if the provided `path` refers to a collection plugin."""
112
+ 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
+ return None
114
+
115
+ plugin_type = match.group('plugin_type')
116
+
117
+ if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
118
+ # We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
119
+ # To keep things simple we'll fall back to just identifying the namespace and collection.
120
+ # 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'))))
122
+
123
+ if plugin_type == 'modules':
124
+ plugin_type = 'module'
125
+
126
+ if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
127
+ # The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
128
+ # We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
129
+ # Callers in this case need to identify the deprecator to avoid ambiguity, since it could be the same collection or another collection.
130
+ return INDETERMINATE_DEPRECATOR
131
+
132
+ name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
133
+
134
+ return PluginInfo(resolved_name=name, type=plugin_type)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import typing as t
4
+
5
+ from ..common import messages as _messages
6
+
7
+
8
+ class HasPluginInfo(t.Protocol):
9
+ """Protocol to type-annotate and expose PluginLoader-set values."""
10
+
11
+ @property
12
+ def ansible_name(self) -> str | None:
13
+ """Fully resolved plugin name."""
14
+
15
+ @property
16
+ def plugin_type(self) -> str:
17
+ """Plugin type name."""
18
+
19
+
20
+ def get_plugin_info(value: HasPluginInfo) -> _messages.PluginInfo:
21
+ """Utility method that returns a `PluginInfo` from an object implementing the `HasPluginInfo` protocol."""
22
+ return _messages.PluginInfo(
23
+ resolved_name=value.ansible_name,
24
+ type=value.plugin_type,
25
+ )
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import keyword
4
+
5
+
6
+ def validate_collection_name(collection_name: object, name: str = 'collection_name') -> None:
7
+ """Validate a collection name."""
8
+ if not isinstance(collection_name, str):
9
+ raise TypeError(f"{name} must be {str} instead of {type(collection_name)}")
10
+
11
+ parts = collection_name.split('.')
12
+
13
+ if len(parts) != 2 or not all(part.isidentifier() and not keyword.iskeyword(part) for part in parts):
14
+ raise ValueError(f"{name} must consist of two non-keyword identifiers separated by '.'")
@@ -17,6 +17,6 @@
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
- __version__ = '2.19.0b2'
20
+ __version__ = '2.19.0b4'
21
21
  __author__ = 'Ansible, Inc.'
22
22
  __codename__ = "What Is and What Should Never Be"
@@ -75,7 +75,7 @@ except ImportError:
75
75
  # Python2 & 3 way to get NoneType
76
76
  NoneType = type(None)
77
77
 
78
- from ._internal import _traceback, _errors, _debugging
78
+ from ._internal import _traceback, _errors, _debugging, _deprecator
79
79
 
80
80
  from .common.text.converters import (
81
81
  to_native,
@@ -509,16 +509,31 @@ class AnsibleModule(object):
509
509
  warn(warning)
510
510
  self.log('[WARNING] %s' % warning)
511
511
 
512
- def deprecate(self, msg, version=None, date=None, collection_name=None):
513
- if version is not None and date is not None:
514
- raise AssertionError("implementation error -- version and date must not both be set")
515
- deprecate(msg, version=version, date=date)
516
- # For compatibility, we accept that neither version nor date is set,
517
- # and treat that the same as if version would not have been set
518
- if date is not None:
519
- self.log('[DEPRECATION WARNING] %s %s' % (msg, date))
520
- else:
521
- self.log('[DEPRECATION WARNING] %s %s' % (msg, version))
512
+ def deprecate(
513
+ self,
514
+ msg: str,
515
+ version: str | None = None,
516
+ date: str | None = None,
517
+ collection_name: str | None = None,
518
+ *,
519
+ deprecator: _messages.PluginInfo | None = None,
520
+ help_text: str | None = None,
521
+ ) -> None:
522
+ """
523
+ Record a deprecation warning to be returned with the module result.
524
+ Most callers do not need to provide `collection_name` or `deprecator` -- but provide only one if needed.
525
+ Specify `version` or `date`, but not both.
526
+ If `date` is a string, it must be in the form `YYYY-MM-DD`.
527
+ """
528
+ _skip_stackwalk = True
529
+
530
+ deprecate( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
531
+ msg=msg,
532
+ version=version,
533
+ date=date,
534
+ deprecator=_deprecator.get_best_deprecator(deprecator=deprecator, collection_name=collection_name),
535
+ help_text=help_text,
536
+ )
522
537
 
523
538
  def load_file_common_arguments(self, params, path=None):
524
539
  """
@@ -1404,6 +1419,7 @@ class AnsibleModule(object):
1404
1419
  self.cleanup(path)
1405
1420
 
1406
1421
  def _return_formatted(self, kwargs):
1422
+ _skip_stackwalk = True
1407
1423
 
1408
1424
  self.add_path_info(kwargs)
1409
1425
 
@@ -1411,6 +1427,13 @@ class AnsibleModule(object):
1411
1427
  kwargs['invocation'] = {'module_args': self.params}
1412
1428
 
1413
1429
  if 'warnings' in kwargs:
1430
+ self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name
1431
+ msg='Passing `warnings` to `exit_json` or `fail_json` is deprecated.',
1432
+ version='2.23',
1433
+ help_text='Use `AnsibleModule.warn` instead.',
1434
+ deprecator=_deprecator.ANSIBLE_CORE_DEPRECATOR,
1435
+ )
1436
+
1414
1437
  if isinstance(kwargs['warnings'], list):
1415
1438
  for w in kwargs['warnings']:
1416
1439
  self.warn(w)
@@ -1422,17 +1445,38 @@ class AnsibleModule(object):
1422
1445
  kwargs['warnings'] = warnings
1423
1446
 
1424
1447
  if 'deprecations' in kwargs:
1448
+ self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name
1449
+ msg='Passing `deprecations` to `exit_json` or `fail_json` is deprecated.',
1450
+ version='2.23',
1451
+ help_text='Use `AnsibleModule.deprecate` instead.',
1452
+ deprecator=_deprecator.ANSIBLE_CORE_DEPRECATOR,
1453
+ )
1454
+
1425
1455
  if isinstance(kwargs['deprecations'], list):
1426
1456
  for d in kwargs['deprecations']:
1427
- if isinstance(d, SEQUENCETYPE) and len(d) == 2:
1428
- self.deprecate(d[0], version=d[1])
1457
+ if isinstance(d, (KeysView, Sequence)) and len(d) == 2:
1458
+ self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name,ansible-invalid-deprecated-version
1459
+ msg=d[0],
1460
+ version=d[1],
1461
+ deprecator=_deprecator.get_best_deprecator(),
1462
+ )
1429
1463
  elif isinstance(d, Mapping):
1430
- self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'),
1431
- collection_name=d.get('collection_name'))
1464
+ self.deprecate( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
1465
+ msg=d['msg'],
1466
+ version=d.get('version'),
1467
+ date=d.get('date'),
1468
+ deprecator=_deprecator.get_best_deprecator(collection_name=d.get('collection_name')),
1469
+ )
1432
1470
  else:
1433
- self.deprecate(d) # pylint: disable=ansible-deprecated-no-version
1471
+ self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name,ansible-deprecated-no-version
1472
+ msg=d,
1473
+ deprecator=_deprecator.get_best_deprecator(),
1474
+ )
1434
1475
  else:
1435
- self.deprecate(kwargs['deprecations']) # pylint: disable=ansible-deprecated-no-version
1476
+ self.deprecate( # pylint: disable=ansible-deprecated-unnecessary-collection-name,ansible-deprecated-no-version
1477
+ msg=kwargs['deprecations'],
1478
+ deprecator=_deprecator.get_best_deprecator(),
1479
+ )
1436
1480
 
1437
1481
  deprecations = get_deprecations()
1438
1482
  if deprecations:
@@ -1452,6 +1496,7 @@ class AnsibleModule(object):
1452
1496
 
1453
1497
  def exit_json(self, **kwargs) -> t.NoReturn:
1454
1498
  """ return from the module, without error """
1499
+ _skip_stackwalk = True
1455
1500
 
1456
1501
  self.do_cleanup_files()
1457
1502
  self._return_formatted(kwargs)
@@ -1473,6 +1518,8 @@ class AnsibleModule(object):
1473
1518
  When `exception` is not specified, a formatted traceback will be retrieved from the current exception.
1474
1519
  If no exception is pending, the current call stack will be used instead.
1475
1520
  """
1521
+ _skip_stackwalk = True
1522
+
1476
1523
  msg = str(msg) # coerce to str instead of raising an error due to an invalid type
1477
1524
 
1478
1525
  kwargs.update(
@@ -22,6 +22,7 @@ from ansible.module_utils.common.parameters import (
22
22
 
23
23
  from ansible.module_utils.common.text.converters import to_native
24
24
  from ansible.module_utils.common.warnings import deprecate, warn
25
+ from ansible.module_utils.common import messages as _messages
25
26
 
26
27
  from ansible.module_utils.common.validation import (
27
28
  check_mutually_exclusive,
@@ -300,9 +301,13 @@ class ModuleArgumentSpecValidator(ArgumentSpecValidator):
300
301
  result = super(ModuleArgumentSpecValidator, self).validate(parameters)
301
302
 
302
303
  for d in result._deprecations:
303
- deprecate(d['msg'],
304
- version=d.get('version'), date=d.get('date'),
305
- collection_name=d.get('collection_name'))
304
+ # DTFIX-FUTURE: pass an actual deprecator instead of one derived from collection_name
305
+ deprecate( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
306
+ msg=d['msg'],
307
+ version=d.get('version'),
308
+ date=d.get('date'),
309
+ deprecator=_messages.PluginInfo._from_collection_name(d.get('collection_name')),
310
+ )
306
311
 
307
312
  for w in result._warnings:
308
313
  warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias']))
@@ -13,7 +13,7 @@ import dataclasses as _dataclasses
13
13
  # deprecated: description='typing.Self exists in Python 3.11+' python_version='3.10'
14
14
  from ..compat import typing as _t
15
15
 
16
- from ansible.module_utils._internal import _datatag
16
+ from ansible.module_utils._internal import _datatag, _validation
17
17
 
18
18
  if _sys.version_info >= (3, 10):
19
19
  # Using slots for reduced memory usage and improved performance.
@@ -27,13 +27,27 @@ else:
27
27
  class PluginInfo(_datatag.AnsibleSerializableDataclass):
28
28
  """Information about a loaded plugin."""
29
29
 
30
- requested_name: str
31
- """The plugin name as requested, before resolving, which may be partially or fully qualified."""
32
30
  resolved_name: str
33
31
  """The resolved canonical plugin name; always fully-qualified for collection plugins."""
34
32
  type: str
35
33
  """The plugin type."""
36
34
 
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
+
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
43
+
44
+ _validation.validate_collection_name(collection_name)
45
+
46
+ return cls(
47
+ resolved_name=collection_name,
48
+ type=cls._COLLECTION_ONLY_TYPE,
49
+ )
50
+
37
51
 
38
52
  @_dataclasses.dataclass(**_dataclass_kwargs)
39
53
  class Detail(_datatag.AnsibleSerializableDataclass):
@@ -75,34 +89,37 @@ class WarningSummary(SummaryBase):
75
89
  class DeprecationSummary(WarningSummary):
76
90
  """Deprecation summary with details (possibly derived from an exception __cause__ chain) and an optional traceback."""
77
91
 
78
- version: _t.Optional[str] = None
79
- date: _t.Optional[str] = None
80
- plugin: _t.Optional[PluginInfo] = None
81
-
82
- @property
83
- def collection_name(self) -> _t.Optional[str]:
84
- if not self.plugin:
85
- return None
86
-
87
- parts = self.plugin.resolved_name.split('.')
88
-
89
- if len(parts) < 2:
90
- return None
91
-
92
- collection_name = '.'.join(parts[:2])
92
+ deprecator: _t.Optional[PluginInfo] = None
93
+ """
94
+ The identifier for the content which is being deprecated.
95
+ """
93
96
 
94
- # deprecated: description='enable the deprecation message for collection_name' core_version='2.23'
95
- # from ansible.module_utils.datatag import deprecate_value
96
- # collection_name = deprecate_value(collection_name, 'The `collection_name` property is deprecated.', removal_version='2.27')
97
+ date: _t.Optional[str] = None
98
+ """
99
+ The date after which a new release of `deprecator` will remove the feature described by `msg`.
100
+ Ignored if `deprecator` is not provided.
101
+ """
97
102
 
98
- return collection_name
103
+ version: _t.Optional[str] = None
104
+ """
105
+ The version of `deprecator` which will remove the feature described by `msg`.
106
+ Ignored if `deprecator` is not provided.
107
+ Ignored if `date` is provided.
108
+ """
99
109
 
100
110
  def _as_simple_dict(self) -> _t.Dict[str, _t.Any]:
101
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
+
102
119
  result = self._as_dict()
103
120
  result.update(
104
121
  msg=self._format(),
105
- collection_name=self.collection_name,
122
+ collection_name=collection_name,
106
123
  )
107
124
 
108
125
  return result
@@ -29,7 +29,6 @@ def get_bin_path(arg, opt_dirs=None, required=None):
29
29
  deprecate(
30
30
  msg="The `required` parameter in `get_bin_path` API is deprecated.",
31
31
  version="2.21",
32
- collection_name="ansible.builtin",
33
32
  )
34
33
 
35
34
  paths = []
@@ -3,14 +3,12 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- import dataclasses
7
6
  import os
8
7
  import pathlib
9
8
  import subprocess
10
9
  import sys
11
10
  import typing as t
12
11
 
13
- from ansible.module_utils._internal import _plugin_exec_context
14
12
  from ansible.module_utils.common.text.converters import to_bytes
15
13
 
16
14
  _ANSIBLE_PARENT_PATH = pathlib.Path(__file__).parents[3]
@@ -99,7 +97,6 @@ if __name__ == '__main__':
99
97
 
100
98
  json_params = {json_params!r}
101
99
  profile = {profile!r}
102
- plugin_info_dict = {plugin_info_dict!r}
103
100
  module_fqn = {module_fqn!r}
104
101
  modlib_path = {modlib_path!r}
105
102
 
@@ -110,19 +107,15 @@ if __name__ == '__main__':
110
107
  _ansiballz.run_module(
111
108
  json_params=json_params,
112
109
  profile=profile,
113
- plugin_info_dict=plugin_info_dict,
114
110
  module_fqn=module_fqn,
115
111
  modlib_path=modlib_path,
116
112
  init_globals=dict(_respawned=True),
117
113
  )
118
114
  """
119
115
 
120
- plugin_info = _plugin_exec_context.PluginExecContext.get_current_plugin_info()
121
-
122
116
  respawn_code = respawn_code_template.format(
123
117
  json_params=basic._ANSIBLE_ARGS,
124
118
  profile=basic._ANSIBLE_PROFILE,
125
- plugin_info_dict=dataclasses.asdict(plugin_info),
126
119
  module_fqn=module_fqn,
127
120
  modlib_path=modlib_path,
128
121
  )