ansible-core 2.19.0b2__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 (70) hide show
  1. ansible/_internal/_ansiballz.py +1 -4
  2. ansible/_internal/_templating/_datatag.py +3 -4
  3. ansible/_internal/_templating/_engine.py +6 -1
  4. ansible/_internal/_templating/_jinja_plugins.py +2 -6
  5. ansible/cli/__init__.py +3 -2
  6. ansible/cli/arguments/option_helpers.py +4 -1
  7. ansible/cli/doc.py +0 -1
  8. ansible/config/manager.py +2 -2
  9. ansible/constants.py +0 -62
  10. ansible/errors/__init__.py +6 -2
  11. ansible/executor/module_common.py +11 -7
  12. ansible/executor/task_executor.py +6 -8
  13. ansible/galaxy/api.py +1 -1
  14. ansible/galaxy/collection/__init__.py +3 -3
  15. ansible/module_utils/_internal/_ansiballz.py +4 -30
  16. ansible/module_utils/_internal/_datatag/_tags.py +3 -25
  17. ansible/module_utils/_internal/_deprecator.py +134 -0
  18. ansible/module_utils/_internal/_plugin_info.py +25 -0
  19. ansible/module_utils/_internal/_validation.py +14 -0
  20. ansible/module_utils/ansible_release.py +1 -1
  21. ansible/module_utils/basic.py +64 -17
  22. ansible/module_utils/common/arg_spec.py +8 -3
  23. ansible/module_utils/common/messages.py +40 -23
  24. ansible/module_utils/common/process.py +0 -1
  25. ansible/module_utils/common/respawn.py +0 -7
  26. ansible/module_utils/common/warnings.py +13 -13
  27. ansible/module_utils/datatag.py +13 -13
  28. ansible/modules/async_status.py +1 -1
  29. ansible/modules/dnf5.py +1 -1
  30. ansible/modules/get_url.py +1 -1
  31. ansible/playbook/task.py +0 -2
  32. ansible/plugins/__init__.py +18 -8
  33. ansible/plugins/action/__init__.py +6 -14
  34. ansible/plugins/action/gather_facts.py +2 -4
  35. ansible/plugins/callback/oneline.py +7 -1
  36. ansible/plugins/callback/tree.py +7 -1
  37. ansible/plugins/connection/local.py +1 -1
  38. ansible/plugins/connection/paramiko_ssh.py +9 -2
  39. ansible/plugins/doc_fragments/action_core.py +1 -1
  40. ansible/plugins/filter/core.py +4 -1
  41. ansible/plugins/inventory/__init__.py +2 -2
  42. ansible/plugins/loader.py +194 -130
  43. ansible/plugins/lookup/url.py +2 -2
  44. ansible/plugins/strategy/__init__.py +6 -6
  45. ansible/release.py +1 -1
  46. ansible/template/__init__.py +1 -1
  47. ansible/utils/collection_loader/_collection_meta.py +5 -3
  48. ansible/utils/display.py +133 -71
  49. ansible/utils/py3compat.py +1 -7
  50. ansible/utils/ssh_functions.py +4 -1
  51. ansible/vars/manager.py +18 -10
  52. ansible/vars/plugins.py +4 -4
  53. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/METADATA +1 -1
  54. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/RECORD +67 -65
  55. ansible_test/_internal/commands/sanity/pylint.py +1 -0
  56. ansible_test/_internal/docker_util.py +4 -3
  57. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +475 -0
  58. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_comment.py +137 -0
  59. ansible/module_utils/_internal/_dataclass_annotation_patch.py +0 -64
  60. ansible/module_utils/_internal/_plugin_exec_context.py +0 -49
  61. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +0 -399
  62. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/Apache-License.txt +0 -0
  63. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/BSD-3-Clause.txt +0 -0
  64. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/COPYING +0 -0
  65. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/MIT-license.txt +0 -0
  66. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/PSF-license.txt +0 -0
  67. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/WHEEL +0 -0
  68. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/entry_points.txt +0 -0
  69. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/simplified_bsd.txt +0 -0
  70. {ansible_core-2.19.0b2.dist-info → ansible_core-2.19.0b3.dist-info}/top_level.txt +0 -0
@@ -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.0b3'
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
  )
@@ -4,15 +4,12 @@
4
4
 
5
5
  from __future__ import annotations as _annotations
6
6
 
7
- import datetime as _datetime
8
7
  import typing as _t
9
8
 
10
- from ansible.module_utils._internal import _traceback, _plugin_exec_context
9
+ from ansible.module_utils._internal import _traceback, _deprecator
11
10
  from ansible.module_utils.common import messages as _messages
