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
ansible/plugins/loader.py CHANGED
@@ -29,7 +29,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_text, to_na
29
29
  from ansible.module_utils.six import string_types
30
30
  from ansible.parsing.yaml.loader import AnsibleLoader
31
31
  from ansible._internal._yaml._loader import AnsibleInstrumentedLoader
32
- from ansible.plugins import get_plugin_class, MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE
32
+ from ansible.plugins import get_plugin_class, MODULE_CACHE, PATH_CACHE, PLUGIN_PATH_CACHE, AnsibleJinja2Plugin
33
33
  from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
34
34
  from ansible.utils.collection_loader._collection_finder import _AnsibleCollectionFinder, _get_collection_metadata
35
35
  from ansible.utils.display import Display
@@ -135,29 +135,44 @@ class PluginPathContext(object):
135
135
 
136
136
 
137
137
  class PluginLoadContext(object):
138
- def __init__(self):
139
- self.original_name = None
140
- self.redirect_list = []
141
- self.error_list = []
142
- self.import_error_list = []
143
- self.load_attempts = []
144
- self.pending_redirect = None
145
- self.exit_reason = None
146
- self.plugin_resolved_path = None
147
- self.plugin_resolved_name = None
148
- self.plugin_resolved_collection = None # empty string for resolved plugins from user-supplied paths
149
- self.deprecated = False
150
- self.removal_date = None
151
- self.removal_version = None
152
- self.deprecation_warnings = []
153
- self.resolved = False
154
- self._resolved_fqcn = None
155
- self.action_plugin = None
138
+ def __init__(self, plugin_type: str, legacy_package_name: str) -> None:
139
+ self.original_name: str | None = None
140
+ self.redirect_list: list[str] = []
141
+ self.raw_error_list: list[Exception] = []
142
+ """All exception instances encountered during the plugin load."""
143
+ self.error_list: list[str] = []
144
+ """Stringified exceptions, excluding import errors."""
145
+ self.import_error_list: list[Exception] = []
146
+ """All ImportError exception instances encountered during the plugin load."""
147
+ self.load_attempts: list[str] = []
148
+ self.pending_redirect: str | None = None
149
+ self.exit_reason: str | None = None
150
+ self.plugin_resolved_path: str | None = None
151
+ self.plugin_resolved_name: str | None = None
152
+ """For collection plugins, the resolved Python module FQ __name__; for non-collections, the short name."""
153
+ self.plugin_resolved_collection: str | None = None # empty string for resolved plugins from user-supplied paths
154
+ """For collection plugins, the resolved collection {ns}.{col}; empty string for non-collection plugins."""
155
+ self.deprecated: bool = False
156
+ self.removal_date: str | None = None
157
+ self.removal_version: str | None = None
158
+ self.deprecation_warnings: list[str] = []
159
+ self.resolved: bool = False
160
+ self._resolved_fqcn: str | None = None
161
+ self.action_plugin: str | None = None
162
+ self._plugin_type: str = plugin_type
163
+ """The type of the plugin."""
164
+ self._legacy_package_name = legacy_package_name
165
+ """The legacy sys.modules package name from the plugin loader instance; stored to prevent potentially incorrect manual computation."""
166
+ self._python_module_name: str | None = None
167
+ """
168
+ The fully qualified Python module name for the plugin (accessible via `sys.modules`).
169
+ For non-collection non-core plugins, this may include a non-existent synthetic package element with a hash of the file path to avoid collisions.
170
+ """
156
171
 
157
172
  @property
158
- def resolved_fqcn(self):
173
+ def resolved_fqcn(self) -> str | None:
159
174
  if not self.resolved:
160
- return
175
+ return None
161
176
 
162
177
  if not self._resolved_fqcn:
163
178
  final_plugin = self.redirect_list[-1]
@@ -169,7 +184,7 @@ class PluginLoadContext(object):
169
184
 
170
185
  return self._resolved_fqcn
171
186
 
172
- def record_deprecation(self, name, deprecation, collection_name):
187
+ def record_deprecation(self, name: str, deprecation: dict[str, t.Any] | None, collection_name: str) -> t.Self:
173
188
  if not deprecation:
174
189
  return self
