ansible-core 2.19.0b3__py3-none-any.whl → 2.19.0b5__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.
- ansible/_internal/__init__.py +2 -2
- ansible/_internal/_collection_proxy.py +1 -1
- ansible/_internal/_errors/_alarm_timeout.py +66 -0
- ansible/_internal/_errors/_captured.py +25 -30
- ansible/_internal/_errors/_error_factory.py +89 -0
- ansible/_internal/_errors/_error_utils.py +240 -0
- ansible/_internal/_errors/_task_timeout.py +28 -0
- ansible/_internal/_event_formatting.py +127 -0
- ansible/_internal/_json/__init__.py +6 -6
- ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
- ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
- ansible/_internal/_json/_profiles/_legacy.py +3 -11
- ansible/_internal/_ssh/__init__.py +0 -0
- ansible/_internal/_ssh/_agent_launch.py +91 -0
- ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
- ansible/_internal/_templating/__init__.py +5 -3
- ansible/_internal/_templating/_datatag.py +2 -1
- ansible/_internal/_templating/_engine.py +3 -4
- ansible/_internal/_templating/_jinja_bits.py +21 -16
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +31 -3
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_transform.py +20 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_testing.py +26 -0
- ansible/_internal/_yaml/_dumper.py +1 -1
- ansible/_internal/_yaml/_errors.py +7 -7
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
- ansible/cli/__init__.py +5 -82
- ansible/cli/arguments/option_helpers.py +8 -5
- ansible/cli/doc.py +84 -28
- ansible/cli/inventory.py +1 -1
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +27 -23
- ansible/config/manager.py +142 -101
- ansible/constants.py +1 -1
- ansible/errors/__init__.py +96 -49
- ansible/executor/module_common.py +8 -10
- ansible/executor/powershell/async_watchdog.ps1 +2 -2
- ansible/executor/powershell/async_wrapper.ps1 +3 -3
- ansible/executor/powershell/become_wrapper.ps1 +20 -2
- ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
- ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
- ansible/executor/powershell/exec_wrapper.ps1 +219 -6
- ansible/executor/powershell/module_manifest.py +52 -0
- ansible/executor/powershell/module_wrapper.ps1 +47 -21
- ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
- ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
- ansible/executor/process/worker.py +38 -113
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/collection/__init__.py +1 -4
- ansible/inventory/manager.py +1 -0
- ansible/module_utils/_internal/__init__.py +0 -3
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz.py +4 -2
- ansible/module_utils/_internal/_datatag/__init__.py +20 -14
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +66 -48
- ansible/module_utils/_internal/_errors.py +88 -17
- ansible/module_utils/_internal/_event_utils.py +61 -0
- ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
- ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +1 -1
- ansible/module_utils/_internal/_stack.py +22 -0
- ansible/module_utils/_internal/_text_utils.py +6 -0
- ansible/module_utils/_internal/_traceback.py +11 -8
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +49 -15
- ansible/module_utils/common/arg_spec.py +2 -2
- ansible/module_utils/common/collections.py +6 -0
- ansible/module_utils/common/json.py +2 -2
- ansible/module_utils/common/text/converters.py +3 -3
- ansible/module_utils/common/validation.py +1 -1
- ansible/module_utils/common/warnings.py +80 -23
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/virtual/linux.py +2 -2
- ansible/module_utils/parsing/convert_bool.py +6 -0
- ansible/module_utils/service.py +2 -9
- ansible/modules/apt_repository.py +7 -29
- ansible/modules/assemble.py +4 -4
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +5 -5
- ansible/modules/cron.py +3 -5
- ansible/modules/dnf5.py +15 -22
- ansible/modules/git.py +1 -6
- ansible/modules/hostname.py +0 -1
- ansible/modules/pip.py +2 -4
- ansible/modules/service.py +3 -9
- ansible/modules/sysvinit.py +3 -3
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +4 -4
- ansible/parsing/mod_args.py +1 -1
- ansible/parsing/plugin_docs.py +2 -2
- ansible/parsing/utils/yaml.py +3 -3
- ansible/parsing/vault/__init__.py +4 -4
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +0 -3
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +9 -32
- ansible/plugins/action/add_host.py +1 -1
- ansible/plugins/action/assemble.py +8 -16
- ansible/plugins/action/async_status.py +7 -2
- ansible/plugins/action/copy.py +8 -7
- ansible/plugins/action/gather_facts.py +8 -8
- ansible/plugins/action/package.py +5 -8
- ansible/plugins/action/script.py +8 -15
- ansible/plugins/action/service.py +3 -7
- ansible/plugins/action/template.py +6 -8
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/connection/local.py +2 -2
- ansible/plugins/connection/ssh.py +17 -9
- ansible/plugins/connection/winrm.py +5 -2
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +13 -6
- ansible/plugins/filter/encryption.py +4 -4
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +69 -16
- ansible/plugins/loader.py +10 -9
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +66 -9
- ansible/plugins/shell/sh.py +9 -5
- ansible/plugins/test/core.py +21 -15
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +30 -2
- ansible/utils/collection_loader/__init__.py +2 -0
- ansible/utils/display.py +107 -128
- ansible/utils/hashing.py +0 -1
- ansible/utils/listify.py +6 -4
- ansible/utils/plugin_docs.py +2 -1
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
- ansible_test/_data/completion/docker.txt +3 -3
- ansible_test/_data/completion/remote.txt +1 -0
- ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +2 -2
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +4 -4
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/util.py +20 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -37,15 +37,16 @@ _ANSIBLE_TAGGED_OBJECT_SLOTS = tuple(('_ansible_tags_mapping',))
|
|
37
37
|
# shared empty frozenset for default values
|
38
38
|
_empty_frozenset: t.FrozenSet = frozenset()
|
39
39
|
|
40
|
+
# Technical Notes
|
41
|
+
#
|
42
|
+
# Tagged values compare (and thus hash) the same as their base types, so a value that differs only by its tags will appear identical to non-tag-aware code.
|
43
|
+
# This will affect storage and update of tagged values in dictionary keys, sets, etc. While tagged values can be used as keys in hashable collections,
|
44
|
+
# updating a key usually requires removal and re-addition.
|
45
|
+
|
40
46
|
|
41
47
|
class AnsibleTagHelper:
|
42
48
|
"""Utility methods for working with Ansible data tags."""
|
43
49
|
|
44
|
-
# DTFIX-RELEASE: bikeshed the name and location of this class, also, related, how much more of it should be exposed as public API?
|
45
|
-
# it may make sense to move this into another module, but the implementations should remain here (so they can be used without circular imports here)
|
46
|
-
# if they're in a separate module, is a class even needed, or should they be globals?
|
47
|
-
# DTFIX-RELEASE: add docstrings to all non-override methods in this class
|
48
|
-
|
49
50
|
@staticmethod
|
50
51
|
def untag(value: _T, *tag_types: t.Type[AnsibleDatatagBase]) -> _T:
|
51
52
|
"""
|
@@ -105,7 +106,7 @@ class AnsibleTagHelper:
|
|
105
106
|
if issubclass(the_type, AnsibleTaggedObject):
|
106
107
|
the_type = type_or_value._native_type
|
107
108
|
|
108
|
-
# DTFIX-
|
109
|
+
# DTFIX-FUTURE: provide a knob to optionally report the real type for debugging purposes
|
109
110
|
return the_type
|
110
111
|
|
111
112
|
@staticmethod
|
@@ -339,10 +340,11 @@ class AnsibleSerializableDateTime(AnsibleSerializableWrapper[datetime.datetime])
|
|
339
340
|
@dataclasses.dataclass(**_tag_dataclass_kwargs)
|
340
341
|
class AnsibleSerializableDataclass(AnsibleSerializable, metaclass=abc.ABCMeta):
|
341
342
|
_validation_allow_subclasses = True
|
343
|
+
_validation_auto_enabled = True
|
342
344
|
|
343
345
|
def _as_dict(self) -> t.Dict[str, t.Any]:
|
344
346
|
# omit None values when None is the field default
|
345
|
-
# DTFIX-
|
347
|
+
# DTFIX-FUTURE: this implementation means we can never change the default on fields which have None for their default
|
346
348
|
# other defaults can be changed -- but there's no way to override this behavior either way for other default types
|
347
349
|
# it's a trip hazard to have the default logic here, rather than per field (or not at all)
|
348
350
|
# consider either removing the filtering or requiring it to be explicitly set per field using dataclass metadata
|
@@ -351,7 +353,7 @@ class AnsibleSerializableDataclass(AnsibleSerializable, metaclass=abc.ABCMeta):
|
|
351
353
|
|
352
354
|
@classmethod
|
353
355
|
def _from_dict(cls, d: t.Dict[str, t.Any]) -> t.Self:
|
354
|
-
# DTFIX-
|
356
|
+
# DTFIX-FUTURE: optimize this to avoid the dataclasses fields metadata and get_origin stuff at runtime
|
355
357
|
type_hints = t.get_type_hints(cls)
|
356
358
|
mutated_dict: dict[str, t.Any] | None = None
|
357
359
|
|
@@ -368,7 +370,11 @@ class AnsibleSerializableDataclass(AnsibleSerializable, metaclass=abc.ABCMeta):
|
|
368
370
|
def __init_subclass__(cls, **kwargs) -> None:
|
369
371
|
super(AnsibleSerializableDataclass, cls).__init_subclass__(**kwargs) # cannot use super() without arguments when using slots
|
370
372
|
|
371
|
-
|
373
|
+
if cls._validation_auto_enabled:
|
374
|
+
try:
|
375
|
+
_dataclass_validation.inject_post_init_validation(cls, cls._validation_allow_subclasses) # code gen a real __post_init__ method
|
376
|
+
except Exception as ex:
|
377
|
+
raise Exception(f'Validation code generation failed on {cls}.') from ex
|
372
378
|
|
373
379
|
|
374
380
|
class Tripwire:
|
@@ -524,7 +530,6 @@ class CollectionWithMro(c.Collection, t.Protocol):
|
|
524
530
|
__mro__: tuple[type, ...]
|
525
531
|
|
526
532
|
|
527
|
-
# DTFIX-RELEASE: This should probably reside elsewhere.
|
528
533
|
def is_non_scalar_collection_type(value: type) -> t.TypeGuard[type[CollectionWithMro]]:
|
529
534
|
"""Returns True if the value is a non-scalar collection type, otherwise returns False."""
|
530
535
|
return issubclass(value, c.Collection) and not issubclass(value, str) and not issubclass(value, bytes)
|
@@ -878,7 +883,6 @@ class _AnsibleTaggedList(list, AnsibleTaggedObject):
|
|
878
883
|
# Propagation of tags in these cases is left to the caller, based on needs specific to their use case.
|
879
884
|
|
880
885
|
|
881
|
-
# DTFIX-RELEASE: do we want frozenset too?
|
882
886
|
class _AnsibleTaggedSet(set, AnsibleTaggedObject):
|
883
887
|
__slots__ = _ANSIBLE_TAGGED_OBJECT_SLOTS
|
884
888
|
|
@@ -914,10 +918,12 @@ class _AnsibleTaggedTuple(tuple, AnsibleTaggedObject):
|
|
914
918
|
return super()._copy_collection()
|
915
919
|
|
916
920
|
|
917
|
-
# This set gets augmented with additional types when some controller-only types are imported.
|
918
|
-
# While we could proxy or subclass builtin singletons, they're idiomatically compared with "is" reference
|
919
|
-
# equality, which we can't customize.
|
920
921
|
_untaggable_types = {type(None), bool}
|
922
|
+
"""
|
923
|
+
Attempts to apply tags to values of these types will be silently ignored.
|
924
|
+
While we could proxy or subclass builtin singletons, they're idiomatically compared with "is" reference equality, which we can't customize.
|
925
|
+
This set gets augmented with additional types when some controller-only types are imported.
|
926
|
+
"""
|
921
927
|
|
922
928
|
# noinspection PyProtectedMember
|
923
929
|
_ANSIBLE_ALLOWED_VAR_TYPES = frozenset({type(None), bool}) | set(AnsibleTaggedObject._tagged_type_map) | set(AnsibleTaggedObject._tagged_type_map.values())
|
@@ -3,8 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import dataclasses
|
4
4
|
import typing as t
|
5
5
|
|
6
|
-
from ansible.module_utils.
|
7
|
-
from ansible.module_utils._internal import _datatag
|
6
|
+
from ansible.module_utils._internal import _datatag, _messages
|
8
7
|
|
9
8
|
|
10
9
|
@dataclasses.dataclass(**_datatag._tag_dataclass_kwargs)
|
@@ -14,3 +13,4 @@ class Deprecated(_datatag.AnsibleDatatagBase):
|
|
14
13
|
date: t.Optional[str] = None
|
15
14
|
version: t.Optional[str] = None
|
16
15
|
deprecator: t.Optional[_messages.PluginInfo] = None
|
16
|
+
formatted_traceback: t.Optional[str] = None
|
@@ -1,79 +1,52 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import inspect
|
4
3
|
import re
|
5
4
|
import pathlib
|
6
5
|
import sys
|
7
6
|
import typing as t
|
8
7
|
|
9
|
-
from ansible.module_utils.
|
8
|
+
from ansible.module_utils._internal import _stack, _messages, _validation
|
10
9
|
|
11
|
-
_ansible_module_base_path: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
|
12
|
-
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
|
13
10
|
|
14
|
-
|
15
|
-
"""
|
11
|
+
def deprecator_from_collection_name(collection_name: str | None) -> _messages.PluginInfo | None:
|
12
|
+
"""Returns an instance with the special `collection` type to refer to a non-plugin or ambiguous caller within a collection."""
|
13
|
+
# CAUTION: This function is exposed in public API as ansible.module_utils.datatag.deprecator_from_collection_name.
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
if not collection_name:
|
16
|
+
return None
|
19
17
|
|
20
|
-
|
21
|
-
{
|
22
|
-
'action',
|
23
|
-
'become',
|
24
|
-
'cache',
|
25
|
-
'callback',
|
26
|
-
'cliconf',
|
27
|
-
'connection',
|
28
|
-
# doc_fragments - no code execution
|
29
|
-
# filter - basename inadequate to identify plugin
|
30
|
-
'httpapi',
|
31
|
-
'inventory',
|
32
|
-
'lookup',
|
33
|
-
'module', # only for collections
|
34
|
-
'netconf',
|
35
|
-
'shell',
|
36
|
-
'strategy',
|
37
|
-
'terminal',
|
38
|
-
# test - basename inadequate to identify plugin
|
39
|
-
'vars',
|
40
|
-
}
|
41
|
-
)
|
42
|
-
"""Plugin types which are valid for identifying a deprecator for deprecation purposes."""
|
18
|
+
_validation.validate_collection_name(collection_name)
|
43
19
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
}
|
49
|
-
)
|
50
|
-
"""Plugin types for which basename cannot be used to identify the plugin name."""
|
20
|
+
return _messages.PluginInfo(
|
21
|
+
resolved_name=collection_name,
|
22
|
+
type=_COLLECTION_ONLY_TYPE,
|
23
|
+
)
|
51
24
|
|
52
25
|
|
53
|
-
def get_best_deprecator(*, deprecator: PluginInfo | None = None, collection_name: str | None = None) -> PluginInfo:
|
26
|
+
def get_best_deprecator(*, deprecator: _messages.PluginInfo | None = None, collection_name: str | None = None) -> _messages.PluginInfo:
|
54
27
|
"""Return the best-available `PluginInfo` for the caller of this method."""
|
55
28
|
_skip_stackwalk = True
|
56
29
|
|
57
30
|
if deprecator and collection_name:
|
58
31
|
raise ValueError('Specify only one of `deprecator` or `collection_name`.')
|
59
32
|
|
60
|
-
return deprecator or
|
33
|
+
return deprecator or deprecator_from_collection_name(collection_name) or get_caller_plugin_info() or INDETERMINATE_DEPRECATOR
|
61
34
|
|
62
35
|
|
63
|
-
def get_caller_plugin_info() -> PluginInfo | None:
|
36
|
+
def get_caller_plugin_info() -> _messages.PluginInfo | None:
|
64
37
|
"""Try to get `PluginInfo` for the caller of this method, ignoring marked infrastructure stack frames."""
|
65
38
|
_skip_stackwalk = True
|
66
39
|
|
67
|
-
if frame_info :=
|
40
|
+
if frame_info := _stack.caller_frame():
|
68
41
|
return _path_as_core_plugininfo(frame_info.filename) or _path_as_collection_plugininfo(frame_info.filename)
|
69
42
|
|
70
43
|
return None # pragma: nocover
|
71
44
|
|
72
45
|
|
73
|
-
def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
|
46
|
+
def _path_as_core_plugininfo(path: str) -> _messages.PluginInfo | None:
|
74
47
|
"""Return a `PluginInfo` instance if the provided `path` refers to a core plugin."""
|
75
48
|
try:
|
76
|
-
relpath = str(pathlib.Path(path).relative_to(
|
49
|
+
relpath = str(pathlib.Path(path).relative_to(_ANSIBLE_MODULE_BASE_PATH))
|
77
50
|
except ValueError:
|
78
51
|
return None # not ansible-core
|
79
52
|
|
@@ -104,10 +77,10 @@ def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
|
|
104
77
|
|
105
78
|
name = f'{namespace}.{plugin_name}'
|
106
79
|
|
107
|
-
return PluginInfo(resolved_name=name, type=plugin_type)
|
80
|
+
return _messages.PluginInfo(resolved_name=name, type=plugin_type)
|
108
81
|
|
109
82
|
|
110
|
-
def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
|
83
|
+
def _path_as_collection_plugininfo(path: str) -> _messages.PluginInfo | None:
|
111
84
|
"""Return a `PluginInfo` instance if the provided `path` refers to a collection plugin."""
|
112
85
|
if not (match := re.search(r'/ansible_collections/(?P<ns>\w+)/(?P<coll>\w+)/plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', path)):
|
113
86
|
return None
|
@@ -118,7 +91,7 @@ def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
|
|
118
91
|
# We're able to detect the namespace, collection and plugin type -- but we have no way to identify the plugin name currently.
|
119
92
|
# To keep things simple we'll fall back to just identifying the namespace and collection.
|
120
93
|
# In the future we could improve the detection and/or make it easier for a caller to identify the plugin name.
|
121
|
-
return
|
94
|
+
return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
|
122
95
|
|
123
96
|
if plugin_type == 'modules':
|
124
97
|
plugin_type = 'module'
|
@@ -131,4 +104,49 @@ def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
|
|
131
104
|
|
132
105
|
name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
|
133
106
|
|
134
|
-
return PluginInfo(resolved_name=name, type=plugin_type)
|
107
|
+
return _messages.PluginInfo(resolved_name=name, type=plugin_type)
|
108
|
+
|
109
|
+
|
110
|
+
_COLLECTION_ONLY_TYPE: t.Final = 'collection'
|
111
|
+
"""Ersatz placeholder plugin type for use by a `PluginInfo` instance that references only a collection."""
|
112
|
+
|
113
|
+
_ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
|
114
|
+
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
|
115
|
+
|
116
|
+
ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin')
|
117
|
+
"""Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
|
118
|
+
|
119
|
+
INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name='indeterminate', type='indeterminate')
|
120
|
+
"""Singleton `PluginInfo` instance for indeterminate deprecator."""
|
121
|
+
|
122
|
+
_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
|
123
|
+
{
|
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',
|
142
|
+
}
|
143
|
+
)
|
144
|
+
"""Plugin types which are valid for identifying a deprecator for deprecation purposes."""
|
145
|
+
|
146
|
+
_AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
|
147
|
+
{
|
148
|
+
'filter',
|
149
|
+
'test',
|
150
|
+
}
|
151
|
+
)
|
152
|
+
"""Plugin types for which basename cannot be used to identify the plugin name."""
|
@@ -3,28 +3,99 @@
|
|
3
3
|
|
4
4
|
"""Internal error handling logic for targets. Not for use on the controller."""
|
5
5
|
|
6
|
-
from __future__ import annotations
|
6
|
+
from __future__ import annotations as _annotations
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
import traceback as _sys_traceback
|
9
|
+
import typing as _t
|
10
10
|
|
11
|
+
from . import _messages
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
return ErrorSummary(
|
15
|
-
details=_create_error_details(exception),
|
16
|
-
formatted_traceback=_traceback.maybe_extract_traceback(exception, _traceback.TracebackEvent.ERROR),
|
17
|
-
)
|
13
|
+
MSG_REASON_DIRECT_CAUSE: _t.Final[str] = '<<< caused by >>>'
|
14
|
+
MSG_REASON_HANDLING_CAUSE: _t.Final[str] = '<<< while handling >>>'
|
18
15
|
|
16
|
+
TRACEBACK_REASON_EXCEPTION_DIRECT_WARNING: _t.Final[str] = 'The above exception was the direct cause of the following warning:'
|
19
17
|
|
20
|
-
def _create_error_details(exception: BaseException) -> tuple[Detail, ...]:
|
21
|
-
"""Return an `ErrorMessage` tuple created from the given exception."""
|
22
|
-
target_exception: BaseException | None = exception
|
23
|
-
error_details: list[Detail] = []
|
24
18
|
|
25
|
-
|
26
|
-
|
19
|
+
class EventFactory:
|
20
|
+
"""Factory for creating `Event` instances from `BaseException` instances on targets."""
|
27
21
|
|
28
|
-
|
22
|
+
_MAX_DEPTH = 10
|
23
|
+
"""Maximum exception chain depth. Exceptions beyond this depth will be omitted."""
|
29
24
|
|
30
|
-
|
25
|
+
@classmethod
|
26
|
+
def from_exception(cls, exception: BaseException, include_traceback: bool) -> _messages.Event:
|
27
|
+
return cls(include_traceback)._convert_exception(exception)
|
28
|
+
|
29
|
+
def __init__(self, include_traceback: bool) -> None:
|
30
|
+
self._include_traceback = include_traceback
|
31
|
+
self._depth = 0
|
32
|
+
|
33
|
+
def _convert_exception(self, exception: BaseException) -> _messages.Event:
|
34
|
+
if self._depth > self._MAX_DEPTH:
|
35
|
+
return _messages.Event(
|
36
|
+
msg="Maximum depth exceeded, omitting further events.",
|
37
|
+
)
|
38
|
+
|
39
|
+
self._depth += 1
|
40
|
+
|
41
|
+
try:
|
42
|
+
return _messages.Event(
|
43
|
+
msg=self._get_msg(exception),
|
44
|
+
formatted_traceback=self._get_formatted_traceback(exception),
|
45
|
+
formatted_source_context=self._get_formatted_source_context(exception),
|
46
|
+
help_text=self._get_help_text(exception),
|
47
|
+
chain=self._get_chain(exception),
|
48
|
+
events=self._get_events(exception),
|
49
|
+
)
|
50
|
+
finally:
|
51
|
+
self._depth -= 1
|
52
|
+
|
53
|
+
def _get_msg(self, exception: BaseException) -> str | None:
|
54
|
+
return str(exception).strip()
|
55
|
+
|
56
|
+
def _get_formatted_traceback(self, exception: BaseException) -> str | None:
|
57
|
+
if self._include_traceback:
|
58
|
+
return ''.join(_sys_traceback.format_exception(type(exception), exception, exception.__traceback__, chain=False))
|
59
|
+
|
60
|
+
return None
|
61
|
+
|
62
|
+
def _get_formatted_source_context(self, exception: BaseException) -> str | None:
|
63
|
+
return None
|
64
|
+
|
65
|
+
def _get_help_text(self, exception: BaseException) -> str | None:
|
66
|
+
return None
|
67
|
+
|
68
|
+
def _get_chain(self, exception: BaseException) -> _messages.EventChain | None:
|
69
|
+
if cause := self._get_cause(exception):
|
70
|
+
return _messages.EventChain(
|
71
|
+
msg_reason=MSG_REASON_DIRECT_CAUSE,
|
72
|
+
traceback_reason='The above exception was the direct cause of the following exception:',
|
73
|
+
event=self._convert_exception(cause),
|
74
|
+
follow=self._follow_cause(exception),
|
75
|
+
)
|
76
|
+
|
77
|
+
if context := self._get_context(exception):
|
78
|
+
return _messages.EventChain(
|
79
|
+
msg_reason=MSG_REASON_HANDLING_CAUSE,
|
80
|
+
traceback_reason='During handling of the above exception, another exception occurred:',
|
81
|
+
event=self._convert_exception(context),
|
82
|
+
follow=False,
|
83
|
+
)
|
84
|
+
|
85
|
+
return None
|
86
|
+
|
87
|
+
def _follow_cause(self, exception: BaseException) -> bool:
|
88
|
+
return True
|
89
|
+
|
90
|
+
def _get_cause(self, exception: BaseException) -> BaseException | None:
|
91
|
+
return exception.__cause__
|
92
|
+
|
93
|
+
def _get_context(self, exception: BaseException) -> BaseException | None:
|
94
|
+
if exception.__suppress_context__:
|
95
|
+
return None
|
96
|
+
|
97
|
+
return exception.__context__
|
98
|
+
|
99
|
+
def _get_events(self, exception: BaseException) -> tuple[_messages.Event, ...] | None:
|
100
|
+
# deprecated: description='move BaseExceptionGroup support here from ControllerEventFactory' python_version='3.10'
|
101
|
+
return None
|
@@ -0,0 +1,61 @@
|
|
1
|
+
from __future__ import annotations as _annotations
|
2
|
+
|
3
|
+
import typing as _t
|
4
|
+
|
5
|
+
from ansible.module_utils._internal import _text_utils, _messages
|
6
|
+
|
7
|
+
|
8
|
+
def deduplicate_message_parts(message_parts: list[str]) -> str:
|
9
|
+
"""Format the given list of messages into a brief message, while deduplicating repeated elements."""
|
10
|
+
message_parts = list(reversed(message_parts))
|
11
|
+
|
12
|
+
message = message_parts.pop(0)
|
13
|
+
|
14
|
+
for message_part in message_parts:
|
15
|
+
# avoid duplicate messages where the cause was already concatenated to the exception message
|
16
|
+
if message_part.endswith(message):
|
17
|
+
message = message_part
|
18
|
+
else:
|
19
|
+
message = _text_utils.concat_message(message_part, message)
|
20
|
+
|
21
|
+
return message
|
22
|
+
|
23
|
+
|
24
|
+
def format_event_brief_message(event: _messages.Event) -> str:
|
25
|
+
"""
|
26
|
+
Format an event into a brief message.
|
27
|
+
Help text, contextual information and sub-events will be omitted.
|
28
|
+
"""
|
29
|
+
message_parts: list[str] = []
|
30
|
+
|
31
|
+
while True:
|
32
|
+
message_parts.append(event.msg)
|
33
|
+
|
34
|
+
if not event.chain or not event.chain.follow:
|
35
|
+
break
|
36
|
+
|
37
|
+
event = event.chain.event
|
38
|
+
|
39
|
+
return deduplicate_message_parts(message_parts)
|
40
|
+
|
41
|
+
|
42
|
+
def deprecation_as_dict(deprecation: _messages.DeprecationSummary) -> _t.Dict[str, _t.Any]:
|
43
|
+
"""Returns a dictionary representation of the deprecation object in the format exposed to playbooks."""
|
44
|
+
from ansible.module_utils._internal._deprecator import INDETERMINATE_DEPRECATOR # circular import from messages
|
45
|
+
|
46
|
+
if deprecation.deprecator and deprecation.deprecator != INDETERMINATE_DEPRECATOR:
|
47
|
+
collection_name = '.'.join(deprecation.deprecator.resolved_name.split('.')[:2])
|
48
|
+
else:
|
49
|
+
collection_name = None
|
50
|
+
|
51
|
+
result = dict(
|
52
|
+
msg=format_event_brief_message(deprecation.event),
|
53
|
+
collection_name=collection_name,
|
54
|
+
)
|
55
|
+
|
56
|
+
if deprecation.date:
|
57
|
+
result.update(date=deprecation.date)
|
58
|
+
else:
|
59
|
+
result.update(version=deprecation.version)
|
60
|
+
|
61
|
+
return result
|
@@ -6,7 +6,7 @@ import json
|
|
6
6
|
import typing as t
|
7
7
|
|
8
8
|
from ansible.module_utils import _internal
|
9
|
-
from ansible.module_utils.
|
9
|
+
from ansible.module_utils._internal import _messages
|
10
10
|
from ansible.module_utils._internal._datatag import (
|
11
11
|
AnsibleSerializable,
|
12
12
|
AnsibleSerializableWrapper,
|
@@ -87,7 +87,8 @@ 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.
|
90
|
+
_messages.Event,
|
91
|
+
_messages.EventChain,
|
91
92
|
_messages.ErrorSummary,
|
92
93
|
_messages.WarningSummary,
|
93
94
|
_messages.DeprecationSummary,
|
@@ -203,11 +204,27 @@ class _JSONSerializationProfile(t.Generic[_T_encoder, _T_decoder]):
|
|
203
204
|
|
204
205
|
@classmethod
|
205
206
|
def handle_key(cls, k: t.Any) -> t.Any:
|
207
|
+
"""Validation/conversion hook before a dict key is serialized. The default implementation only accepts str-typed keys."""
|
208
|
+
# NOTE: Since JSON requires string keys, there is no support for preserving tags on dictionary keys during serialization.
|
209
|
+
|
206
210
|
if not isinstance(k, str): # DTFIX-FUTURE: optimize this to use all known str-derived types in type map / allowed types
|
207
211
|
raise TypeError(f'Key of type {type(k).__name__!r} is not JSON serializable by the {cls.profile_name!r} profile.')
|
208
212
|
|
209
213
|
return k
|
210
214
|
|
215
|
+
@classmethod
|
216
|
+
def _handle_key_str_fallback(cls, k: t.Any) -> t.Any:
|
217
|
+
"""Legacy implementations should use this key handler for backward compatibility with stdlib JSON key conversion quirks."""
|
218
|
+
# DTFIX-FUTURE: optimized exact-type table lookup first
|
219
|
+
|
220
|
+
if isinstance(k, str):
|
221
|
+
return k
|
222
|
+
|
223
|
+
if k is None or isinstance(k, (int, float)):
|
224
|
+
return json.dumps(k)
|
225
|
+
|
226
|
+
raise TypeError(f'Key of type {type(k).__name__!r} is not JSON serializable by the {cls.profile_name!r} profile.')
|
227
|
+
|
211
228
|
@classmethod
|
212
229
|
def default(cls, o: t.Any) -> t.Any:
|
213
230
|
# Preserve the built-in JSON encoder support for subclasses of scalar types.
|
@@ -373,8 +390,8 @@ Future code changes should further restrict bytes to string conversions to elimi
|
|
373
390
|
Additional warnings at other boundaries may be needed to give users an opportunity to resolve the issues before they become errors.
|
374
391
|
"""
|
375
392
|
# DTFIX-FUTURE: add strict UTF8 string encoding checking to serialization profiles (to match the checks performed during deserialization)
|
376
|
-
#
|
377
|
-
# DTFIX-
|
393
|
+
# DTFIX3: the surrogateescape note above isn't quite right, for encoding use surrogatepass, which does work
|
394
|
+
# DTFIX-FUTURE: this config setting should probably be deprecated
|
378
395
|
|
379
396
|
|
380
397
|
def _create_encoding_check_error() -> Exception:
|
@@ -22,6 +22,8 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
22
22
|
}
|
23
23
|
)
|
24
24
|
|
25
|
+
cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
|
26
|
+
|
25
27
|
|
26
28
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
27
29
|
_profile = _Profile
|
@@ -26,6 +26,8 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
26
26
|
_datetime.datetime: cls.serialize_as_isoformat, # legacy _json_encode_fallback behavior *and* legacy parameters.py does this before serialization
|
27
27
|
}
|
28
28
|
|
29
|
+
cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
|
30
|
+
|
29
31
|
|
30
32
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
31
33
|
_profile = _Profile
|
@@ -17,7 +17,7 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
17
17
|
@classmethod
|
18
18
|
def post_init(cls) -> None:
|
19
19
|
cls.serialize_map = {
|
20
|
-
#
|
20
|
+
# DTFIX5: support serialization of every type that is supported in the Ansible variable type system
|
21
21
|
set: cls.serialize_as_list,
|
22
22
|
tuple: cls.serialize_as_list,
|
23
23
|
_datetime.date: cls.serialize_as_isoformat,
|
@@ -41,6 +41,8 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
41
41
|
'__ansible_vault': _functools.partial(cls.unsupported_target_type_error, '__ansible_vault'),
|
42
42
|
}
|
43
43
|
|
44
|
+
cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
|
45
|
+
|
44
46
|
|
45
47
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
46
48
|
_profile = _Profile
|