ansible-core 2.19.0b5__py3-none-any.whl → 2.19.0b7__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 (184) hide show
  1. ansible/_internal/_ansiballz/__init__.py +0 -0
  2. ansible/_internal/_ansiballz/_builder.py +101 -0
  3. ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
  4. ansible/_internal/_templating/_jinja_bits.py +22 -4
  5. ansible/_internal/_templating/_jinja_common.py +1 -1
  6. ansible/_internal/_templating/_jinja_plugins.py +5 -2
  7. ansible/_internal/_templating/_template_vars.py +72 -0
  8. ansible/_internal/_templating/_transform.py +6 -0
  9. ansible/_internal/_yaml/_constructor.py +4 -4
  10. ansible/_internal/_yaml/_dumper.py +26 -18
  11. ansible/cli/__init__.py +9 -14
  12. ansible/cli/adhoc.py +6 -3
  13. ansible/cli/arguments/option_helpers.py +1 -1
  14. ansible/cli/console.py +2 -2
  15. ansible/cli/doc.py +4 -4
  16. ansible/cli/inventory.py +5 -7
  17. ansible/config/base.yml +33 -6
  18. ansible/errors/__init__.py +2 -1
  19. ansible/executor/module_common.py +75 -44
  20. ansible/executor/powershell/psrp_put_file.ps1 +1 -1
  21. ansible/executor/process/worker.py +2 -2
  22. ansible/executor/task_executor.py +2 -2
  23. ansible/executor/task_queue_manager.py +34 -70
  24. ansible/executor/task_result.py +1 -1
  25. ansible/galaxy/api.py +3 -6
  26. ansible/galaxy/collection/__init__.py +1 -6
  27. ansible/galaxy/collection/concrete_artifact_manager.py +4 -10
  28. ansible/galaxy/dependency_resolution/providers.py +3 -3
  29. ansible/galaxy/role.py +2 -2
  30. ansible/inventory/group.py +6 -1
  31. ansible/inventory/host.py +6 -1
  32. ansible/module_utils/_internal/__init__.py +7 -4
  33. ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
  34. ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
  35. ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
  36. ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
  37. ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +10 -38
  38. ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
  39. ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
  40. ansible/module_utils/_internal/_datatag/__init__.py +23 -1
  41. ansible/module_utils/_internal/_deprecator.py +39 -34
  42. ansible/module_utils/_internal/_json/_profiles/__init__.py +1 -0
  43. ansible/module_utils/_internal/_messages.py +26 -2
  44. ansible/module_utils/_internal/_plugin_info.py +14 -1
  45. ansible/module_utils/ansible_release.py +1 -1
  46. ansible/module_utils/basic.py +58 -70
  47. ansible/module_utils/common/respawn.py +4 -41
  48. ansible/module_utils/common/yaml.py +1 -1
  49. ansible/module_utils/connection.py +8 -11
  50. ansible/module_utils/csharp/Ansible.Basic.cs +1 -1
  51. ansible/module_utils/csharp/Ansible.Privilege.cs +2 -2
  52. ansible/module_utils/facts/hardware/base.py +1 -1
  53. ansible/module_utils/facts/hardware/linux.py +1 -1
  54. ansible/module_utils/facts/other/facter.py +1 -1
  55. ansible/module_utils/facts/sysctl.py +4 -6
  56. ansible/module_utils/facts/system/caps.py +2 -2
  57. ansible/module_utils/facts/system/distribution.py +2 -2
  58. ansible/module_utils/facts/system/local.py +1 -1
  59. ansible/module_utils/facts/virtual/linux.py +1 -1
  60. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  61. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  62. ansible/module_utils/powershell/Ansible.ModuleUtils.CommandUtil.psm1 +1 -1
  63. ansible/module_utils/powershell/Ansible.ModuleUtils.WebRequest.psm1 +1 -1
  64. ansible/module_utils/service.py +1 -1
  65. ansible/module_utils/urls.py +5 -5
  66. ansible/modules/apt.py +9 -3
  67. ansible/modules/apt_repository.py +10 -10
  68. ansible/modules/assemble.py +7 -5
  69. ansible/modules/async_wrapper.py +7 -17
  70. ansible/modules/command.py +3 -3
  71. ansible/modules/copy.py +4 -4
  72. ansible/modules/cron.py +1 -1
  73. ansible/modules/expect.py +5 -5
  74. ansible/modules/file.py +16 -17
  75. ansible/modules/find.py +3 -3
  76. ansible/modules/get_url.py +17 -0
  77. ansible/modules/git.py +9 -7
  78. ansible/modules/hostname.py +2 -2
  79. ansible/modules/known_hosts.py +12 -14
  80. ansible/modules/package.py +6 -0
  81. ansible/modules/pip.py +9 -11
  82. ansible/modules/raw.py +2 -2
  83. ansible/modules/replace.py +2 -2
  84. ansible/modules/slurp.py +10 -13
  85. ansible/modules/stat.py +6 -8
  86. ansible/modules/unarchive.py +6 -6
  87. ansible/modules/user.py +1 -1
  88. ansible/modules/wait_for.py +38 -33
  89. ansible/modules/yum_repository.py +4 -3
  90. ansible/parsing/dataloader.py +2 -2
  91. ansible/parsing/mod_args.py +38 -20
  92. ansible/parsing/vault/__init__.py +9 -13
  93. ansible/playbook/base.py +7 -4
  94. ansible/playbook/helpers.py +1 -1
  95. ansible/playbook/included_file.py +3 -1
  96. ansible/playbook/play_context.py +2 -0
  97. ansible/playbook/playbook_include.py +23 -56
  98. ansible/playbook/role/__init__.py +38 -21
  99. ansible/playbook/taggable.py +19 -5
  100. ansible/playbook/task.py +2 -0
  101. ansible/plugins/action/__init__.py +2 -2
  102. ansible/plugins/action/assemble.py +2 -1
  103. ansible/plugins/action/assert.py +2 -2
  104. ansible/plugins/action/fetch.py +3 -3
  105. ansible/plugins/action/script.py +5 -4
  106. ansible/plugins/action/template.py +9 -3
  107. ansible/plugins/cache/__init__.py +17 -19
  108. ansible/plugins/callback/__init__.py +77 -87
  109. ansible/plugins/callback/default.py +0 -3
  110. ansible/plugins/callback/junit.py +0 -6
  111. ansible/plugins/callback/tree.py +5 -5
  112. ansible/plugins/connection/local.py +4 -4
  113. ansible/plugins/connection/paramiko_ssh.py +5 -5
  114. ansible/plugins/connection/ssh.py +9 -7
  115. ansible/plugins/connection/winrm.py +1 -1
  116. ansible/plugins/filter/core.py +19 -21
  117. ansible/plugins/filter/encryption.py +10 -2
  118. ansible/plugins/filter/pow.yml +1 -1
  119. ansible/plugins/filter/root.yml +1 -1
  120. ansible/plugins/filter/strftime.yml +3 -3
  121. ansible/plugins/filter/to_uuid.yml +1 -1
  122. ansible/plugins/inventory/script.py +1 -1
  123. ansible/plugins/list.py +5 -4
  124. ansible/plugins/loader.py +5 -0
  125. ansible/plugins/lookup/password.py +4 -6
  126. ansible/plugins/lookup/template.py +9 -4
  127. ansible/plugins/shell/powershell.py +3 -2
  128. ansible/plugins/shell/sh.py +3 -2
  129. ansible/plugins/strategy/__init__.py +3 -3
  130. ansible/plugins/test/core.py +2 -2
  131. ansible/release.py +1 -1
  132. ansible/template/__init__.py +9 -53
  133. ansible/utils/collection_loader/_collection_finder.py +3 -3
  134. ansible/utils/display.py +38 -37
  135. ansible/utils/galaxy.py +2 -2
  136. ansible/utils/hashing.py +6 -7
  137. ansible/utils/path.py +6 -8
  138. ansible/utils/py3compat.py +2 -1
  139. ansible/utils/ssh_functions.py +3 -2
  140. ansible/utils/vars.py +4 -1
  141. ansible/vars/manager.py +6 -3
  142. ansible/vars/plugins.py +3 -3
  143. ansible/vars/reserved.py +6 -4
  144. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/METADATA +1 -1
  145. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/RECORD +184 -173
  146. ansible_test/_internal/__init__.py +5 -0
  147. ansible_test/_internal/ansible_util.py +1 -1
  148. ansible_test/_internal/classification/python.py +6 -0
  149. ansible_test/_internal/cli/commands/__init__.py +0 -5
  150. ansible_test/_internal/cli/environments.py +51 -5
  151. ansible_test/_internal/commands/coverage/__init__.py +1 -1
  152. ansible_test/_internal/commands/integration/__init__.py +18 -5
  153. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  154. ansible_test/_internal/commands/integration/coverage.py +7 -2
  155. ansible_test/_internal/commands/sanity/__init__.py +3 -1
  156. ansible_test/_internal/commands/sanity/integration_aliases.py +11 -0
  157. ansible_test/_internal/commands/shell/__init__.py +43 -4
  158. ansible_test/_internal/commands/units/__init__.py +4 -1
  159. ansible_test/_internal/config.py +21 -13
  160. ansible_test/_internal/debugging.py +166 -0
  161. ansible_test/_internal/delegation.py +21 -13
  162. ansible_test/_internal/host_profiles.py +259 -16
  163. ansible_test/_internal/inventory.py +4 -0
  164. ansible_test/_internal/metadata.py +94 -4
  165. ansible_test/_internal/processes.py +80 -0
  166. ansible_test/_internal/provisioning.py +10 -4
  167. ansible_test/_internal/python_requirements.py +27 -0
  168. ansible_test/_internal/ssh.py +1 -5
  169. ansible_test/_internal/target.py +8 -0
  170. ansible_test/_internal/thread.py +2 -1
  171. ansible_test/_internal/timeout.py +1 -1
  172. ansible_test/_internal/util.py +20 -12
  173. ansible_test/_internal/util_common.py +13 -3
  174. ansible_test/_util/target/injector/python.py +8 -0
  175. ansible_test/_util/target/setup/requirements.py +3 -9
  176. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/WHEEL +0 -0
  177. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/entry_points.txt +0 -0
  178. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/COPYING +0 -0
  179. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  180. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  181. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  182. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  183. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  184. {ansible_core-2.19.0b5.dist-info → ansible_core-2.19.0b7.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,6 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import errno
9
8
  import fnmatch
10
9
  import functools
11
10
  import glob
@@ -1689,11 +1688,7 @@ def _extract_tar_dir(tar, dirname, b_dest):
1689
1688
  b_dir_path = os.path.join(b_dest, to_bytes(dirname, errors='surrogate_or_strict'))
1690
1689
 
1691
1690
  b_parent_path = os.path.dirname(b_dir_path)
1692
- try:
1693
- os.makedirs(b_parent_path, mode=S_IRWXU_RXG_RXO)
1694
- except OSError as e:
1695
- if e.errno != errno.EEXIST:
1696
- raise
1691
+ os.makedirs(b_parent_path, mode=S_IRWXU_RXG_RXO, exist_ok=True)
1697
1692
 
1698
1693
  if tar_member.type == tarfile.SYMTYPE:
1699
1694
  b_link_path = to_bytes(tar_member.linkname, errors='surrogate_or_strict')
@@ -449,9 +449,9 @@ def _extract_collection_from_git(repo_url, coll_ver, b_path):
449
449
  except subprocess.CalledProcessError as proc_err:
450
450
  raise AnsibleError( # should probably be LookupError
451
451
  'Failed to switch a cloned Git repo `{repo_url!s}` '
452
- 'to the requested revision `{commitish!s}`.'.
452
+ 'to the requested revision `{revision!s}`.'.
453
453
  format(
454
- commitish=to_native(version),
454
+ revision=to_native(version),
455
455
  repo_url=to_native(git_url),
456
456
  ),
457
457
  ) from proc_err
@@ -656,14 +656,8 @@ def _get_json_from_installed_dir(
656
656
  try:
657
657
  with open(b_json_filepath, 'rb') as manifest_fd:
658
658
  b_json_text = manifest_fd.read()
659
- except (IOError, OSError):
660
- raise LookupError(
661
- "The collection {manifest!s} path '{path!s}' does not exist.".
662
- format(
663
- manifest=filename,
664
- path=to_native(b_json_filepath),
665
- )
666
- )
659
+ except OSError as ex:
660
+ raise LookupError(f"The collection {filename!r} path {to_text(b_json_filepath)!r} does not exist.") from ex
667
661
 
668
662
  manifest_txt = to_text(b_json_text, errors='surrogate_or_strict')
669
663
 
@@ -148,7 +148,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
148
148
 
149
149
  :param resolutions: Mapping of identifier, candidate pairs.
150
150
 
151
- :param candidates: Possible candidates for the identifer.
151
+ :param candidates: Possible candidates for the identifier.
152
152
  Mapping of identifier, list of candidate pairs.
153
153
 
154
154
  :param information: Requirement information of each package.
@@ -158,7 +158,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
158
158
  :param backtrack_causes: Sequence of requirement information that were
159
159
  the requirements that caused the resolver to most recently backtrack.
160
160
 
161
- The preference could depend on a various of issues, including
161
+ The preference could depend on various of issues, including
162
162
  (not necessarily in this order):
163
163
 
164
164
  * Is this package pinned in the current resolution result?
@@ -404,7 +404,7 @@ class CollectionDependencyProviderBase(AbstractProvider):
404
404
 
405
405
  :param requirement: A requirement that produced the `candidate`.
406
406
 
407
- :param candidate: A pinned candidate supposedly matchine the \
407
+ :param candidate: A pinned candidate supposedly matching the \
408
408
  `requirement` specifier. It is guaranteed to \
409
409
  have been generated from the `requirement`.
410
410
 
ansible/galaxy/role.py CHANGED
@@ -438,8 +438,8 @@ class GalaxyRole(object):
438
438
  if not (self.src and os.path.isfile(self.src)):
439
439
  try:
440
440
  os.unlink(tmp_file)
441
- except (OSError, IOError) as e:
442
- display.warning(u"Unable to remove tmp file (%s): %s" % (tmp_file, to_text(e)))
441
+ except OSError as ex:
442
+ display.error_as_warning(f"Unable to remove tmp file {tmp_file!r}.", exception=ex)
443
443
  return True
444
444
 
445
445
  return False
@@ -26,7 +26,7 @@ from ansible import constants as C
26
26
  from ansible.errors import AnsibleError
27
27
  from ansible.module_utils.common.text.converters import to_native, to_text
28
28
  from ansible.utils.display import Display
29
- from ansible.utils.vars import combine_vars
29
+ from ansible.utils.vars import combine_vars, validate_variable_name
30
30
 
31
31
  from . import helpers # this is left as a module import to facilitate easier unit test patching
32
32
 
@@ -221,6 +221,11 @@ class Group:
221
221
  def set_variable(self, key: str, value: t.Any) -> None:
222
222
  key = helpers.remove_trust(key)
223
223
 
224
+ try:
225
+ validate_variable_name(key)
226
+ except AnsibleError as ex:
227
+ Display().deprecated(msg=f'Accepting inventory variable with invalid name {key!r}.', version='2.23', help_text=ex._help_text, obj=ex.obj)
228
+
224
229
  if key == 'ansible_group_priority':
225
230
  self.set_priority(int(value))
226
231
  else:
ansible/inventory/host.py CHANGED
@@ -22,8 +22,10 @@ import typing as t
22
22
 
23
23
  from collections.abc import Mapping, MutableMapping
24
24
 
25
+ from ansible.errors import AnsibleError
25
26
  from ansible.inventory.group import Group, InventoryObjectType
26
27
  from ansible.parsing.utils.addresses import patterns
28
+ from ansible.utils.display import Display
27
29
  from ansible.utils.vars import combine_vars, get_unique_id, validate_variable_name
28
30
 
29
31
  from . import helpers # this is left as a module import to facilitate easier unit test patching
@@ -117,7 +119,10 @@ class Host:
117
119
  def set_variable(self, key: str, value: t.Any) -> None:
118
120
  key = helpers.remove_trust(key)
119
121
 
120
- validate_variable_name(key)
122
+ try:
123
+ validate_variable_name(key)
124
+ except AnsibleError as ex:
125
+ Display().deprecated(msg=f'Accepting inventory variable with invalid name {key!r}.', version='2.23', help_text=ex._help_text, obj=ex.obj)
121
126
 
122
127
  if key in self.vars and isinstance(self.vars[key], MutableMapping) and isinstance(value, Mapping):
123
128
  self.vars = combine_vars(self.vars, {key: value})
@@ -4,6 +4,9 @@ import collections.abc as c
4
4
 
5
5
  import typing as t
6
6
 
7
+ if t.TYPE_CHECKING:
8
+ from ansible.module_utils.compat.typing import TypeGuard
9
+
7
10
 
8
11
  INTERMEDIATE_MAPPING_TYPES = (c.Mapping,)
9
12
  """
@@ -18,18 +21,18 @@ These will be converted to a simple Python `list` before serialization or storag
18
21
  CAUTION: Scalar types which are sequences should be excluded when using this.
19
22
  """
20
23
 
21
- ITERABLE_SCALARS_NOT_TO_ITERATE_FIXME = (str, bytes)
24
+ ITERABLE_SCALARS_NOT_TO_ITERATE = (str, bytes)
22
25
  """Scalars which are also iterable, and should thus be excluded from iterable checks."""
23
26
 
24
27
 
25
- def is_intermediate_mapping(value: object) -> bool:
28
+ def is_intermediate_mapping(value: object) -> TypeGuard[c.Mapping]:
26
29
  """Returns `True` if `value` is a type supported for projection to a Python `dict`, otherwise returns `False`."""
27
30
  return isinstance(value, INTERMEDIATE_MAPPING_TYPES)
28
31
 
29
32
 
30
- def is_intermediate_iterable(value: object) -> bool:
33
+ def is_intermediate_iterable(value: object) -> TypeGuard[c.Iterable]:
31
34
  """Returns `True` if `value` is a type supported for projection to a Python `list`, otherwise returns `False`."""
32
- return isinstance(value, INTERMEDIATE_ITERABLE_TYPES) and not isinstance(value, ITERABLE_SCALARS_NOT_TO_ITERATE_FIXME)
35
+ return isinstance(value, INTERMEDIATE_ITERABLE_TYPES) and not isinstance(value, ITERABLE_SCALARS_NOT_TO_ITERATE)
33
36
 
34
37
 
35
38
  is_controller: bool = False
File without changes
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import atexit
4
+ import dataclasses
5
+ import importlib.util
6
+ import os
7
+ import sys
8
+
9
+ import typing as t
10
+
11
+
12
+ @dataclasses.dataclass(frozen=True)
13
+ class Options:
14
+ """Code coverage options."""
15
+
16
+ config: str
17
+ output: str | None
18
+
19
+
20
+ def run(args: dict[str, t.Any]) -> None: # pragma: nocover
21
+ """Bootstrap `coverage` for the current Ansible module invocation."""
22
+ options = Options(**args)
23
+
24
+ if options.output:
25
+ # Enable code coverage analysis of the module.
26
+ # This feature is for internal testing and may change without notice.
27
+ python_version_string = '.'.join(str(v) for v in sys.version_info[:2])
28
+ os.environ['COVERAGE_FILE'] = f'{options.output}=python-{python_version_string}=coverage'
29
+
30
+ import coverage
31
+
32
+ cov = coverage.Coverage(config_file=options.config)
33
+
34
+ def atexit_coverage() -> None:
35
+ cov.stop()
36
+ cov.save()
37
+
38
+ atexit.register(atexit_coverage)
39
+
40
+ cov.start()
41
+ else:
42
+ # Verify coverage is available without importing it.
43
+ # This will detect when a module would fail with coverage enabled with minimal overhead.
44
+ if importlib.util.find_spec('coverage') is None:
45
+ raise RuntimeError('Could not find the `coverage` Python module.')
@@ -0,0 +1,62 @@
1
+ """
2
+ Remote debugging support for AnsiballZ modules.
3
+
4
+ To use with PyCharm:
5
+
6
+ 1) Choose an available port for PyCharm to listen on (e.g. 5678).
7
+ 2) Create a Python Debug Server using that port.
8
+ 3) Start the Python Debug Server.
9
+ 4) Ensure the correct version of `pydevd-pycharm` is installed for the interpreter(s) which will run the code being debugged.
10
+ 5) Configure Ansible with the `_ANSIBALLZ_DEBUGGER_CONFIG` option.
11
+ See `Options` below for the structure of the debugger configuration.
12
+ Example configuration using an environment variable:
13
+ export _ANSIBLE_ANSIBALLZ_DEBUGGER_CONFIG='{"module": "pydevd_pycharm", "settrace": {"host": "localhost", "port": 5678, "suspend": false}}'
14
+ 6) Set any desired breakpoints.
15
+ 7) Run Ansible commands.
16
+
17
+ A similar process should work for other pydevd based debuggers, such as Visual Studio Code, but they have not been tested.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import dataclasses
23
+ import importlib
24
+ import json
25
+ import os
26
+ import pathlib
27
+
28
+ import typing as t
29
+
30
+
31
+ @dataclasses.dataclass(frozen=True)
32
+ class Options:
33
+ """Debugger options for pydevd and its derivatives."""
34
+
35
+ module: str = 'pydevd'
36
+ """The Python module which will be imported and which provides the `settrace` method."""
37
+ settrace: dict[str, object] = dataclasses.field(default_factory=dict)
38
+ """The options to pass to the `{module}.settrace` method."""
39
+ source_mapping: dict[str, str] = dataclasses.field(default_factory=dict)
40
+ """
41
+ A mapping of source paths to provide to pydevd.
42
+ This setting is used internally by AnsiballZ and is not required unless Ansible CLI commands are run from a different system than your IDE.
43
+ In that scenario, use this setting instead of configuring source mapping in your IDE.
44
+ The key is a path known to the IDE.
45
+ The value is the same path as known to the Ansible CLI.
46
+ Both file paths and directories are supported.
47
+ """
48
+
49
+
50
+ def run(args: dict[str, t.Any]) -> None: # pragma: nocover
51
+ """Enable remote debugging."""
52
+
53
+ options = Options(**args)
54
+ temp_dir = pathlib.Path(__file__).parent.parent.parent.parent.parent.parent
55
+ path_mapping = [[key, str(temp_dir / value)] for key, value in options.source_mapping.items()]
56
+
57
+ os.environ['PATHS_FROM_ECLIPSE_TO_PYTHON'] = json.dumps(path_mapping)
58
+
59
+ debugging_module = importlib.import_module(options.module)
60
+ debugging_module.settrace(**options.settrace)
61
+
62
+ pass # when suspend is True, execution pauses here -- it's also a convenient place to put a breakpoint
@@ -5,17 +5,15 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import atexit
9
- import importlib.util
8
+ import importlib
10
9
  import json
11
- import os
12
10
  import runpy
13
11
  import sys
14
12
  import typing as t
15
13
 
16
- from . import _errors, _traceback, _messages
17
- from .. import basic
18
- from ..common.json import get_module_encoder, Direction
14
+ from ansible.module_utils import basic
15
+ from ansible.module_utils._internal import _errors, _traceback, _messages, _ansiballz
16
+ from ansible.module_utils.common.json import get_module_encoder, Direction
19
17
 
20
18
 
21
19
  def run_module(
@@ -24,13 +22,16 @@ def run_module(
24
22
  profile: str,
25
23
  module_fqn: str,
26
24
  modlib_path: str,
25
+ extensions: dict[str, dict[str, object]],
27
26
  init_globals: dict[str, t.Any] | None = None,
28
- coverage_config: str | None = None,
29
- coverage_output: str | None = None,
30
27
  ) -> None: # pragma: nocover
31
28
  """Used internally by the AnsiballZ wrapper to run an Ansible module."""
32
29
  try:
33
- _enable_coverage(coverage_config, coverage_output)
30
+ for extension, args in extensions.items():
31
+ # importing _ansiballz instead of _extensions avoids an unnecessary import when extensions are not in use
32
+ extension_module = importlib.import_module(f'{_ansiballz.__name__}._extensions.{extension}')
33
+ extension_module.run(args)
34
+
34
35
  _run_module(
35
36
  json_params=json_params,
36
37
  profile=profile,
@@ -42,35 +43,6 @@ def run_module(
42
43
  _handle_exception(ex, profile)
43
44
 
44
45
 
45
- def _enable_coverage(coverage_config: str | None, coverage_output: str | None) -> None: # pragma: nocover
46
- """Bootstrap `coverage` for the current Ansible module invocation."""
47
- if not coverage_config:
48
- return
49
-
50
- if coverage_output:
51
- # Enable code coverage analysis of the module.
52
- # This feature is for internal testing and may change without notice.
53
- python_version_string = '.'.join(str(v) for v in sys.version_info[:2])
54
- os.environ['COVERAGE_FILE'] = f'{coverage_output}=python-{python_version_string}=coverage'
55
-
56
- import coverage
57
-
58
- cov = coverage.Coverage(config_file=coverage_config)
59
-
60
- def atexit_coverage():
61
- cov.stop()
62
- cov.save()
63
-
64
- atexit.register(atexit_coverage)
65
-
66
- cov.start()
67
- else:
68
- # Verify coverage is available without importing it.
69
- # This will detect when a module would fail with coverage enabled with minimal overhead.
70
- if importlib.util.find_spec('coverage') is None:
71
- raise RuntimeError('Could not find the `coverage` Python module.')
72
-
73
-
74
46
  def _run_module(
75
47
  *,
76
48
  json_params: bytes,
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import sys
5
+
6
+ from ... import basic
7
+ from . import _respawn_wrapper
8
+
9
+
10
+ def create_payload() -> str:
11
+ """Create and return an AnsiballZ payload for respawning a module."""
12
+ main = sys.modules['__main__']
13
+ code = inspect.getsource(_respawn_wrapper)
14
+
15
+ args = dict(
16
+ module_fqn=main._module_fqn,
17
+ modlib_path=main._modlib_path,
18
+ profile=basic._ANSIBLE_PROFILE,
19
+ json_params=basic._ANSIBLE_ARGS,
20
+ )
21
+
22
+ args_string = '\n'.join(f'{key}={value!r},' for key, value in args.items())
23
+
24
+ wrapper = f"""{code}
25
+
26
+ if __name__ == "__main__":
27
+ _respawn_main(
28
+ {args_string}
29
+ )
30
+ """
31
+
32
+ return wrapper
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def _respawn_main(
5
+ json_params: bytes,
6
+ profile: str,
7
+ module_fqn: str,
8
+ modlib_path: str,
9
+ ) -> None:
10
+ import sys
11
+
12
+ sys.path.insert(0, modlib_path)
13
+
14
+ from ansible.module_utils._internal._ansiballz import _loader
15
+
16
+ _loader.run_module(
17
+ json_params=json_params,
18
+ profile=profile,
19
+ module_fqn=module_fqn,
20
+ modlib_path=modlib_path,
21
+ extensions={},
22
+ init_globals=dict(_respawned=True),
23
+ )
@@ -5,6 +5,7 @@ import collections.abc as c
5
5
  import copy
6
6
  import dataclasses
7
7
  import datetime
8
+ import enum
8
9
  import inspect
9
10
  import sys
10
11
 
@@ -216,7 +217,7 @@ class AnsibleTagHelper:
216
217
  return value
217
218
 
218
219
 
219
- class AnsibleSerializable(metaclass=abc.ABCMeta):
220
+ class AnsibleSerializable:
220
221
  __slots__ = _NO_INSTANCE_STORAGE
221
222
 
222
223
  _known_type_map: t.ClassVar[t.Dict[str, t.Type['AnsibleSerializable']]] = {}
@@ -274,6 +275,27 @@ class AnsibleSerializable(metaclass=abc.ABCMeta):
274
275
  return f'{name}({arg_string})'
275
276
 
276
277
 
278
+ class AnsibleSerializableEnum(AnsibleSerializable, enum.Enum):
279
+ """Base class for serializable enumerations."""
280
+
281
+ def _as_dict(self) -> t.Dict[str, t.Any]:
282
+ return dict(value=self.value)
283
+
284
+ @classmethod
285
+ def _from_dict(cls, d: t.Dict[str, t.Any]) -> t.Self:
286
+ return cls(d['value'].lower())
287
+
288
+ def __str__(self) -> str:
289
+ return self.value
290
+
291
+ def __repr__(self) -> str:
292
+ return f'<{self.__class__.__name__}.{self.name}>'
293
+
294
+ @staticmethod
295
+ def _generate_next_value_(name, start, count, last_values):
296
+ return name.lower()
297
+
298
+
277
299
  class AnsibleSerializableWrapper(AnsibleSerializable, t.Generic[_T], metaclass=abc.ABCMeta):
278
300
  __slots__ = ('_value',)
279
301
 
@@ -5,7 +5,7 @@ import pathlib
5
5
  import sys
6
6
  import typing as t
7
7
 
8
- from ansible.module_utils._internal import _stack, _messages, _validation
8
+ from ansible.module_utils._internal import _stack, _messages, _validation, _plugin_info
9
9
 
10
10
 
11
11
  def deprecator_from_collection_name(collection_name: str | None) -> _messages.PluginInfo | None:
@@ -19,7 +19,7 @@ def deprecator_from_collection_name(collection_name: str | None) -> _messages.Pl
19
19
 
20
20
  return _messages.PluginInfo(
21
21
  resolved_name=collection_name,
22
- type=_COLLECTION_ONLY_TYPE,
22
+ type=None,
23
23
  )
24
24
 
25
25
 
@@ -38,11 +38,16 @@ def get_caller_plugin_info() -> _messages.PluginInfo | None:
38
38
  _skip_stackwalk = True
39
39
 
40
40
  if frame_info := _stack.caller_frame():
41
- return _path_as_core_plugininfo(frame_info.filename) or _path_as_collection_plugininfo(frame_info.filename)
41
+ return _path_as_plugininfo(frame_info.filename)
42
42
 
43
43
  return None # pragma: nocover
44
44
 
45
45
 
46
+ def _path_as_plugininfo(path: str) -> _messages.PluginInfo | None:
47
+ """Return a `PluginInfo` instance if the provided `path` refers to a plugin."""
48
+ return _path_as_core_plugininfo(path) or _path_as_collection_plugininfo(path)
49
+
50
+
46
51
  def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
47
52
  """Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
48
53
  try:
@@ -54,7 +59,7 @@ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
54
59
 
55
60
  if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
56
61
  plugin_name = match.group("plugin_name")
57
- plugin_type = match.group("plugin_type")
62
+ plugin_type = _plugin_info.normalize_plugin_type(match.group("plugin_type"))
58
63
 
59
64
  if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
60
65
  # The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
@@ -62,16 +67,20 @@ def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
62
67
  # Callers in this case need to identify the deprecating plugin name, otherwise only ansible-core will be reported.
63
68
  # Reporting ansible-core is never wrong, it just may be missing an additional detail (plugin name) in the "on behalf of" case.
64
69
  return ANSIBLE_CORE_DEPRECATOR
70
+
71
+ if plugin_name == '__init__':
72
+ # The plugin type is known, but the caller isn't a specific plugin -- instead, it's core plugin infrastructure (the base class).
73
+ return _messages.PluginInfo(resolved_name=namespace, type=plugin_type)
65
74
  elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
66
75
  # AnsiballZ Python package for core modules
67
76
  plugin_name = match.group("module_name")
68
- plugin_type = "module"
77
+ plugin_type = _messages.PluginType.MODULE
69
78
  elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
70
79
  # AnsiballZ Python package for non-core library/role modules
71
80
  namespace = 'ansible.legacy'
72
81
 
73
82
  plugin_name = match.group("module_name")
74
- plugin_type = "module"
83
+ plugin_type = _messages.PluginType.MODULE
75
84
  else:
76
85
  return ANSIBLE_CORE_DEPRECATOR # non-plugin core path, safe to use ansible-core for the same reason as the non-deprecator plugin type case above
77
86
 
@@ -85,7 +94,7 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
85
94
  if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
86
95
  return None
87
96
 
88
- plugin_type = match.group('plugin_type')
97
+ plugin_type = _plugin_info.normalize_plugin_type(match.group('plugin_type'))
89
98
 
90
99
  if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
91
100
  # We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
@@ -93,9 +102,6 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
93
102
  # In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
94
103
  return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
95
104
 
96
- if plugin_type == 'modules':
97
- plugin_type = 'module'
98
-
99
105
  if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
100
106
  # The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
101
107
  # We have no way of knowing if the intermediate code is deprecating its own feature, or acting on behalf of another plugin.
@@ -104,11 +110,10 @@ def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
104
110
 
105
111
  name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
106
112
 
107
- return _messages.PluginInfo(resolved_name=name, type=plugin_type)
113
+ # DTFIX-FUTURE: deprecations from __init__ will be incorrectly attributed to a plugin of that name
108
114
 
115
+ return _messages.PluginInfo(resolved_name=name, type=plugin_type)
109
116
 
110
- _COLLECTION_ONLY_TYPE: t.Final = 'collection'
111
- """Ersatz placeholder plugin type for use by a `PluginInfo` instance that references only a collection."""
112
117
 
113
118
  _ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
114
119
  """Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
@@ -116,37 +121,37 @@ _ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file_
116
121
  ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin')
117
122
  """Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
118
123
 
119
- INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name='indeterminate', type='indeterminate')
124
+ INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name=None, type=None)
120
125
  """Singleton `PluginInfo` instance for indeterminate deprecator."""