175
190
 
@@ -183,7 +198,12 @@ class PluginLoadContext(object):
183
198
  removal_version = None
184
199
  warning_text = '{0} has been deprecated.{1}{2}'.format(name, ' ' if warning_text else '', warning_text)
185
200
 
186
- display.deprecated(warning_text, date=removal_date, version=removal_version, collection_name=collection_name)
201
+ display.deprecated( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
202
+ msg=warning_text,
203
+ date=removal_date,
204
+ version=removal_version,
205
+ deprecator=PluginInfo._from_collection_name(collection_name),
206
+ )
187
207
 
188
208
  self.deprecated = True
189
209
  if removal_date:
@@ -193,28 +213,79 @@ class PluginLoadContext(object):
193
213
  self.deprecation_warnings.append(warning_text)
194
214
  return self
195
215
 
196
- def resolve(self, resolved_name, resolved_path, resolved_collection, exit_reason, action_plugin):
216
+ def resolve(self, resolved_name: str, resolved_path: str, resolved_collection: str, exit_reason: str, action_plugin: str) -> t.Self:
217
+ """Record a resolved collection plugin."""
197
218
  self.pending_redirect = None
198
219
  self.plugin_resolved_name = resolved_name
199
220
  self.plugin_resolved_path = resolved_path
200
221
  self.plugin_resolved_collection = resolved_collection
201
222
  self.exit_reason = exit_reason
223
+ self._python_module_name = resolved_name
202
224
  self.resolved = True
203
225
  self.action_plugin = action_plugin
226
+
227
+ return self
228
+
229
+ def resolve_legacy(self, name: str, pull_cache: dict[str, PluginPathContext]) -> t.Self:
230
+ """Record a resolved legacy plugin."""
231
+ plugin_path_context = pull_cache[name]
232
+
233
+ self.plugin_resolved_name = name
234
+ self.plugin_resolved_path = plugin_path_context.path
235
+ self.plugin_resolved_collection = 'ansible.builtin' if plugin_path_context.internal else ''
236
+ self._resolved_fqcn = 'ansible.builtin.' + name if plugin_path_context.internal else name
237
+ self._python_module_name = self._make_legacy_python_module_name()
238
+ self.resolved = True
239
+
240
+ return self
241
+
242
+ def resolve_legacy_jinja_plugin(self, name: str, known_plugin: AnsibleJinja2Plugin) -> t.Self:
243
+ """Record a resolved legacy Jinja plugin."""
244
+ internal = known_plugin.ansible_name.startswith('ansible.builtin.')
245
+
246
+ self.plugin_resolved_name = name
247
+ self.plugin_resolved_path = known_plugin._original_path
248
+ self.plugin_resolved_collection = 'ansible.builtin' if internal else ''
249
+ self._resolved_fqcn = known_plugin.ansible_name
250
+ self._python_module_name = self._make_legacy_python_module_name()
251
+ self.resolved = True
252
+
204
253
  return self
205
254
 
206
- def redirect(self, redirect_name):
255
+ def redirect(self, redirect_name: str) -> t.Self:
207
256
  self.pending_redirect = redirect_name
208
257
  self.exit_reason = 'pending redirect resolution from {0} to {1}'.format(self.original_name, redirect_name)
209
258
  self.resolved = False
259
+
210
260
  return self
211
261
 
212
- def nope(self, exit_reason):
262
+ def nope(self, exit_reason: str) -> t.Self:
213
263
  self.pending_redirect = None
214
264
  self.exit_reason = exit_reason
215
265
  self.resolved = False
266
+
216
267
  return self
217
268
 
