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
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):
@@ -426,7 +505,8 @@ class PluginLoader:
426
505
 
427
506
  # if type name != 'module_doc_fragment':
428
507
  if type_name in C.CONFIGURABLE_PLUGINS and not C.config.has_configuration_definition(type_name, name):
429
- documentation_source = getattr(module, 'DOCUMENTATION', '')
508
+ # trust-tagged source propagates to loaded values; expressions and templates in config require trust
509
+ documentation_source = _tags.TrustedAsTemplate().tag(getattr(module, 'DOCUMENTATION', ''))
430
510
  try:
431
511
  dstring = yaml.load(_tags.Origin(path=path).tag(documentation_source), Loader=AnsibleLoader)
432
512
  except ParserError as e:
@@ -488,7 +568,13 @@ class PluginLoader:
488
568
  entry = collection_meta.get('plugin_routing', {}).get(plugin_type, {}).get(subdir_qualified_resource, None)
489
569
  return entry
490
570
 
491
- def _find_fq_plugin(self, fq_name, extension, plugin_load_context, ignore_deprecated=False):
571
+ def _find_fq_plugin(
572
+ self,
573
+ fq_name: str,
574
+ extension: str | None,
575
+ plugin_load_context: PluginLoadContext,
576
+ ignore_deprecated: bool = False,
577
+ ) -> PluginLoadContext:
492
578
  """Search builtin paths to find a plugin. No external paths are searched,
493
579
  meaning plugins inside roles inside collections will be ignored.
494
580
  """
@@ -525,17 +611,13 @@ class PluginLoader:
525
611
  version=removal_version,
526
612
  date=removal_date,
527
613
  removed=True,
528
- plugin=PluginInfo(
529
- requested_name=acr.collection,
530
- resolved_name=acr.collection,
531
- type='collection',
532
- ),
614
+ deprecator=PluginInfo._from_collection_name(acr.collection),
533
615
  )
534
- plugin_load_context.removal_date = removal_date
535
- plugin_load_context.removal_version = removal_version
616
+ plugin_load_context.date = removal_date
617
+ plugin_load_context.version = removal_version
536
618
  plugin_load_context.resolved = True
537
619
  plugin_load_context.exit_reason = removed_msg
538
- raise AnsiblePluginRemovedError(removed_msg, plugin_load_context=plugin_load_context)
620
+ raise AnsiblePluginRemovedError(message=removed_msg, plugin_load_context=plugin_load_context)
539
621
 
540
622
  redirect = routing_metadata.get('redirect', None)
541
623
 
@@ -592,7 +674,7 @@ class PluginLoader:
592
674
  # look for any matching extension in the package location (sans filter)
593
675
  found_files = [f
594
676
  for f in glob.iglob(os.path.join(pkg_path, n_resource) + '.*')
595
- if os.path.isfile(f) and not f.endswith(C.MODULE_IGNORE_EXTS)]
677
+ if os.path.isfile(f) and not any(f.endswith(ext) for ext in C.MODULE_IGNORE_EXTS)]
596
678
 
597
679
  if not found_files:
598
680
  return plugin_load_context.nope('failed fuzzy extension match for {0} in {1}'.format(full_name, acr.collection))
@@ -623,7 +705,7 @@ class PluginLoader:
623
705
  collection_list: list[str] | None = None,
624
706
  ) -> PluginLoadContext:
625
707
  """ Find a plugin named name, returning contextual info about the load, recursively resolving redirection """
626
- plugin_load_context = PluginLoadContext()
708
+ plugin_load_context = PluginLoadContext(self.type, self.package)
627
709
  plugin_load_context.original_name = name
628
710
  while True:
629
711
  result = self._resolve_plugin_step(name, mod_type, ignore_deprecated, check_aliases, collection_list, plugin_load_context=plugin_load_context)
@@ -636,11 +718,8 @@ class PluginLoader:
636
718
  else:
637
719
  break
