ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b6__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 +1 -1
- ansible/_internal/_ansiballz/__init__.py +0 -0
- ansible/_internal/_ansiballz/_builder.py +101 -0
- ansible/_internal/{_ansiballz.py → _ansiballz/_wrapper.py} +11 -11
- 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 +5 -5
- 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 +28 -20
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +36 -5
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_template_vars.py +72 -0
- ansible/_internal/_templating/_transform.py +26 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_yaml/_constructor.py +4 -4
- ansible/_internal/_yaml/_dumper.py +26 -18
- 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 +11 -93
- ansible/cli/arguments/option_helpers.py +3 -4
- ansible/cli/console.py +1 -1
- ansible/cli/doc.py +86 -30
- ansible/cli/inventory.py +5 -7
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +46 -0
- ansible/errors/__init__.py +98 -50
- ansible/executor/module_common.py +75 -49
- 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 +40 -115
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/api.py +1 -4
- ansible/galaxy/collection/__init__.py +2 -10
- ansible/galaxy/collection/concrete_artifact_manager.py +2 -8
- ansible/galaxy/role.py +2 -2
- ansible/inventory/manager.py +1 -1
- ansible/module_utils/_internal/__init__.py +7 -7
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz/__init__.py +0 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/__init__.py +0 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_coverage.py +45 -0
- ansible/module_utils/_internal/_ansiballz/_extensions/_pydevd.py +62 -0
- ansible/module_utils/_internal/{_ansiballz.py → _ansiballz/_loader.py} +13 -39
- ansible/module_utils/_internal/_ansiballz/_respawn.py +32 -0
- ansible/module_utils/_internal/_ansiballz/_respawn_wrapper.py +23 -0
- ansible/module_utils/_internal/_datatag/__init__.py +43 -15
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +67 -55
- 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 +22 -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} +54 -49
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +15 -2
- 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 +95 -71
- 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/respawn.py +4 -41
- 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/connection.py +8 -11
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/hardware/linux.py +1 -1
- ansible/module_utils/facts/sysctl.py +4 -6
- ansible/module_utils/facts/system/caps.py +2 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/system/local.py +1 -1
- ansible/module_utils/facts/virtual/linux.py +2 -2
- ansible/module_utils/service.py +3 -10
- ansible/module_utils/urls.py +4 -4
- ansible/modules/apt_repository.py +17 -39
- ansible/modules/assemble.py +2 -2
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +12 -22
- ansible/modules/command.py +3 -3
- ansible/modules/copy.py +4 -4
- ansible/modules/cron.py +1 -1
- ansible/modules/dnf5.py +14 -22
- ansible/modules/file.py +16 -17
- ansible/modules/find.py +3 -3
- ansible/modules/get_url.py +17 -0
- ansible/modules/git.py +9 -7
- ansible/modules/hostname.py +0 -1
- ansible/modules/known_hosts.py +12 -14
- ansible/modules/package.py +6 -0
- ansible/modules/replace.py +2 -2
- ansible/modules/service.py +3 -9
- ansible/modules/slurp.py +10 -13
- ansible/modules/stat.py +5 -7
- ansible/modules/unarchive.py +6 -6
- ansible/modules/user.py +1 -1
- ansible/modules/wait_for.py +28 -30
- ansible/modules/yum_repository.py +4 -3
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +6 -6
- 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 +10 -14
- ansible/playbook/base.py +7 -2
- ansible/playbook/included_file.py +3 -1
- ansible/playbook/play_context.py +2 -0
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +19 -8
- ansible/playbook/task.py +2 -0
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +8 -31
- 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/fetch.py +3 -3
- 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 +11 -10
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/cache/__init__.py +17 -19
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/callback/tree.py +5 -5
- ansible/plugins/connection/local.py +6 -6
- ansible/plugins/connection/paramiko_ssh.py +5 -5
- ansible/plugins/connection/ssh.py +25 -15
- ansible/plugins/connection/winrm.py +6 -3
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +32 -27
- ansible/plugins/filter/encryption.py +14 -6
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +73 -19
- ansible/plugins/loader.py +7 -7
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/lookup/template.py +9 -4
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +67 -9
- ansible/plugins/shell/sh.py +10 -5
- ansible/plugins/strategy/__init__.py +3 -3
- ansible/plugins/test/core.py +22 -16
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +38 -54
- ansible/utils/collection_loader/_collection_finder.py +3 -3
- ansible/utils/display.py +124 -138
- ansible/utils/galaxy.py +2 -2
- ansible/utils/hashing.py +6 -8
- ansible/utils/listify.py +6 -4
- ansible/utils/path.py +5 -7
- ansible/utils/py3compat.py +2 -1
- ansible/utils/ssh_functions.py +3 -2
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- ansible/vars/plugins.py +3 -3
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/RECORD +224 -204
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.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/commands/integration/coverage.py +7 -2
- ansible_test/_internal/host_profiles.py +62 -10
- ansible_test/_internal/provisioning.py +10 -4
- ansible_test/_internal/ssh.py +1 -5
- ansible_test/_internal/thread.py +2 -1
- ansible_test/_internal/timeout.py +1 -1
- ansible_test/_internal/util.py +40 -12
- 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 +61 -7
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible_test/_util/target/setup/requirements.py +3 -9
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b6.dist-info}/top_level.txt +0 -0
@@ -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
|
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
|
17
|
-
from
|
18
|
-
from
|
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
|
-
|
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,
|
@@ -97,7 +69,9 @@ def _handle_exception(exception: BaseException, profile: str) -> t.NoReturn:
|
|
97
69
|
"""Handle the given exception."""
|
98
70
|
result = dict(
|
99
71
|
failed=True,
|
100
|
-
exception=
|
72
|
+
exception=_messages.ErrorSummary(
|
73
|
+
event=_errors.EventFactory.from_exception(exception, _traceback.is_traceback_enabled(_traceback.TracebackEvent.ERROR)),
|
74
|
+
),
|
101
75
|
)
|
102
76
|
|
103
77
|
encoder = get_module_encoder(profile, Direction.MODULE_TO_CONTROLLER)
|
@@ -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
|
|
@@ -37,15 +38,16 @@ _ANSIBLE_TAGGED_OBJECT_SLOTS = tuple(('_ansible_tags_mapping',))
|
|
37
38
|
# shared empty frozenset for default values
|
38
39
|
_empty_frozenset: t.FrozenSet = frozenset()
|
39
40
|
|
41
|
+
# Technical Notes
|
42
|
+
#
|
43
|
+
# 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.
|
44
|
+
# 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,
|
45
|
+
# updating a key usually requires removal and re-addition.
|
46
|
+
|
40
47
|
|
41
48
|
class AnsibleTagHelper:
|
42
49
|
"""Utility methods for working with Ansible data tags."""
|
43
50
|
|
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
51
|
@staticmethod
|
50
52
|
def untag(value: _T, *tag_types: t.Type[AnsibleDatatagBase]) -> _T:
|
51
53
|
"""
|
@@ -105,7 +107,7 @@ class AnsibleTagHelper:
|
|
105
107
|
if issubclass(the_type, AnsibleTaggedObject):
|
106
108
|
the_type = type_or_value._native_type
|
107
109
|
|
108
|
-
# DTFIX-
|
110
|
+
# DTFIX-FUTURE: provide a knob to optionally report the real type for debugging purposes
|
109
111
|
return the_type
|
110
112
|
|
111
113
|
@staticmethod
|
@@ -215,7 +217,7 @@ class AnsibleTagHelper:
|
|
215
217
|
return value
|
216
218
|
|
217
219
|
|
218
|
-
class AnsibleSerializable
|
220
|
+
class AnsibleSerializable:
|
219
221
|
__slots__ = _NO_INSTANCE_STORAGE
|
220
222
|
|
221
223
|
_known_type_map: t.ClassVar[t.Dict[str, t.Type['AnsibleSerializable']]] = {}
|
@@ -273,6 +275,27 @@ class AnsibleSerializable(metaclass=abc.ABCMeta):
|
|
273
275
|
return f'{name}({arg_string})'
|
274
276
|
|
275
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
|
+
|
276
299
|
class AnsibleSerializableWrapper(AnsibleSerializable, t.Generic[_T], metaclass=abc.ABCMeta):
|
277
300
|
__slots__ = ('_value',)
|
278
301
|
|
@@ -339,10 +362,11 @@ class AnsibleSerializableDateTime(AnsibleSerializableWrapper[datetime.datetime])
|
|
339
362
|
@dataclasses.dataclass(**_tag_dataclass_kwargs)
|
340
363
|
class AnsibleSerializableDataclass(AnsibleSerializable, metaclass=abc.ABCMeta):
|
341
364
|
_validation_allow_subclasses = True
|
365
|
+
_validation_auto_enabled = True
|
342
366
|
|
343
367
|
def _as_dict(self) -> t.Dict[str, t.Any]:
|
344
368
|
# omit None values when None is the field default
|
345
|
-
# DTFIX-
|
369
|
+
# DTFIX-FUTURE: this implementation means we can never change the default on fields which have None for their default
|
346
370
|
# other defaults can be changed -- but there's no way to override this behavior either way for other default types
|
347
371
|
# it's a trip hazard to have the default logic here, rather than per field (or not at all)
|
348
372
|
# consider either removing the filtering or requiring it to be explicitly set per field using dataclass metadata
|
@@ -351,7 +375,7 @@ class AnsibleSerializableDataclass(AnsibleSerializable, metaclass=abc.ABCMeta):
|
|
351
375
|
|
352
376
|
@classmethod
|
353
377
|
def _from_dict(cls, d: t.Dict[str, t.Any]) -> t.Self:
|
354
|
-
# DTFIX-
|
378
|
+
# DTFIX-FUTURE: optimize this to avoid the dataclasses fields metadata and get_origin stuff at runtime
|
355
379
|
type_hints = t.get_type_hints(cls)
|
356
380
|
mutated_dict: dict[str, t.Any] | None = None
|
357
381
|
|
@@ -368,7 +392,11 @@ class AnsibleSerializableDataclass(AnsibleSerializable, metaclass=abc.ABCMeta):
|
|
368
392
|
def __init_subclass__(cls, **kwargs) -> None:
|
369
393
|
super(AnsibleSerializableDataclass, cls).__init_subclass__(**kwargs) # cannot use super() without arguments when using slots
|
370
394
|
|
371
|
-
|
395
|
+
if cls._validation_auto_enabled:
|
396
|
+
try:
|
397
|
+
_dataclass_validation.inject_post_init_validation(cls, cls._validation_allow_subclasses) # code gen a real __post_init__ method
|
398
|
+
except Exception as ex:
|
399
|
+
raise Exception(f'Validation code generation failed on {cls}.') from ex
|
372
400
|
|
373
401
|
|
374
402
|
class Tripwire:
|
@@ -524,7 +552,6 @@ class CollectionWithMro(c.Collection, t.Protocol):
|
|
524
552
|
__mro__: tuple[type, ...]
|
525
553
|
|
526
554
|
|
527
|
-
# DTFIX-RELEASE: This should probably reside elsewhere.
|
528
555
|
def is_non_scalar_collection_type(value: type) -> t.TypeGuard[type[CollectionWithMro]]:
|
529
556
|
"""Returns True if the value is a non-scalar collection type, otherwise returns False."""
|
530
557
|
return issubclass(value, c.Collection) and not issubclass(value, str) and not issubclass(value, bytes)
|
@@ -878,7 +905,6 @@ class _AnsibleTaggedList(list, AnsibleTaggedObject):
|
|
878
905
|
# Propagation of tags in these cases is left to the caller, based on needs specific to their use case.
|
879
906
|
|
880
907
|
|
881
|
-
# DTFIX-RELEASE: do we want frozenset too?
|
882
908
|
class _AnsibleTaggedSet(set, AnsibleTaggedObject):
|
883
909
|
__slots__ = _ANSIBLE_TAGGED_OBJECT_SLOTS
|
884
910
|
|
@@ -914,10 +940,12 @@ class _AnsibleTaggedTuple(tuple, AnsibleTaggedObject):
|
|
914
940
|
return super()._copy_collection()
|
915
941
|
|
916
942
|
|
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
943
|
_untaggable_types = {type(None), bool}
|
944
|
+
"""
|
945
|
+
Attempts to apply tags to values of these types will be silently ignored.
|
946
|
+
While we could proxy or subclass builtin singletons, they're idiomatically compared with "is" reference equality, which we can't customize.
|
947
|
+
This set gets augmented with additional types when some controller-only types are imported.
|
948
|
+
"""
|
921
949
|
|
922
950
|
# noinspection PyProtectedMember
|
923
951
|
_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, _plugin_info
|
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=None,
|
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
|
|
@@ -81,7 +54,7 @@ def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
|
|
81
54
|
|
82
55
|
if match := re.match(r'plugins/(?P<plugin_type>\w+)/(?P<plugin_name>\w+)', relpath):
|
83
56
|
plugin_name = match.group("plugin_name")
|
84
|
-
plugin_type = match.group("plugin_type")
|
57
|
+
plugin_type = _plugin_info.normalize_plugin_type(match.group("plugin_type"))
|
85
58
|
|
86
59
|
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
87
60
|
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
@@ -92,36 +65,33 @@ def _path_as_core_plugininfo(path: str) -> PluginInfo | None:
|
|
92
65
|
elif match := re.match(r'modules/(?P<module_name>\w+)', relpath):
|
93
66
|
# AnsiballZ Python package for core modules
|
94
67
|
plugin_name = match.group("module_name")
|
95
|
-
plugin_type =
|
68
|
+
plugin_type = _messages.PluginType.MODULE
|
96
69
|
elif match := re.match(r'legacy/(?P<module_name>\w+)', relpath):
|
97
70
|
# AnsiballZ Python package for non-core library/role modules
|
98
71
|
namespace = 'ansible.legacy'
|
99
72
|
|
100
73
|
plugin_name = match.group("module_name")
|
101
|
-
plugin_type =
|
74
|
+
plugin_type = _messages.PluginType.MODULE
|
102
75
|
else:
|
103
76
|
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
|
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
|
114
87
|
|
115
|
-
plugin_type = match.group('plugin_type')
|
88
|
+
plugin_type = _plugin_info.normalize_plugin_type(match.group('plugin_type'))
|
116
89
|
|
117
90
|
if plugin_type in _AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES:
|
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
|
122
|
-
|
123
|
-
if plugin_type == 'modules':
|
124
|
-
plugin_type = 'module'
|
94
|
+
return deprecator_from_collection_name('.'.join((match.group('ns'), match.group('coll'))))
|
125
95
|
|
126
96
|
if plugin_type not in _DEPRECATOR_PLUGIN_TYPES:
|
127
97
|
# The plugin type isn't a known deprecator type, so we have to assume the caller is intermediate code.
|
@@ -131,4 +101,46 @@ def _path_as_collection_plugininfo(path: str) -> PluginInfo | None:
|
|
131
101
|
|
132
102
|
name = '.'.join((match.group('ns'), match.group('coll'), match.group('plugin_name')))
|
133
103
|
|
134
|
-
return PluginInfo(resolved_name=name, type=plugin_type)
|
104
|
+
return _messages.PluginInfo(resolved_name=name, type=plugin_type)
|
105
|
+
|
106
|
+
|
107
|
+
_ANSIBLE_MODULE_BASE_PATH: t.Final = pathlib.Path(sys.modules['ansible'].__file__).parent
|
108
|
+
"""Runtime-detected base path of the `ansible` Python package to distinguish between Ansible-owned and external code."""
|
109
|
+
|
110
|
+
ANSIBLE_CORE_DEPRECATOR: t.Final = deprecator_from_collection_name('ansible.builtin')
|
111
|
+
"""Singleton `PluginInfo` instance for ansible-core callers where the plugin can/should not be identified in messages."""
|
112
|
+
|
113
|
+
INDETERMINATE_DEPRECATOR: t.Final = _messages.PluginInfo(resolved_name=None, type=None)
|
114
|
+
"""Singleton `PluginInfo` instance for indeterminate deprecator."""
|
115
|
+
|
116
|
+
_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
|
117
|
+
{
|
118
|
+
_messages.PluginType.ACTION,
|
119
|
+
_messages.PluginType.BECOME,
|
120
|
+
_messages.PluginType.CACHE,
|
121
|
+
_messages.PluginType.CALLBACK,
|
122
|
+
_messages.PluginType.CLICONF,
|
123
|
+
_messages.PluginType.CONNECTION,
|
124
|
+
# DOC_FRAGMENTS - no code execution
|
125
|
+
# FILTER - basename inadequate to identify plugin
|
126
|
+
_messages.PluginType.HTTPAPI,
|
127
|
+
_messages.PluginType.INVENTORY,
|
128
|
+
_messages.PluginType.LOOKUP,
|
129
|
+
_messages.PluginType.MODULE, # only for collections
|
130
|
+
_messages.PluginType.NETCONF,
|
131
|
+
_messages.PluginType.SHELL,
|
132
|
+
_messages.PluginType.STRATEGY,
|
133
|
+
_messages.PluginType.TERMINAL,
|
134
|
+
# TEST - basename inadequate to identify plugin
|
135
|
+
_messages.PluginType.VARS,
|
136
|
+
}
|
137
|
+
)
|
138
|
+
"""Plugin types which are valid for identifying a deprecator for deprecation purposes."""
|
139
|
+
|
140
|
+
_AMBIGUOUS_DEPRECATOR_PLUGIN_TYPES: t.Final = frozenset(
|
141
|
+
{
|
142
|
+
_messages.PluginType.FILTER,
|
143
|
+
_messages.PluginType.TEST,
|
144
|
+
}
|
145
|
+
)
|
146
|
+
"""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
|