269
+ def _make_legacy_python_module_name(self) -> str:
270
+ """
271
+ Generate a fully-qualified Python module name for a legacy/builtin plugin.
272
+
273
+ The same package namespace is shared for builtin and legacy plugins.
274
+ Explicit requests for builtins via `ansible.builtin` are handled elsewhere with an aliased collection package resolved by the collection loader.
275
+ Only unqualified and `ansible.legacy`-qualified requests land here; whichever plugin is visible at the time will end up in sys.modules.
276
+ Filter and test plugin host modules receive special name suffixes to avoid collisions unrelated to the actual plugin name.
277
+ """
278
+ name = os.path.splitext(self.plugin_resolved_path)[0]
279
+ basename = os.path.basename(name)
280
+
281
+ if self._plugin_type in ('filter', 'test'):
282
+ # Unlike other plugin types, filter and test plugin names are independent of the file where they are defined.
283
+ # As a result, the Python module name must be derived from the full path of the plugin.
284
+ # This prevents accidental shadowing of unrelated plugins of the same type.
285
+ basename += f'_{abs(hash(self.plugin_resolved_path))}'
286
+
287
+ return f'{self._legacy_package_name}.{basename}'
288
+
218
289
 
219
290
  class PluginLoader:
220
291
  """
@@ -224,7 +295,15 @@ class PluginLoader:
224
295
  paths, and the python path. The first match is used.
225
296
  """
226
297
 
227
- def __init__(self, class_name, package, config, subdir, aliases=None, required_base_class=None):
298
+ def __init__(
299
+ self,
300
+ class_name: str,
301
+ package: str,
302
+ config: str | list[str],
303
+ subdir: str,
304
+ aliases: dict[str, str] | None = None,
305
+ required_base_class: str | None = None,
306
+ ) -> None:
228
307
  aliases = {} if aliases is None else aliases
229
308
 
230
309
  self.class_name = class_name
@@ -250,15 +329,15 @@ class PluginLoader:
250
329
  PLUGIN_PATH_CACHE[class_name] = defaultdict(dict)
251
330
 
252
331
  # hold dirs added at runtime outside of config
253
- self._extra_dirs = []
332
+ self._extra_dirs: list[str] = []
254
333
 
255
334
  # caches
256
335
  self._module_cache = MODULE_CACHE[class_name]
257
336
  self._paths = PATH_CACHE[class_name]
258
337
  self._plugin_path_cache = PLUGIN_PATH_CACHE[class_name]
259
- self._plugin_instance_cache = {} if self.subdir == 'vars_plugins' else None
338
+ self._plugin_instance_cache: dict[str, tuple[object, PluginLoadContext]] | None = {} if self.subdir == 'vars_plugins' else None
260
339
 
261
- self._searched_paths = set()
340
+ self._searched_paths: set[str] = set()
262
341
 
263
342
  @property
264
343
  def type(self):
@@ -488,7 +567,13 @@ class PluginLoader:
488
567
  entry = collection_meta.get('plugin_routing', {}).get(plugin_type, {}).get(subdir_qualified_resource, None)
489
568
  return entry
490
569
 
491
- def _find_fq_plugin(self, fq_name, extension, plugin_load_context, ignore_deprecated=False):
570
+ def _find_fq_plugin(
571
+ self,
572
+ fq_name: str,
573
+ extension: str | None,
574
+ plugin_load_context: PluginLoadContext,
575
+ ignore_deprecated: bool = False,
576
+ ) -> PluginLoadContext:
492
577
  """Search builtin paths to find a plugin. No external paths are searched,
493
578
  meaning plugins inside roles inside collections will be ignored.
494
579
  """
@@ -525,17 +610,13 @@ class PluginLoader:
525
610
  version=removal_version,
526
611
  date=removal_date,
527
612
  removed=True,
528
- plugin=PluginInfo(
529
- requested_name=acr.collection,
530
- resolved_name=acr.collection,
531
- type='collection',
532
- ),
613
+ deprecator=PluginInfo._from_collection_name(acr.collection),
533
614
  )
534
- plugin_load_context.removal_date = removal_date
535
- plugin_load_context.removal_version = removal_version
615
+ plugin_load_context.date = removal_date
616
+ plugin_load_context.version = removal_version
536
617
  plugin_load_context.resolved = True
537
618
  plugin_load_context.exit_reason = removed_msg
538
- raise AnsiblePluginRemovedError(removed_msg, plugin_load_context=plugin_load_context)
619
+ raise AnsiblePluginRemovedError(message=removed_msg, plugin_load_context=plugin_load_context)
539
620
 
540
621
  redirect = routing_metadata.get('redirect', None)
541
622
 
@@ -623,7 +704,7 @@ class PluginLoader:
623
704
  collection_list: list[str] | None = None,