12
11
  from ansible.module_utils import _internal
13
12
 
14
- _UNSET = _t.cast(_t.Any, object())
15
-
16
13
 
17
14
  def warn(warning: str) -> None:
18
15
  """Record a warning to be returned with the module result."""
@@ -28,22 +25,23 @@ def warn(warning: str) -> None:
28
25
  def deprecate(
29
26
  msg: str,
30
27
  version: str | None = None,
31
- date: str | _datetime.date | None = None,
32
- collection_name: str | None = _UNSET,
28
+ date: str | None = None,
29
+ collection_name: str | None = None,
33
30
  *,
31
+ deprecator: _messages.PluginInfo | None = None,
34
32
  help_text: str | None = None,
35
33
  obj: object | None = None,
36
34
  ) -> None:
37
35
  """
38
- Record a deprecation warning to be returned with the module result.
36
+ Record a deprecation warning.
39
37
  The `obj` argument is only useful in a controller context; it is ignored for target-side callers.
38
+ Most callers do not need to provide `collection_name` or `deprecator` -- but provide only one if needed.
39
+ Specify `version` or `date`, but not both.
40
+ If `date` is a string, it must be in the form `YYYY-MM-DD`.
40
41
  """
41
- if isinstance(date, _datetime.date):
42
- date = str(date)
42
+ _skip_stackwalk = True
43
43
 
44
- # deprecated: description='enable the deprecation message for collection_name' core_version='2.23'
45
- # if collection_name is not _UNSET:
46
- # deprecate('The `collection_name` argument to `deprecate` is deprecated.', version='2.27')
44
+ deprecator = _deprecator.get_best_deprecator(deprecator=deprecator, collection_name=collection_name)
47
45
 
48
46
  if _internal.is_controller:
49
47
  _display = _internal.import_controller_module('ansible.utils.display').Display()
@@ -53,6 +51,8 @@ def deprecate(
53
51
  date=date,
54
52
  help_text=help_text,
55
53
  obj=obj,
54
+ # skip passing collection_name; get_best_deprecator already accounted for it when present
55
+ deprecator=deprecator,
56
56
  )
57
57
 
58
58
  return
@@ -64,7 +64,7 @@ def deprecate(
64
64
  formatted_traceback=_traceback.maybe_capture_traceback(_traceback.TracebackEvent.DEPRECATED),
65
65
  version=version,
66
66
  date=date,
67
- plugin=_plugin_exec_context.PluginExecContext.get_current_plugin_info(),
67
+ deprecator=deprecator,
68
68
  )] = None
69
69
 
70
70
 
@@ -1,11 +1,11 @@
1
1
  """Public API for data tagging."""
2
2
  from __future__ import annotations as _annotations
3
3
 
4
- import datetime as _datetime
5
4
  import typing as _t
6
5
 
7
- from ._internal import _plugin_exec_context, _datatag
6
+ from ._internal import _datatag, _deprecator
8
7
  from ._internal._datatag import _tags
8
+ from .common import messages as _messages
9
9
 
10
10
  _T = _t.TypeVar('_T')
11
11
 
@@ -14,28 +14,28 @@ def deprecate_value(
14
14
  value: _T,
15
15
  msg: str,
16
16
  *,
17
+ version: str | None = None,
18
+ date: str | None = None,
19
+ collection_name: str | None = None,
20
+ deprecator: _messages.PluginInfo | None = None,
17
21
  help_text: str | None = None,
18
- removal_date: str | _datetime.date | None = None,
19
- removal_version: str | None = None,
20
22
  ) -> _T:
21
23
  """
22
24
  Return `value` tagged with the given deprecation details.
23
25
  The types `None` and `bool` cannot be deprecated and are returned unmodified.
24
26
  Raises a `TypeError` if `value` is not a supported type.
25
- If `removal_date` is a string, it must be in the form `YYYY-MM-DD`.
26
- This function is only supported in contexts where an Ansible plugin/module is executing.
27
+ Most callers do not need to provide `collection_name` or `deprecator` -- but provide only one if needed.
28
+ Specify `version` or `date`, but not both.
29
+ If `date` is provided, it should be in the form `YYYY-MM-DD`.
27
30
  """
28
- if isinstance(removal_date, str):
29
- # The `fromisoformat` method accepts other ISO 8601 formats than `YYYY-MM-DD` starting with Python 3.11.
30
- # That should be considered undocumented behavior of `deprecate_value` rather than an intentional feature.
31
- removal_date = _datetime.date.fromisoformat(removal_date)
31
+ _skip_stackwalk = True
32
32
 