121
126
 
122
127
  _DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
123
128
  {
124
- 'action',
125
- 'become',
126
- 'cache',
127
- 'callback',
128
- 'cliconf',
129
- 'connection',
130
- # doc_fragments - no code execution
131
- # filter - basename inadequate to identify plugin
132
- 'httpapi',
133
- 'inventory',
134
- 'lookup',
135
- 'module', # only for collections
136
- 'netconf',
137
- 'shell',
138
- 'strategy',
139
- 'terminal',
140
- # test - basename inadequate to identify plugin
141
- 'vars',
129
+ _messages.PluginType.ACTION,
130
+ _messages.PluginType.BECOME,
131
+ _messages.PluginType.CACHE,
132
+ _messages.PluginType.CALLBACK,
133
+ _messages.PluginType.CLICONF,
134
+ _messages.PluginType.CONNECTION,
135
+ # DOC_FRAGMENTS - no code execution
136
+ # FILTER - basename inadequate to identify plugin
137
+ _messages.PluginType.HTTPAPI,
138
+ _messages.PluginType.INVENTORY,
139
+ _messages.PluginType.LOOKUP,
140
+ _messages.PluginType.MODULE, # only for collections
141
+ _messages.PluginType.NETCONF,
142
+ _messages.PluginType.SHELL,
143
+ _messages.PluginType.STRATEGY,
144
+ _messages.PluginType.TERMINAL,
145
+ # TEST - basename inadequate to identify plugin
146
+ _messages.PluginType.VARS,
142
147
  }
143
148
  )
144
149
  """Plugin types which are valid for identifying a deprecator for deprecation purposes."""
145
150
 
146
151
  _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
147
152
  {
148
- 'filter',
149
- 'test',
153
+ _messages.PluginType.FILTER,
154
+ _messages.PluginType.TEST,
150
155
  }
151
156
  )
152
157
  """Plugin types for which basename cannot be used to identify the plugin name."""
@@ -87,6 +87,7 @@ For controller-to-module, type behavior is profile dependent.
87
87
  _common_module_response_types: frozenset[type[AnsibleSerializable]] = frozenset(
88
88
  {
89
89
  _messages.PluginInfo,
90
+ _messages.PluginType,
90
91
  _messages.Event,
91
92
  _messages.EventChain,
92
93
  _messages.ErrorSummary,