624
705
  ) -> PluginLoadContext:
625
706
  """ Find a plugin named name, returning contextual info about the load, recursively resolving redirection """
626
- plugin_load_context = PluginLoadContext()
707
+ plugin_load_context = PluginLoadContext(self.type, self.package)
627
708
  plugin_load_context.original_name = name
628
709
  while True:
629
710
  result = self._resolve_plugin_step(name, mod_type, ignore_deprecated, check_aliases, collection_list, plugin_load_context=plugin_load_context)
@@ -636,11 +717,8 @@ class PluginLoader:
636
717
  else:
637
718
  break
638
719
 
639
- # TODO: smuggle these to the controller when we're in a worker, reduce noise from normal things like missing plugin packages during collection search
640
- if plugin_load_context.error_list:
641
- display.warning("errors were encountered during the plugin load for {0}:\n{1}".format(name, plugin_load_context.error_list))
642
-
643
- # TODO: display/return import_error_list? Only useful for forensics...
720
+ for ex in plugin_load_context.raw_error_list:
721
+ display.error_as_warning(f"Error loading plugin {name!r}.", ex)
644
722
 
645
723
  # FIXME: store structured deprecation data in PluginLoadContext and use display.deprecate
646
724
  # if plugin_load_context.deprecated and C.config.get_config_value('DEPRECATION_WARNINGS'):
@@ -650,9 +728,15 @@ class PluginLoader:
650
728
 
651
729
  return plugin_load_context
652
730
 
653
- # FIXME: name bikeshed
654
- def _resolve_plugin_step(self, name, mod_type='', ignore_deprecated=False,
655
- check_aliases=False, collection_list=None, plugin_load_context=PluginLoadContext()):
731
+ def _resolve_plugin_step(
732
+ self,
733
+ name: str,
734
+ mod_type: str = '',
735
+ ignore_deprecated: bool = False,
736
+ check_aliases: bool = False,
737
+ collection_list: list[str] | None = None,
738
+ plugin_load_context: PluginLoadContext | None = None,
739
+ ) -> PluginLoadContext:
656
740
  if not plugin_load_context:
657
741
  raise ValueError('A PluginLoadContext is required')
658
742
 
@@ -707,11 +791,14 @@ class PluginLoader:
707
791
  except (AnsiblePluginRemovedError, AnsiblePluginCircularRedirect, AnsibleCollectionUnsupportedVersionError):
708
792
  # these are generally fatal, let them fly
709
793
  raise
710
- except ImportError as ie:
711
- plugin_load_context.import_error_list.append(ie)
712
794
  except Exception as ex:
713
- # FIXME: keep actual errors, not just assembled messages
714
- plugin_load_context.error_list.append(to_native(ex))
795
+ plugin_load_context.raw_error_list.append(ex)
796
+
797
+ # DTFIX-RELEASE: can we deprecate/remove these stringified versions?
798
+ if isinstance(ex, ImportError):
799
+ plugin_load_context.import_error_list.append(ex)
800
+ else:
801
+ plugin_load_context.error_list.append(str(ex))
715
802
 
716
803
  if plugin_load_context.error_list:
717
804
  display.debug(msg='plugin lookup for {0} failed; errors: {1}'.format(name, '; '.join(plugin_load_context.error_list)))
@@ -737,13 +824,7 @@ class PluginLoader:
737
824
  # requested mod_type
738
825
  pull_cache = self._plugin_path_cache[suffix]
739
826
  try:
740
- path_with_context = pull_cache[name]
741
- plugin_load_context.plugin_resolved_path = path_with_context.path
742
- plugin_load_context.plugin_resolved_name = name
743
- plugin_load_context.plugin_resolved_collection = 'ansible.builtin' if path_with_context.internal else ''
744
- plugin_load_context._resolved_fqcn = ('ansible.builtin.' + name if path_with_context.internal else name)
745
- plugin_load_context.resolved = True
746
- return plugin_load_context
827
+ return plugin_load_context.resolve_legacy(name=name, pull_cache=pull_cache)
747
828
  except KeyError:
748
829
  # Cache miss. Now let's find the plugin
749
830
  pass