33
33
  deprecated = _tags.Deprecated(
34
34
  msg=msg,
35
35
  help_text=help_text,
36
- removal_date=removal_date,
37
- removal_version=removal_version,
38
- plugin=_plugin_exec_context.PluginExecContext.get_current_plugin_info(),
36
+ date=date,
37
+ version=version,
38
+ deprecator=_deprecator.get_best_deprecator(deprecator=deprecator, collection_name=collection_name),
39
39
  )
40
40
 
41
41
  return deprecated.tag(value)
@@ -68,7 +68,7 @@ EXAMPLES = r"""
68
68
  ansible.builtin.async_status:
69
69
  jid: '{{ dnf_sleeper.ansible_job_id }}'
70
70
  register: job_result
71
- until: job_result.finished
71
+ until: job_result is finished
72
72
  retries: 100
73
73
  delay: 10
74
74
 
ansible/modules/dnf5.py CHANGED
@@ -797,7 +797,7 @@ class Dnf5Module(YumDnf):
797
797
  if self.module.check_mode:
798
798
  if results:
799
799
  msg = "Check mode: No changes made, but would have if not in check mode"
800
- else:
800
+ elif changed:
801
801
  transaction.download()
802
802
  if not self.download_only:
803
803
  transaction.set_description("ansible dnf5 module")
@@ -87,7 +87,7 @@ options:
87
87
  - 'If a checksum is passed to this parameter, the digest of the
88
88
  destination file will be calculated after it is downloaded to ensure
89
89
  its integrity and verify that the transfer completed successfully.
90
- Format: <algorithm>:<checksum|url>, for example C(checksum="sha256:D98291AC[...]B6DC7B97",
90
+ Format: <algorithm>:<checksum|url>, for example C(checksum="sha256:D98291AC[...]B6DC7B97"),
91
91
  C(checksum="sha256:http://example.com/path/sha256sum.txt").'
92
92
  - If you worry about portability, only the sha1 algorithm is available
93
93
  on all platforms and python versions.
ansible/playbook/task.py CHANGED
@@ -227,8 +227,6 @@ class Task(Base, Conditional, Taggable, CollectionSearch, Notifiable, Delegatabl
227
227
  raise AnsibleError("you must specify a value when using %s" % k, obj=ds)
228
228
  new_ds['loop_with'] = loop_name
229
229
  new_ds['loop'] = v
230
- # display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead",
231
- # version="2.10", collection_name='ansible.builtin')
232
230
 
233
231
  def preprocess_data(self, ds):
234
232
  """
@@ -20,24 +20,26 @@
20
20
  from __future__ import annotations
21
21
 
22
22
  import abc
23
+ import functools
23
24
  import types
24
25
  import typing as t
25
26
 
26
27
  from ansible import constants as C
27
28
  from ansible.errors import AnsibleError
28
29
  from ansible.utils.display import Display
30
+ from ansible.utils import display as _display
29
31
 
30
- from ansible.module_utils._internal import _plugin_exec_context
32
+ from ansible.module_utils._internal import _plugin_info
31
33
 
32
34
  display = Display()
33
35
 
34
36
  if t.TYPE_CHECKING:
35
- from .loader import PluginPathContext
37
+ from . import loader as _t_loader
36
38
 
37
39
  # Global so that all instances of a PluginLoader will share the caches
38
40
  MODULE_CACHE = {} # type: dict[str, dict[str, types.ModuleType]]
39
- PATH_CACHE = {} # type: dict[str, list[PluginPathContext] | None]
40
- PLUGIN_PATH_CACHE = {} # type: dict[str, dict[str, dict[str, PluginPathContext]]]
41
+ PATH_CACHE = {} # type: dict[str, list[_t_loader.PluginPathContext] | None]
42
+ PLUGIN_PATH_CACHE = {} # type: dict[str, dict[str, dict[str, _t_loader.PluginPathContext]]]
41
43
 
42
44
 
43
45
  def get_plugin_class(obj):
@@ -50,10 +52,10 @@ def get_plugin_class(obj):
50
52
  class _ConfigurablePlugin(t.Protocol):
51
53
  """Protocol to provide type-safe access to config for plugin-related mixins."""
52
54
 
53
- def get_option(self, option: str, hostvars: dict[str, object] | None = None) -> object: ...
55
+ def get_option(self, option: str, hostvars: dict[str, object] | None = None) -> t.Any: ...
54
56
 
55
57
 