638
720
 
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...
721
+ for ex in plugin_load_context.raw_error_list:
722
+ display.error_as_warning(f"Error loading plugin {name!r}.", ex)
644
723
 
645
724
  # FIXME: store structured deprecation data in PluginLoadContext and use display.deprecate
646
725
  # if plugin_load_context.deprecated and C.config.get_config_value('DEPRECATION_WARNINGS'):
@@ -650,9 +729,15 @@ class PluginLoader:
650
729
 
651
730
  return plugin_load_context
652
731
 
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()):
732
+ def _resolve_plugin_step(
733
+ self,
734
+ name: str,
735
+ mod_type: str = '',
736
+ ignore_deprecated: bool = False,
737
+ check_aliases: bool = False,
738
+ collection_list: list[str] | None = None,
739
+ plugin_load_context: PluginLoadContext | None = None,
740
+ ) -> PluginLoadContext:
656
741
  if not plugin_load_context:
657
742
  raise ValueError('A PluginLoadContext is required')
658
743
 
@@ -707,11 +792,14 @@ class PluginLoader:
707
792
  except (AnsiblePluginRemovedError, AnsiblePluginCircularRedirect, AnsibleCollectionUnsupportedVersionError):
708
793
  # these are generally fatal, let them fly
709
794
  raise
710
- except ImportError as ie:
711
- plugin_load_context.import_error_list.append(ie)
712
795
  except Exception as ex:
713
- # FIXME: keep actual errors, not just assembled messages
714
- plugin_load_context.error_list.append(to_native(ex))
796
+ plugin_load_context.raw_error_list.append(ex)
797
+
798
+ # DTFIX-RELEASE: can we deprecate/remove these stringified versions?
799
+ if isinstance(ex, ImportError):
800
+ plugin_load_context.import_error_list.append(ex)
801
+ else:
802
+ plugin_load_context.error_list.append(str(ex))
715
803
 
716
804
  if plugin_load_context.error_list:
717
805
  display.debug(msg='plugin lookup for {0} failed; errors: {1}'.format(name, '; '.join(plugin_load_context.error_list)))
@@ -737,13 +825,7 @@ class PluginLoader:
737
825
  # requested mod_type
738
826
  pull_cache = self._plugin_path_cache[suffix]
739
827
  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
828
+ return plugin_load_context.resolve_legacy(name=name, pull_cache=pull_cache)
747
829
  except KeyError:
748
830
  # Cache miss. Now let's find the plugin
749
831
  pass
@@ -796,13 +878,7 @@ class PluginLoader:
796
878
 
797
879
  self._searched_paths.add(path)
798
880
  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
881
+ return plugin_load_context.resolve_legacy(name=name, pull_cache=pull_cache)
806
882
  except KeyError:
807
883
  # Didn't find the plugin in this directory. Load modules from the next one
808
884
  pass
@@ -810,18 +886,18 @@ class PluginLoader:
810
886
  # if nothing is found, try finding alias/deprecated
811
887
  if not name.startswith('_'):
812
888
  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
889
+
890
+ try:
891
+ plugin_load_context.resolve_legacy(name=alias_name, pull_cache=pull_cache)
892
+ except KeyError:
893
+ pass
894
+ else:
895
+ display.deprecated(
896
+ msg=f'Plugin {name!r} automatically redirected to {alias_name!r}.',
897
+ help_text=f'Use {alias_name!r} instead of {name!r} to refer to the plugin.',
898
+ version='2.23',
899
+ )
900
+
825
901
  return plugin_load_context
826
902
 
827
903
  # last ditch, if it's something that can be redirected, look for a builtin redirect before giving up
@@ -831,7 +907,7 @@ class PluginLoader:
831
907
 
832
908
  return plugin_load_context.nope('{0} is not eligible for last-chance resolution'.format(name))
833
909
 
834
- def has_plugin(self, name, collection_list=None):
910
+ def has_plugin(self, name: str, collection_list: list[str] | None = None) -> bool:
835
911
  """ Checks if a plugin named name exists """
836
912
 
837
913
  try:
@@ -842,41 +918,37 @@ class PluginLoader:
842
918
  # log and continue, likely an innocuous type/package loading failure in collections import
843
919
  display.debug('has_plugin error: {0}'.format(to_text(ex)))
844
920
 
845
- __contains__ = has_plugin
846
-
847
- def _load_module_source(self, name, path):
921
+ return False
848
922
 
849
- # avoid collisions across plugins
850
- if name.startswith('ansible_collections.'):
851
- full_name = name
852
- else:
853
- full_name = '.'.join([self.package, name])
923
+ __contains__ = has_plugin
854
924
 
855
- if full_name in sys.modules:
925
+ def _load_module_source(self, *, python_module_name: str, path: str) -> types.ModuleType:
926
+ if python_module_name in sys.modules:
856
927
  # Avoids double loading, See https://github.com/ansible/ansible/issues/13110
857
- return sys.modules[full_name]
928
+ return sys.modules[python_module_name]
858
929
 
859
930
  with warnings.catch_warnings():
860
931
  # FIXME: this still has issues if the module was previously imported but not "cached",
861
932
  # we should bypass this entire codepath for things that are directly importable
862
933
  warnings.simplefilter("ignore", RuntimeWarning)
863
- spec = importlib.util.spec_from_file_location(to_native(full_name), to_native(path))
934
+ spec = importlib.util.spec_from_file_location(to_native(python_module_name), to_native(path))
864
935
  module = importlib.util.module_from_spec(spec)
865
936
 
866
937
  # mimic import machinery; make the module-being-loaded available in sys.modules during import
867
938
  # and remove if there's a failure...
868
- sys.modules[full_name] = module
939
+ sys.modules[python_module_name] = module
869
940
 
870
941
  try:
871
942
  spec.loader.exec_module(module)
872
943
  except Exception:
873
- del sys.modules[full_name]
944
+ del sys.modules[python_module_name]
874
945
  raise
875
946
 
876
947
  return module
877
948
 
878
949
  def _update_object(
879
950
  self,
951
+ *,
880
952
  obj: _AnsiblePluginInfoMixin,
881
953
  name: str,
882
954
  path: str,
@@ -907,9 +979,9 @@ class PluginLoader:
907
979
  is_core_plugin = ctx.plugin_load_context.plugin_resolved_collection == 'ansible.builtin'
908
980
  if self.class_name == 'StrategyModule' and not is_core_plugin:
909
981
  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.'
982
+ msg='Use of strategy plugins not included in ansible.builtin are deprecated and do not carry '
983
+ 'any backwards compatibility guarantees. No alternative for third party strategy plugins '
984
+ 'is currently planned.',
913
985
  )
914
986
 
915
987
  return ctx.object
@@ -936,8 +1008,6 @@ class PluginLoader:
936
1008
  return get_with_context_result(None, plugin_load_context)
937
1009
 
938
1010
  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
1011
  resolved_type_name = plugin_load_context.plugin_resolved_name
942
1012
  path = plugin_load_context.plugin_resolved_path
943
1013
  if (cached_result := (self._plugin_instance_cache or {}).get(fq_name)) and cached_result[1].resolved:
@@ -947,7 +1017,7 @@ class PluginLoader:
947
1017
  redirected_names = plugin_load_context.redirect_list or []
948
1018
 
949
1019
  if path not in self._module_cache:
950
- self._module_cache[path] = self._load_module_source(resolved_type_name, path)
1020
+ self._module_cache[path] = self._load_module_source(python_module_name=plugin_load_context._python_module_name, path=path)
951
1021
  found_in_cache = False
952
1022
 
953
1023
  self._load_config_defs(resolved_type_name, self._module_cache[path], path)
@@ -974,7 +1044,7 @@ class PluginLoader:
974
1044
  # A plugin may need to use its _load_name in __init__ (for example, to set
975
1045
  # or get options from config), so update the object before using the constructor
976
1046
  instance = object.__new__(obj)