@@ -796,13 +877,7 @@ class PluginLoader:
796
877
 
797
878
  self._searched_paths.add(path)
798
879
  try:
799
- path_with_context = pull_cache[name]
800
- plugin_load_context.plugin_resolved_path = path_with_context.path
801
- plugin_load_context.plugin_resolved_name = name
802
- plugin_load_context.plugin_resolved_collection = 'ansible.builtin' if path_with_context.internal else ''
803
- plugin_load_context._resolved_fqcn = 'ansible.builtin.' + name if path_with_context.internal else name
804
- plugin_load_context.resolved = True
805
- return plugin_load_context
880
+ return plugin_load_context.resolve_legacy(name=name, pull_cache=pull_cache)
806
881
  except KeyError:
807
882
  # Didn't find the plugin in this directory. Load modules from the next one
808
883
  pass
@@ -810,18 +885,18 @@ class PluginLoader:
810
885
  # if nothing is found, try finding alias/deprecated
811
886
  if not name.startswith('_'):
812
887
  alias_name = '_' + name
813
- # We've already cached all the paths at this point
814
- if alias_name in pull_cache:
815
- path_with_context = pull_cache[alias_name]
816
- if not ignore_deprecated and not os.path.islink(path_with_context.path):
817
- # FIXME: this is not always the case, some are just aliases
818
- display.deprecated('%s is kept for backwards compatibility but usage is discouraged. ' # pylint: disable=ansible-deprecated-no-version
819
- 'The module documentation details page may explain more about this rationale.' % name.lstrip('_'))
820
- plugin_load_context.plugin_resolved_path = path_with_context.path
821
- plugin_load_context.plugin_resolved_name = alias_name
822
- plugin_load_context.plugin_resolved_collection = 'ansible.builtin' if path_with_context.internal else ''
823
- plugin_load_context._resolved_fqcn = 'ansible.builtin.' + alias_name if path_with_context.internal else alias_name
824
- plugin_load_context.resolved = True
888
+
889
+ try:
890
+ plugin_load_context.resolve_legacy(name=alias_name, pull_cache=pull_cache)
891
+ except KeyError:
892
+ pass
893
+ else:
894
+ display.deprecated(
895
+ msg=f'Plugin {name!r} automatically redirected to {alias_name!r}.',
896
+ help_text=f'Use {alias_name!r} instead of {name!r} to refer to the plugin.',
897
+ version='2.23',
898
+ )
899
+
825
900
  return plugin_load_context
826
901
 
827
902
  # last ditch, if it's something that can be redirected, look for a builtin redirect before giving up
@@ -831,7 +906,7 @@ class PluginLoader:
831
906
 
832
907
  return plugin_load_context.nope('{0} is not eligible for last-chance resolution'.format(name))
833
908
 
834
- def has_plugin(self, name, collection_list=None):
909
+ def has_plugin(self, name: str, collection_list: list[str] | None = None) -> bool:
835
910
  """ Checks if a plugin named name exists """
836
911
 
837
912
  try:
@@ -842,41 +917,37 @@ class PluginLoader:
842
917
  # log and continue, likely an innocuous type/package loading failure in collections import
843
918
  display.debug('has_plugin error: {0}'.format(to_text(ex)))
844
919
 
845
- __contains__ = has_plugin
846
-
847
- def _load_module_source(self, name, path):
920
+ return False
848
921
 
849
- # avoid collisions across plugins
850
- if name.startswith('ansible_collections.'):
851
- full_name = name
852
- else:
853
- full_name = '.'.join([self.package, name])
922
+ __contains__ = has_plugin
854
923
 
855
- if full_name in sys.modules:
924
+ def _load_module_source(self, *, python_module_name: str, path: str) -> types.ModuleType:
925
+ if python_module_name in sys.modules:
856
926
  # Avoids double loading, See https://github.com/ansible/ansible/issues/13110
857
- return sys.modules[full_name]
927
+ return sys.modules[python_module_name]
858
928
 
859
929
  with warnings.catch_warnings():
860
930
  # FIXME: this still has issues if the module was previously imported but not "cached",
861
931
  # we should bypass this entire codepath for things that are directly importable