56
- class _AnsiblePluginInfoMixin(_plugin_exec_context.HasPluginInfo):
58
+ class _AnsiblePluginInfoMixin(_plugin_info.HasPluginInfo):
57
59
  """Mixin to provide type annotations and default values for existing PluginLoader-set load-time attrs."""
58
60
  _original_path: str | None = None
59
61
  _load_name: str | None = None
@@ -102,6 +104,14 @@ class AnsiblePlugin(_AnsiblePluginInfoMixin, _ConfigurablePlugin, metaclass=abc.
102
104
  raise KeyError(str(e))
103
105
  return option_value, origin
104
106
 
107
+ @functools.cached_property
108
+ def __plugin_info(self):
109
+ """
110
+ Internal cached property to retrieve `PluginInfo` for this plugin instance.
111
+ Only for use by the `AnsiblePlugin` base class.
112
+ """
113
+ return _plugin_info.get_plugin_info(self)
114
+
105
115
  def get_option(self, option, hostvars=None):
106
116
 
107
117
  if option not in self._options:
@@ -117,7 +127,7 @@ class AnsiblePlugin(_AnsiblePluginInfoMixin, _ConfigurablePlugin, metaclass=abc.
117
127
 
118
128
  def set_option(self, option, value):
119
129
  self._options[option] = C.config.get_config_value(option, plugin_type=self.plugin_type, plugin_name=self._load_name, direct={option: value})
120
- C.handle_config_noise(display)
130
+ _display._report_config_warnings(self.__plugin_info)
121
131
 
122
132
  def set_options(self, task_keys=None, var_options=None, direct=None):
123
133
  """
@@ -134,7 +144,7 @@ class AnsiblePlugin(_AnsiblePluginInfoMixin, _ConfigurablePlugin, metaclass=abc.
134
144
  if self.allow_extras and var_options and '_extras' in var_options:
135
145
  # these are largely unvalidated passthroughs, either plugin or underlying API will validate
136
146
  self._options['_extras'] = var_options['_extras']
137
- C.handle_config_noise(display)
147
+ _display._report_config_warnings(self.__plugin_info)
138
148
 
139
149
  def has_option(self, option):
140
150
  if not self._options:
@@ -318,13 +318,6 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
318
318
  final_environment: dict[str, t.Any] = {}
319
319
  self._compute_environment_string(final_environment)
320
320
 
321
- # `modify_module` adapts PluginInfo to allow target-side use of `PluginExecContext` since modules aren't plugins
322
- plugin = PluginInfo(
323
- requested_name=module_name,
324
- resolved_name=result.resolved_fqcn,
325
- type='module',
326
- )
327
-
328
321
  # modify_module will exit early if interpreter discovery is required; re-run after if necessary
329
322
  for _dummy in (1, 2):
330
323
  try:
@@ -338,7 +331,6 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
338
331
  async_timeout=self._task.async_val,
339
332
  environment=final_environment,
340
333
  remote_is_local=bool(getattr(self._connection, '_remote_is_local', False)),
341
- plugin=plugin,
342
334
  become_plugin=self._connection.become,
343
335
  )
344
336
 
@@ -649,12 +641,12 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
649
641
  # done. Make the files +x if we're asked to, and return.
650
642
  if not self._is_become_unprivileged():
651
643
  if execute:
652
- # Can't depend on the file being transferred with execute permissions.
644
+ # Can't depend on the file being transferred with required permissions.
653
645
  # Only need user perms because no become was used here
654
- res = self._remote_chmod(remote_paths, 'u+x')
646
+ res = self._remote_chmod(remote_paths, 'u+rwx')
655
647
  if res['rc'] != 0:
656
648
  raise AnsibleError(
657
- 'Failed to set execute bit on remote files '
649
+ 'Failed to set permissions on remote files '
658
650
  '(rc: {0}, err: {1})'.format(
659
651
  res['rc'],
660
652
  to_native(res['stderr'])))
@@ -695,10 +687,10 @@ class ActionBase(ABC, _AnsiblePluginInfoMixin):
695
687
  return remote_paths
696
688
 
697
689
  # Step 3b: Set execute if we need to. We do this before anything else
698
- # because some of the methods below might work but not let us set +x
699
- # as part of them.
690
+ # because some of the methods below might work but not let us set
691
+ # permissions as part of them.
700
692
  if execute:
701
- res = self._remote_chmod(remote_paths, 'u+x')
693
+ res = self._remote_chmod(remote_paths, 'u+rwx')
702
694
  if res['rc'] != 0:
703
695
  raise AnsibleError(
704
696
  'Failed to set file mode or acl on remote temporary files '