977
- self._update_object(instance, resolved_type_name, path, redirected_names, fq_name)
1047
+ self._update_object(obj=instance, name=resolved_type_name, path=path, redirected_names=redirected_names, resolved=fq_name)
978
1048
  obj.__init__(instance, *args, **kwargs) # pylint: disable=unnecessary-dunder-call
979
1049
  obj = instance
980
1050
  except TypeError as e:
@@ -984,12 +1054,12 @@ class PluginLoader:
984
1054
  return get_with_context_result(None, plugin_load_context)
985
1055
  raise
986
1056
 
987
- self._update_object(obj, resolved_type_name, path, redirected_names, fq_name)
1057
+ self._update_object(obj=obj, name=resolved_type_name, path=path, redirected_names=redirected_names, resolved=fq_name)
988
1058
  if self._plugin_instance_cache is not None and getattr(obj, 'is_stateless', False):
989
1059
  self._plugin_instance_cache[fq_name] = (obj, plugin_load_context)
990
1060
  elif self._plugin_instance_cache is not None:
991
1061
  # 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())
1062
+ self._plugin_instance_cache[fq_name] = (None, PluginLoadContext(self.type, self.package))
993
1063
  return get_with_context_result(obj, plugin_load_context)
994
1064
 
995
1065
  def _display_plugin_load(self, class_name, name, searched_paths, path, found_in_cache=None, class_only=None):
@@ -1064,10 +1134,15 @@ class PluginLoader:
1064
1134
  basename = os.path.basename(name)
1065
1135
  is_j2 = isinstance(self, Jinja2Loader)
1066
1136
 
1137
+ if path in legacy_excluding_builtin:
1138
+ fqcn = basename
1139
+ else:
1140
+ fqcn = f"ansible.builtin.{basename}"
1141
+
1067
1142
  if is_j2:
1068
1143
  ref_name = path
1069
1144
  else:
1070
- ref_name = basename
1145
+ ref_name = fqcn
1071
1146
 
1072
1147
  if not is_j2 and basename in _PLUGIN_FILTERS[self.package]:
1073
1148
  # j2 plugins get processed in own class, here they would just be container files
@@ -1090,26 +1165,18 @@ class PluginLoader:
1090
1165
  yield path
1091
1166
  continue
1092
1167
 
1093
- if path in legacy_excluding_builtin:
1094
- fqcn = basename
1095
- else:
1096
- fqcn = f"ansible.builtin.{basename}"
1097
-
1098
1168
  if (cached_result := (self._plugin_instance_cache or {}).get(fqcn)) and cached_result[1].resolved:
1099
1169
  # Here just in case, but we don't call all() multiple times for vars plugins, so this should not be used.
1100
1170
  yield cached_result[0]
1101
1171
  continue
1102
1172
 
1103
1173
  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
1174
+ path_context = PluginPathContext(path, path not in legacy_excluding_builtin)
1175
+ load_context = PluginLoadContext(self.type, self.package)
1176
+ load_context.resolve_legacy(basename, {basename: path_context})
1110
1177
 
1111
1178
  try:
1112
- module = self._load_module_source(full_name, path)
1179
+ module = self._load_module_source(python_module_name=load_context._python_module_name, path=path)
1113
1180
  except Exception as e:
1114
1181
  display.warning("Skipping plugin (%s), cannot load: %s" % (path, to_text(e)))
1115
1182
  continue
@@ -1147,7 +1214,7 @@ class PluginLoader:
1147
1214
  except TypeError as e:
1148
1215
  display.warning("Skipping plugin (%s) as it seems to be incomplete: %s" % (path, to_text(e)))
1149
1216
 
1150
- self._update_object(obj, basename, path, resolved=fqcn)
1217
+ self._update_object(obj=obj, name=basename, path=path, resolved=fqcn)
1151
1218
 
1152
1219
  if self._plugin_instance_cache is not None:
1153
1220
  needs_enabled = False
@@ -1239,7 +1306,7 @@ class Jinja2Loader(PluginLoader):
1239
1306
  try:
1240
1307
  # use 'parent' loader class to find files, but cannot return this as it can contain multiple plugins per file
1241
1308
  if plugin_path not in self._module_cache:
1242
- self._module_cache[plugin_path] = self._load_module_source(full_name, plugin_path)
1309
+ self._module_cache[plugin_path] = self._load_module_source(python_module_name=full_name, path=plugin_path)
1243
1310
  module = self._module_cache[plugin_path]
1244
1311
  obj = getattr(module, self.class_name)
1245
1312
  except Exception as e:
@@ -1262,7 +1329,7 @@ class Jinja2Loader(PluginLoader):
1262
1329
  plugin = self._plugin_wrapper_type(func)
1263
1330
  if plugin in plugins:
1264
1331
  continue
1265
- self._update_object(plugin, full, plugin_path, resolved=fq_name)
1332
+ self._update_object(obj=plugin, name=full, path=plugin_path, resolved=fq_name)
1266
1333
  plugins.append(plugin)
1267
1334
 
1268
1335
  return plugins
@@ -1276,7 +1343,7 @@ class Jinja2Loader(PluginLoader):
1276
1343
 
1277
1344
  requested_name = name
1278
1345
 
1279
- context = PluginLoadContext()
1346
+ context = PluginLoadContext(self.type, self.package)
1280
1347
 
1281
1348
  # avoid collection path for legacy
1282
1349
  name = name.removeprefix('ansible.legacy.')
@@ -1288,11 +1355,8 @@ class Jinja2Loader(PluginLoader):
1288
1355
  if isinstance(known_plugin, _DeferredPluginLoadFailure):
1289
1356
  raise known_plugin.ex
1290
1357
 
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
1358
+ context.resolve_legacy_jinja_plugin(name, known_plugin)
1359
+
1296
1360
  return get_with_context_result(known_plugin, context)
1297
1361
 
1298
1362
  plugin = None
@@ -1328,7 +1392,12 @@ class Jinja2Loader(PluginLoader):
1328
1392
 
1329
1393
  warning_text = f'{self.type.title()} "{key}" has been deprecated.{" " if warning_text else ""}{warning_text}'
1330
1394
 
1331
- display.deprecated(warning_text, version=removal_version, date=removal_date, collection_name=acr.collection)
1395
+ display.deprecated( # pylint: disable=ansible-deprecated-date-not-permitted,ansible-deprecated-unnecessary-collection-name
1396
+ msg=warning_text,
1397
+ version=removal_version,
1398
+ date=removal_date,
1399
+ deprecator=PluginInfo._from_collection_name(acr.collection),
1400
+ )
1332
1401
 
1333
1402
  # check removal
1334
1403
  tombstone_entry = routing_entry.get('tombstone')
@@ -1343,11 +1412,7 @@ class Jinja2Loader(PluginLoader):
1343
1412
  version=removal_version,
1344
1413
  date=removal_date,
1345
1414
  removed=True,
1346
- plugin=PluginInfo(
1347
- requested_name=acr.collection,
1348
- resolved_name=acr.collection,
1349
- type='collection',
1350
- ),
1415
+ deprecator=PluginInfo._from_collection_name(acr.collection),
1351
1416
  )
1352
1417
 
1353
1418
  raise AnsiblePluginRemovedError(exc_msg)
@@ -1400,7 +1465,7 @@ class Jinja2Loader(PluginLoader):
1400
1465
  plugin = self._plugin_wrapper_type(func)
1401
1466
  if plugin:
1402
1467
  context = plugin_impl.plugin_load_context
1403
- self._update_object(plugin, requested_name, plugin_impl.object._original_path, resolved=fq_name)
1468
+ self._update_object(obj=plugin, name=requested_name, path=plugin_impl.object._original_path, resolved=fq_name)
1404
1469
  # context will have filename, which for tests/filters might not be correct
1405
1470
  context._resolved_fqcn = plugin.ansible_name
1406
1471
  # 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(