862
932
  warnings.simplefilter("ignore", RuntimeWarning)
863
- spec = importlib.util.spec_from_file_location(to_native(full_name), to_native(path))
933
+ spec = importlib.util.spec_from_file_location(to_native(python_module_name), to_native(path))
864
934
  module = importlib.util.module_from_spec(spec)
865
935
 
866
936
  # mimic import machinery; make the module-being-loaded available in sys.modules during import
867
937
  # and remove if there's a failure...
868
- sys.modules[full_name] = module
938
+ sys.modules[python_module_name] = module
869
939
 
870
940
  try:
871
941
  spec.loader.exec_module(module)
872
942
  except Exception:
873
- del sys.modules[full_name]
943
+ del sys.modules[python_module_name]
874
944
  raise
875
945
 
876
946
  return module
877
947
 
878
948
  def _update_object(
879
949
  self,
950
+ *,
880
951
  obj: _AnsiblePluginInfoMixin,
881
952
  name: str,
882
953
  path: str,
@@ -907,9 +978,9 @@ class PluginLoader:
907
978
  is_core_plugin = ctx.plugin_load_context.plugin_resolved_collection == 'ansible.builtin'
908
979
  if self.class_name == 'StrategyModule' and not is_core_plugin:
909
980
  display.deprecated( # pylint: disable=ansible-deprecated-no-version
910
- 'Use of strategy plugins not included in ansible.builtin are deprecated and do not carry '
911
- 'any backwards compatibility guarantees. No alternative for third party strategy plugins '
912
- 'is currently planned.'
981
+ msg='Use of strategy plugins not included in ansible.builtin are deprecated and do not carry '
982
+ 'any backwards compatibility guarantees. No alternative for third party strategy plugins '
983
+ 'is currently planned.',
913
984
  )
914
985
 
915
986
  return ctx.object
@@ -936,8 +1007,6 @@ class PluginLoader:
936
1007
  return get_with_context_result(None, plugin_load_context)
937
1008
 
938
1009
  fq_name = plugin_load_context.resolved_fqcn
939
- if '.' not in fq_name and plugin_load_context.plugin_resolved_collection:
940
- fq_name = '.'.join((plugin_load_context.plugin_resolved_collection, fq_name))
941
1010
  resolved_type_name = plugin_load_context.plugin_resolved_name
942
1011
  path = plugin_load_context.plugin_resolved_path
943
1012
  if (cached_result := (self._plugin_instance_cache or {}).get(fq_name)) and cached_result[1].resolved:
@@ -947,7 +1016,7 @@ class PluginLoader:
947
1016
  redirected_names = plugin_load_context.redirect_list or []
948
1017
 
949
1018
  if path not in self._module_cache:
950
- self._module_cache[path] = self._load_module_source(resolved_type_name, path)
1019
+ self._module_cache[path] = self._load_module_source(python_module_name=plugin_load_context._python_module_name, path=path)
951
1020
  found_in_cache = False
952
1021
 
953
1022
  self._load_config_defs(resolved_type_name, self._module_cache[path], path)
@@ -974,7 +1043,7 @@ class PluginLoader:
974
1043
  # A plugin may need to use its _load_name in __init__ (for example, to set
975
1044
  # or get options from config), so update the object before using the constructor
976
1045
  instance = object.__new__(obj)
977
- self._update_object(instance, resolved_type_name, path, redirected_names, fq_name)
1046
+ self._update_object(obj=instance, name=resolved_type_name, path=path, redirected_names=redirected_names, resolved=fq_name)
978
1047
  obj.__init__(instance, *args, **kwargs) # pylint: disable=unnecessary-dunder-call
979
1048
  obj = instance
980
1049
  except TypeError as e:
@@ -984,12 +1053,12 @@ class PluginLoader:
984
1053
  return get_with_context_result(None, plugin_load_context)
985
1054
  raise
986
1055
 
987
- self._update_object(obj, resolved_type_name, path, redirected_names, fq_name)
1056
+ self._update_object(obj=obj, name=resolved_type_name, path=path, redirected_names=redirected_names, resolved=fq_name)
988
1057
  if self._plugin_instance_cache is not None and getattr(obj, 'is_stateless', False):
989
1058
  self._plugin_instance_cache[fq_name] = (obj, plugin_load_context)
990
1059
  elif self._plugin_instance_cache is not None:
991
1060
  # The cache doubles as the load order, so record the FQCN even if the plugin hasn't set is_stateless = True
992
- self._plugin_instance_cache[fq_name] = (None, PluginLoadContext())
1061
+ self._plugin_instance_cache[fq_name] = (None, PluginLoadContext(self.type, self.package))
993
1062
  return get_with_context_result(obj, plugin_load_context)
994
1063
 
995
1064
  def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_cache=None, class_only=None):
@@ -1064,10 +1133,15 @@ class PluginLoader:
1064
1133
  basename = os.path.basename(name)
1065
1134
  is_j2 = isinstance(self, Jinja2Loader)
1066
1135
 
1136
+ if path in legacy_excluding_builtin:
1137
+ fqcn = basename
1138
+ else:
1139
+ fqcn = f"ansible.builtin.{basename}"
1140
+
1067
1141
  if is_j2:
1068
1142
  ref_name = path
1069
1143
  else:
1070
- ref_name = basename
1144
+ ref_name = fqcn
1071
1145
 
1072
1146
  if not is_j2 and basename in _PLUGIN_FILTERS[self.package]:
1073
1147
  # j2 plugins get processed in own class, here they would just be container files
@@ -1090,26 +1164,18 @@ class PluginLoader:
1090
1164
  yield path
1091
1165
  continue
1092
1166
 
1093
- if path in legacy_excluding_builtin:
1094
- fqcn = basename
1095
- else:
1096
- fqcn = f"ansible.builtin.{basename}"
1097
-
1098
1167
  if (cached_result := (self._plugin_instance_cache or {}).get(fqcn)) and cached_result[1].resolved:
1099
1168
  # Here just in case, but we don't call all() multiple times for vars plugins, so this should not be used.
1100
1169
  yield cached_result[0]
1101
1170
  continue
1102
1171
 
1103
1172
  if path not in self._module_cache:
1104
- if self.type in ('filter', 'test'):
1105
- # filter and test plugin files can contain multiple plugins
1106
- # they must have a unique python module name to prevent them from shadowing each other
1107
- full_name = '{0}_{1}'.format(abs(hash(path)), basename)
1108
- else:
1109
- full_name = basename
1173
+ path_context = PluginPathContext(path, path not in legacy_excluding_builtin)
1174
+ load_context = PluginLoadContext(self.type, self.package)
1175
+ load_context.resolve_legacy(basename, {basename: path_context})
1110
1176
 
1111
1177
  try:
1112
- module = self._load_module_source(full_name, path)
1178
+ module = self._load_module_source(python_module_name=load_context._python_module_name, path=path)
1113
1179
  except Exception as e:
1114
1180
  display.warning("Skipping plugin (%s), cannot load: %s" % (path, to_text(e)))
1115
1181
  continue
@@ -1147,7 +1213,7 @@ class PluginLoader:
1147
1213
  except TypeError as e:
1148
1214
  display.warning("Skipping plugin (%s) as it seems to be incomplete: %s" % (path, to_text(e)))
1149
1215
 
1150
- self._update_object(obj, basename, path, resolved=fqcn)
1216
+ self._update_object(obj=obj, name=basename, path=path, resolved=fqcn)
1151
1217
 
1152
1218
  if self._plugin_instance_cache is not None:
1153
1219
  needs_enabled = False
@@ -1239,7 +1305,7 @@ class Jinja2Loader(PluginLoader):
1239
1305
  try:
1240
1306
  # use 'parent' loader class to find files, but cannot return this as it can contain multiple plugins per file
1241
1307
  if plugin_path not in self._module_cache:
1242
- self._module_cache[plugin_path] = self._load_module_source(full_name, plugin_path)
1308
+ self._module_cache[plugin_path] = self._load_module_source(python_module_name=full_name, path=plugin_path)
1243
1309
  module = self._module_cache[plugin_path]
1244
1310
  obj = getattr(module, self.class_name)
1245
1311
  except Exception as e:
@@ -1262,7 +1328,7 @@ class Jinja2Loader(PluginLoader):
1262
1328
  plugin = self._plugin_wrapper_type(func)
1263
1329
  if plugin in plugins:
1264
1330
  continue
1265
- self._update_object(plugin, full, plugin_path, resolved=fq_name)
1331
+ self._update_object(obj=plugin, name=full, path=plugin_path, resolved=fq_name)
1266
1332
  plugins.append(plugin)
1267
1333
 
1268
1334
  return plugins
@@ -1276,7 +1342,7 @@ class Jinja2Loader(PluginLoader):
1276
1342
 
1277
1343
  requested_name = name
1278
1344
 
1279
- context = PluginLoadContext()
1345
+ context = PluginLoadContext(self.type, self.package)
1280
1346
 
1281
1347
  # avoid collection path for legacy
1282
1348
  name = name.removeprefix('ansible.legacy.')
@@ -1288,11 +1354,8 @@ class Jinja2Loader(PluginLoader):
1288
1354
  if isinstance(known_plugin, _DeferredPluginLoadFailure):
1289
1355
  raise known_plugin.ex
1290
1356
 
1291
- context.resolved = True
1292
- context.plugin_resolved_name = name
1293
- context.plugin_resolved_path = known_plugin._original_path
1294
- context.plugin_resolved_collection = 'ansible.builtin' if known_plugin.ansible_name.startswith('ansible.builtin.') else ''
1295
- context._resolved_fqcn = known_plugin.ansible_name
1357
+ context.resolve_legacy_jinja_plugin(name, known_plugin)
1358
+
1296
1359
  return get_with_context_result(known_plugin, context)
1297
1360
 
1298
1361
  plugin = None
@@ -1328,7 +1391,12 @@ class Jinja2Loader(PluginLoader):
1328
1391
 
1329
1392
  warning_text = f'{self.type.title()} "{key}" has been deprecated.{" " if warning_text else ""}{warning_text}'
1330
1393
 
1331
- display.deprecated(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection)
1394
+ display.deprecated( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
1395
+ msg=warning_text,
1396
+ version=removal_version,
1397
+ date=removal_date,
1398
+ deprecator=PluginInfo._from_collection_name(acr.collection),
1399
+ )
1332
1400
 
1333
1401
  # check removal
1334
1402
  tombstone_entry = routing_entry.get('tombstone')
@@ -1343,11 +1411,7 @@ class Jinja2Loader(PluginLoader):
1343
1411
  version=removal_version,
1344
1412
  date=removal_date,
1345
1413
  removed=True,
1346
- plugin=PluginInfo(
1347
- requested_name=acr.collection,
1348
- resolved_name=acr.collection,
1349
- type='collection',
1350
- ),
1414
+ deprecator=PluginInfo._from_collection_name(acr.collection),
1351
1415
  )
1352
1416
 
1353
1417
  raise AnsiblePluginRemovedError(exc_msg)
@@ -1400,7 +1464,7 @@ class Jinja2Loader(PluginLoader):
1400
1464
  plugin = self._plugin_wrapper_type(func)
1401
1465
  if plugin:
1402
1466
  context = plugin_impl.plugin_load_context
1403
- self._update_object(plugin, requested_name, plugin_impl.object._original_path, resolved=fq_name)
1467
+ self._update_object(obj=plugin, name=requested_name, path=plugin_impl.object._original_path, resolved=fq_name)
1404
1468
  # context will have filename, which for tests/filters might not be correct
1405
1469
  context._resolved_fqcn = plugin.ansible_name
1406
1470
  # FIXME: once we start caching these results, we'll be missing functions that would have loaded later
@@ -230,8 +230,8 @@ class LookupModule(LookupBase):
230
230
  display.vvvv("url lookup connecting to %s" % term)
231
231
  if self.get_option('follow_redirects') in ('yes', 'no'):
232
232
  display.deprecated(
233
- "Using 'yes' or 'no' for 'follow_redirects' parameter is deprecated.",
234
- version='2.22'
233
+ msg="Using 'yes' or 'no' for 'follow_redirects' parameter is deprecated.",
234
+ version='2.22',
235
235
  )
236
236
  try:
237
237
  response = open_url(