ansible-core 2.15.0__py3-none-any.whl → 2.15.2__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.
Potentially problematic release.
This version of ansible-core might be problematic. Click here for more details.
- ansible/config/manager.py +1 -1
- ansible/galaxy/collection/__init__.py +17 -30
- ansible/galaxy/collection/concrete_artifact_manager.py +12 -6
- ansible/galaxy/dependency_resolution/dataclasses.py +6 -3
- ansible/module_utils/ansible_release.py +1 -1
- ansible/modules/apt_key.py +8 -5
- ansible/modules/apt_repository.py +2 -0
- ansible/modules/deb822_repository.py +1 -1
- ansible/modules/dnf5.py +8 -8
- ansible/modules/find.py +3 -0
- ansible/modules/uri.py +9 -1
- ansible/modules/validate_argument_spec.py +1 -1
- ansible/plugins/action/template.py +26 -15
- ansible/plugins/connection/paramiko_ssh.py +8 -0
- ansible/plugins/connection/psrp.py +3 -3
- ansible/plugins/connection/ssh.py +19 -2
- ansible/plugins/filter/comment.yml +1 -1
- ansible/plugins/filter/split.yml +1 -1
- ansible/plugins/filter/to_yaml.yml +1 -1
- ansible/plugins/lookup/template.py +11 -6
- ansible/plugins/strategy/__init__.py +20 -12
- ansible/plugins/test/change.yml +1 -1
- ansible/plugins/test/changed.yml +1 -1
- ansible/plugins/test/reachable.yml +1 -1
- ansible/plugins/test/succeeded.yml +1 -1
- ansible/plugins/test/success.yml +1 -1
- ansible/plugins/test/successful.yml +1 -1
- ansible/plugins/test/unreachable.yml +1 -1
- ansible/release.py +1 -1
- ansible/template/__init__.py +42 -28
- ansible/utils/_junit_xml.py +5 -1
- {ansible_core-2.15.0.dist-info → ansible_core-2.15.2.dist-info}/METADATA +1 -1
- {ansible_core-2.15.0.dist-info → ansible_core-2.15.2.dist-info}/RECORD +61 -61
- ansible_test/_data/completion/remote.txt +2 -1
- ansible_test/_internal/__init__.py +11 -0
- ansible_test/_internal/cli/commands/env.py +1 -1
- ansible_test/_internal/commands/env/__init__.py +14 -17
- ansible_test/_internal/commands/integration/__init__.py +1 -1
- ansible_test/_internal/commands/integration/cloud/__init__.py +3 -3
- ansible_test/_internal/commands/sanity/pylint.py +4 -4
- ansible_test/_internal/commands/sanity/validate_modules.py +2 -2
- ansible_test/_internal/containers.py +2 -2
- ansible_test/_internal/coverage_util.py +2 -2
- ansible_test/_internal/data.py +2 -7
- ansible_test/_internal/git.py +1 -1
- ansible_test/_internal/junit_xml.py +5 -1
- ansible_test/_internal/payload.py +2 -2
- ansible_test/_internal/provider/layout/__init__.py +1 -1
- ansible_test/_internal/provider/layout/ansible.py +15 -0
- ansible_test/_internal/provider/layout/collection.py +9 -1
- ansible_test/_internal/provisioning.py +5 -2
- ansible_test/_internal/pypi_proxy.py +4 -4
- ansible_test/_internal/test.py +3 -5
- ansible_test/_internal/timeout.py +56 -19
- ansible_test/_internal/util.py +4 -0
- ansible_test/_internal/util_common.py +38 -6
- {ansible_core-2.15.0.data → ansible_core-2.15.2.data}/scripts/ansible-test +0 -0
- {ansible_core-2.15.0.dist-info → ansible_core-2.15.2.dist-info}/COPYING +0 -0
- {ansible_core-2.15.0.dist-info → ansible_core-2.15.2.dist-info}/WHEEL +0 -0
- {ansible_core-2.15.0.dist-info → ansible_core-2.15.2.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.15.0.dist-info → ansible_core-2.15.2.dist-info}/top_level.txt +0 -0
ansible_test/_internal/data.py
CHANGED
|
@@ -10,7 +10,6 @@ from .util import (
|
|
|
10
10
|
ApplicationError,
|
|
11
11
|
import_plugins,
|
|
12
12
|
is_subdir,
|
|
13
|
-
is_valid_identifier,
|
|
14
13
|
ANSIBLE_LIB_ROOT,
|
|
15
14
|
ANSIBLE_TEST_ROOT,
|
|
16
15
|
ANSIBLE_SOURCE_ROOT,
|
|
@@ -219,12 +218,8 @@ class DataContext:
|
|
|
219
218
|
elif 'ansible_collections' not in cwd.split(os.path.sep):
|
|
220
219
|
blocks.append('No "ansible_collections" parent directory was found.')
|
|
221
220
|
|
|
222
|
-
if self.content.
|
|
223
|
-
|
|
224
|
-
blocks.append(f'The namespace "{self.content.collection.namespace}" is an invalid identifier or a reserved keyword.')
|
|
225
|
-
|
|
226
|
-
if not is_valid_identifier(self.content.collection.name):
|
|
227
|
-
blocks.append(f'The name "{self.content.collection.name}" is an invalid identifier or a reserved keyword.')
|
|
221
|
+
if isinstance(self.content.unsupported, list):
|
|
222
|
+
blocks.extend(self.content.unsupported)
|
|
228
223
|
|
|
229
224
|
message = '\n'.join(blocks)
|
|
230
225
|
|
ansible_test/_internal/git.py
CHANGED
|
@@ -77,7 +77,7 @@ class Git:
|
|
|
77
77
|
|
|
78
78
|
def get_branch_fork_point(self, branch: str) -> str:
|
|
79
79
|
"""Return a reference to the point at which the given branch was forked."""
|
|
80
|
-
cmd = ['merge-base', '
|
|
80
|
+
cmd = ['merge-base', branch, 'HEAD']
|
|
81
81
|
return self.run_git(cmd).strip()
|
|
82
82
|
|
|
83
83
|
def is_valid_ref(self, ref: str) -> bool:
|
|
@@ -144,6 +144,10 @@ class TestSuite:
|
|
|
144
144
|
system_out: str | None = None
|
|
145
145
|
system_err: str | None = None
|
|
146
146
|
|
|
147
|
+
def __post_init__(self):
|
|
148
|
+
if self.timestamp and self.timestamp.tzinfo != datetime.timezone.utc:
|
|
149
|
+
raise ValueError(f'timestamp.tzinfo must be {datetime.timezone.utc!r}')
|
|
150
|
+
|
|
147
151
|
@property
|
|
148
152
|
def disabled(self) -> int:
|
|
149
153
|
"""The number of disabled test cases."""
|
|
@@ -187,7 +191,7 @@ class TestSuite:
|
|
|
187
191
|
skipped=self.skipped,
|
|
188
192
|
tests=self.tests,
|
|
189
193
|
time=self.time,
|
|
190
|
-
timestamp=self.timestamp.isoformat(timespec='seconds') if self.timestamp else None,
|
|
194
|
+
timestamp=self.timestamp.replace(tzinfo=None).isoformat(timespec='seconds') if self.timestamp else None,
|
|
191
195
|
)
|
|
192
196
|
|
|
193
197
|
def get_xml_element(self) -> ET.Element:
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Payload management for sending Ansible files and test content to other systems (VMs, containers)."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import atexit
|
|
5
4
|
import os
|
|
6
5
|
import stat
|
|
7
6
|
import tarfile
|
|
@@ -32,6 +31,7 @@ from .data import (
|
|
|
32
31
|
|
|
33
32
|
from .util_common import (
|
|
34
33
|
CommonConfig,
|
|
34
|
+
ExitHandler,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
# improve performance by disabling uid/gid lookups
|
|
@@ -192,7 +192,7 @@ def create_temporary_bin_files(args: CommonConfig) -> tuple[tuple[str, str], ...
|
|
|
192
192
|
temp_path = '/tmp/ansible-tmp-bin'
|
|
193
193
|
else:
|
|
194
194
|
temp_path = tempfile.mkdtemp(prefix='ansible', suffix='bin')
|
|
195
|
-
|
|
195
|
+
ExitHandler.register(remove_tree, temp_path)
|
|
196
196
|
|
|
197
197
|
for name, dest in ANSIBLE_BIN_SYMLINK_MAP.items():
|
|
198
198
|
path = os.path.join(temp_path, name)
|
|
@@ -8,6 +8,11 @@ from . import (
|
|
|
8
8
|
LayoutProvider,
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
+
from ...util import (
|
|
12
|
+
ANSIBLE_SOURCE_ROOT,
|
|
13
|
+
ANSIBLE_TEST_ROOT,
|
|
14
|
+
)
|
|
15
|
+
|
|
11
16
|
|
|
12
17
|
class AnsibleLayout(LayoutProvider):
|
|
13
18
|
"""Layout provider for Ansible source."""
|
|
@@ -26,6 +31,15 @@ class AnsibleLayout(LayoutProvider):
|
|
|
26
31
|
module_utils='lib/ansible/module_utils',
|
|
27
32
|
)
|
|
28
33
|
|
|
34
|
+
errors: list[str] = []
|
|
35
|
+
|
|
36
|
+
if root != ANSIBLE_SOURCE_ROOT:
|
|
37
|
+
errors.extend((
|
|
38
|
+
f'Cannot test "{root}" with ansible-test from "{ANSIBLE_TEST_ROOT}".',
|
|
39
|
+
'',
|
|
40
|
+
f'Did you intend to run "{root}/bin/ansible-test" instead?',
|
|
41
|
+
))
|
|
42
|
+
|
|
29
43
|
return ContentLayout(
|
|
30
44
|
root,
|
|
31
45
|
paths,
|
|
@@ -43,4 +57,5 @@ class AnsibleLayout(LayoutProvider):
|
|
|
43
57
|
unit_module_path='test/units/modules',
|
|
44
58
|
unit_module_utils_path='test/units/module_utils',
|
|
45
59
|
unit_messages=None,
|
|
60
|
+
unsupported=errors,
|
|
46
61
|
)
|
|
@@ -53,6 +53,14 @@ class CollectionLayout(LayoutProvider):
|
|
|
53
53
|
integration_targets_path = self.__check_integration_path(paths, integration_messages)
|
|
54
54
|
self.__check_unit_path(paths, unit_messages)
|
|
55
55
|
|
|
56
|
+
errors: list[str] = []
|
|
57
|
+
|
|
58
|
+
if not is_valid_identifier(collection_namespace):
|
|
59
|
+
errors.append(f'The namespace "{collection_namespace}" is an invalid identifier or a reserved keyword.')
|
|
60
|
+
|
|
61
|
+
if not is_valid_identifier(collection_name):
|
|
62
|
+
errors.append(f'The name "{collection_name}" is an invalid identifier or a reserved keyword.')
|
|
63
|
+
|
|
56
64
|
return ContentLayout(
|
|
57
65
|
root,
|
|
58
66
|
paths,
|
|
@@ -74,7 +82,7 @@ class CollectionLayout(LayoutProvider):
|
|
|
74
82
|
unit_module_path='tests/unit/plugins/modules',
|
|
75
83
|
unit_module_utils_path='tests/unit/plugins/module_utils',
|
|
76
84
|
unit_messages=unit_messages,
|
|
77
|
-
unsupported=
|
|
85
|
+
unsupported=errors,
|
|
78
86
|
)
|
|
79
87
|
|
|
80
88
|
@staticmethod
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Provision hosts for running tests."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import atexit
|
|
5
4
|
import collections.abc as c
|
|
6
5
|
import dataclasses
|
|
7
6
|
import functools
|
|
@@ -27,6 +26,10 @@ from .util import (
|
|
|
27
26
|
type_guard,
|
|
28
27
|
)
|
|
29
28
|
|
|
29
|
+
from .util_common import (
|
|
30
|
+
ExitHandler,
|
|
31
|
+
)
|
|
32
|
+
|
|
30
33
|
from .thread import (
|
|
31
34
|
WrappedThread,
|
|
32
35
|
)
|
|
@@ -124,7 +127,7 @@ def prepare_profiles(
|
|
|
124
127
|
|
|
125
128
|
raise PrimeContainers()
|
|
126
129
|
|
|
127
|
-
|
|
130
|
+
ExitHandler.register(functools.partial(cleanup_profiles, host_state))
|
|
128
131
|
|
|
129
132
|
def provision(profile: HostProfile) -> None:
|
|
130
133
|
"""Provision the given profile."""
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""PyPI proxy management."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import atexit
|
|
5
4
|
import os
|
|
6
5
|
import urllib.parse
|
|
7
6
|
|
|
@@ -23,6 +22,7 @@ from .util import (
|
|
|
23
22
|
)
|
|
24
23
|
|
|
25
24
|
from .util_common import (
|
|
25
|
+
ExitHandler,
|
|
26
26
|
process_scoped_temporary_file,
|
|
27
27
|
)
|
|
28
28
|
|
|
@@ -128,7 +128,7 @@ def configure_target_pypi_proxy(args: EnvironmentConfig, profile: HostProfile, p
|
|
|
128
128
|
run_playbook(args, inventory_path, 'pypi_proxy_prepare.yml', capture=True, variables=dict(
|
|
129
129
|
pypi_endpoint=pypi_endpoint, pypi_hostname=pypi_hostname, force=force))
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
ExitHandler.register(cleanup_pypi_proxy)
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
def configure_pypi_proxy_pip(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str, pypi_hostname: str) -> None:
|
|
@@ -153,7 +153,7 @@ trusted-host = {1}
|
|
|
153
153
|
|
|
154
154
|
if not args.explain:
|
|
155
155
|
write_text_file(pip_conf_path, pip_conf, True)
|
|
156
|
-
|
|
156
|
+
ExitHandler.register(pip_conf_cleanup)
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def configure_pypi_proxy_easy_install(args: EnvironmentConfig, profile: HostProfile, pypi_endpoint: str) -> None:
|
|
@@ -177,4 +177,4 @@ index_url = {0}
|
|
|
177
177
|
|
|
178
178
|
if not args.explain:
|
|
179
179
|
write_text_file(pydistutils_cfg_path, pydistutils_cfg, True)
|
|
180
|
-
|
|
180
|
+
ExitHandler.register(pydistutils_cfg_cleanup)
|
ansible_test/_internal/test.py
CHANGED
|
@@ -114,7 +114,7 @@ class TestResult:
|
|
|
114
114
|
junit_xml.TestSuite(
|
|
115
115
|
name='ansible-test',
|
|
116
116
|
cases=[test_case],
|
|
117
|
-
timestamp=datetime.datetime.
|
|
117
|
+
timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
|
|
118
118
|
),
|
|
119
119
|
],
|
|
120
120
|
)
|
|
@@ -130,7 +130,7 @@ class TestResult:
|
|
|
130
130
|
class TestTimeout(TestResult):
|
|
131
131
|
"""Test timeout."""
|
|
132
132
|
|
|
133
|
-
def __init__(self, timeout_duration: int) -> None:
|
|
133
|
+
def __init__(self, timeout_duration: int | float) -> None:
|
|
134
134
|
super().__init__(command='timeout', test='')
|
|
135
135
|
|
|
136
136
|
self.timeout_duration = timeout_duration
|
|
@@ -153,13 +153,11 @@ One or more of the following situations may be responsible:
|
|
|
153
153
|
|
|
154
154
|
output += '\n\nConsult the console log for additional details on where the timeout occurred.'
|
|
155
155
|
|
|
156
|
-
timestamp = datetime.datetime.utcnow()
|
|
157
|
-
|
|
158
156
|
suites = junit_xml.TestSuites(
|
|
159
157
|
suites=[
|
|
160
158
|
junit_xml.TestSuite(
|
|
161
159
|
name='ansible-test',
|
|
162
|
-
timestamp=
|
|
160
|
+
timestamp=datetime.datetime.now(tz=datetime.timezone.utc),
|
|
163
161
|
cases=[
|
|
164
162
|
junit_xml.TestCase(
|
|
165
163
|
name='timeout',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Timeout management for tests."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import dataclasses
|
|
4
5
|
import datetime
|
|
5
6
|
import functools
|
|
6
7
|
import os
|
|
@@ -19,7 +20,7 @@ from .config import (
|
|
|
19
20
|
|
|
20
21
|
from .util import (
|
|
21
22
|
display,
|
|
22
|
-
|
|
23
|
+
TimeoutExpiredError,
|
|
23
24
|
)
|
|
24
25
|
|
|
25
26
|
from .thread import (
|
|
@@ -35,15 +36,56 @@ from .test import (
|
|
|
35
36
|
)
|
|
36
37
|
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
@dataclasses.dataclass(frozen=True)
|
|
40
|
+
class TimeoutDetail:
|
|
41
|
+
"""Details required to enforce a timeout on test execution."""
|
|
42
|
+
|
|
43
|
+
_DEADLINE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' # format used to maintain backwards compatibility with previous versions of ansible-test
|
|
44
|
+
|
|
45
|
+
deadline: datetime.datetime
|
|
46
|
+
duration: int | float # minutes
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def remaining(self) -> datetime.timedelta:
|
|
50
|
+
"""The amount of time remaining before the timeout occurs. If the timeout has passed, this will be a negative duration."""
|
|
51
|
+
return self.deadline - datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0)
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, t.Any]:
|
|
54
|
+
"""Return timeout details as a dictionary suitable for JSON serialization."""
|
|
55
|
+
return dict(
|
|
56
|
+
deadline=self.deadline.strftime(self._DEADLINE_FORMAT),
|
|
57
|
+
duration=self.duration,
|
|
58
|
+
)
|
|
42
59
|
|
|
43
|
-
|
|
44
|
-
|
|
60
|
+
@staticmethod
|
|
61
|
+
def from_dict(value: dict[str, t.Any]) -> TimeoutDetail:
|
|
62
|
+
"""Return a TimeoutDetail instance using the value previously returned by to_dict."""
|
|
63
|
+
return TimeoutDetail(
|
|
64
|
+
deadline=datetime.datetime.strptime(value['deadline'], TimeoutDetail._DEADLINE_FORMAT).replace(tzinfo=datetime.timezone.utc),
|
|
65
|
+
duration=value['duration'],
|
|
66
|
+
)
|
|
45
67
|
|
|
46
|
-
|
|
68
|
+
@staticmethod
|
|
69
|
+
def create(duration: int | float) -> TimeoutDetail | None:
|
|
70
|
+
"""Return a new TimeoutDetail instance for the specified duration (in minutes), or None if the duration is zero."""
|
|
71
|
+
if not duration:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
if duration == int(duration):
|
|
75
|
+
duration = int(duration)
|
|
76
|
+
|
|
77
|
+
return TimeoutDetail(
|
|
78
|
+
deadline=datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0) + datetime.timedelta(seconds=int(duration * 60)),
|
|
79
|
+
duration=duration,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_timeout() -> TimeoutDetail | None:
|
|
84
|
+
"""Return details about the currently set timeout, if any, otherwise return None."""
|
|
85
|
+
try:
|
|
86
|
+
return TimeoutDetail.from_dict(read_json_file(TIMEOUT_PATH))
|
|
87
|
+
except FileNotFoundError:
|
|
88
|
+
return None
|
|
47
89
|
|
|
48
90
|
|
|
49
91
|
def configure_timeout(args: CommonConfig) -> None:
|
|
@@ -59,27 +101,22 @@ def configure_test_timeout(args: TestConfig) -> None:
|
|
|
59
101
|
if not timeout:
|
|
60
102
|
return
|
|
61
103
|
|
|
62
|
-
|
|
63
|
-
timeout_duration = timeout['duration']
|
|
64
|
-
timeout_deadline = timeout['deadline']
|
|
65
|
-
timeout_remaining = timeout_deadline - timeout_start
|
|
104
|
+
timeout_remaining = timeout.remaining
|
|
66
105
|
|
|
67
|
-
test_timeout = TestTimeout(
|
|
106
|
+
test_timeout = TestTimeout(timeout.duration)
|
|
68
107
|
|
|
69
108
|
if timeout_remaining <= datetime.timedelta():
|
|
70
109
|
test_timeout.write(args)
|
|
71
110
|
|
|
72
|
-
raise
|
|
73
|
-
timeout_duration, timeout_remaining * -1, timeout_deadline))
|
|
111
|
+
raise TimeoutExpiredError(f'The {timeout.duration} minute test timeout expired {timeout_remaining * -1} ago at {timeout.deadline}.')
|
|
74
112
|
|
|
75
|
-
display.info('The
|
|
76
|
-
timeout_duration, timeout_remaining, timeout_deadline), verbosity=1)
|
|
113
|
+
display.info(f'The {timeout.duration} minute test timeout expires in {timeout_remaining} at {timeout.deadline}.', verbosity=1)
|
|
77
114
|
|
|
78
115
|
def timeout_handler(_dummy1: t.Any, _dummy2: t.Any) -> None:
|
|
79
116
|
"""Runs when SIGUSR1 is received."""
|
|
80
117
|
test_timeout.write(args)
|
|
81
118
|
|
|
82
|
-
raise
|
|
119
|
+
raise TimeoutExpiredError(f'Tests aborted after exceeding the {timeout.duration} minute time limit.')
|
|
83
120
|
|
|
84
121
|
def timeout_waiter(timeout_seconds: int) -> None:
|
|
85
122
|
"""Background thread which will kill the current process if the timeout elapses."""
|
|
@@ -88,6 +125,6 @@ def configure_test_timeout(args: TestConfig) -> None:
|
|
|
88
125
|
|
|
89
126
|
signal.signal(signal.SIGUSR1, timeout_handler)
|
|
90
127
|
|
|
91
|
-
instance = WrappedThread(functools.partial(timeout_waiter, timeout_remaining.
|
|
128
|
+
instance = WrappedThread(functools.partial(timeout_waiter, timeout_remaining.total_seconds()))
|
|
92
129
|
instance.daemon = True
|
|
93
130
|
instance.start()
|
ansible_test/_internal/util.py
CHANGED
|
@@ -920,6 +920,10 @@ class ApplicationWarning(Exception):
|
|
|
920
920
|
"""General application warning which interrupts normal program flow."""
|
|
921
921
|
|
|
922
922
|
|
|
923
|
+
class TimeoutExpiredError(SystemExit):
|
|
924
|
+
"""Error raised when the test timeout has been reached or exceeded."""
|
|
925
|
+
|
|
926
|
+
|
|
923
927
|
class SubprocessError(ApplicationError):
|
|
924
928
|
"""Error resulting from failed subprocess execution."""
|
|
925
929
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Common utility code that depends on CommonConfig."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
-
import atexit
|
|
5
4
|
import collections.abc as c
|
|
6
5
|
import contextlib
|
|
7
6
|
import json
|
|
@@ -64,6 +63,39 @@ from .host_configs import (
|
|
|
64
63
|
CHECK_YAML_VERSIONS: dict[str, t.Any] = {}
|
|
65
64
|
|
|
66
65
|
|
|
66
|
+
class ExitHandler:
|
|
67
|
+
"""Simple exit handler implementation."""
|
|
68
|
+
_callbacks: list[tuple[t.Callable, tuple[t.Any, ...], dict[str, t.Any]]] = []
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def register(func: t.Callable, *args, **kwargs) -> None:
|
|
72
|
+
"""Register the given function and args as a callback to execute during program termination."""
|
|
73
|
+
ExitHandler._callbacks.append((func, args, kwargs))
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
@contextlib.contextmanager
|
|
77
|
+
def context() -> t.Generator[None, None, None]:
|
|
78
|
+
"""Run all registered handlers when the context is exited."""
|
|
79
|
+
last_exception: BaseException | None = None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
yield
|
|
83
|
+
finally:
|
|
84
|
+
queue = list(ExitHandler._callbacks)
|
|
85
|
+
|
|
86
|
+
while queue:
|
|
87
|
+
func, args, kwargs = queue.pop()
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
func(*args, **kwargs)
|
|
91
|
+
except BaseException as ex: # pylint: disable=broad-exception-caught
|
|
92
|
+
last_exception = ex
|
|
93
|
+
display.fatal(f'Exit handler failed: {ex}')
|
|
94
|
+
|
|
95
|
+
if last_exception:
|
|
96
|
+
raise last_exception
|
|
97
|
+
|
|
98
|
+
|
|
67
99
|
class ShellScriptTemplate:
|
|
68
100
|
"""A simple substitution template for shell scripts."""
|
|
69
101
|
|
|
@@ -211,7 +243,7 @@ def process_scoped_temporary_file(args: CommonConfig, prefix: t.Optional[str] =
|
|
|
211
243
|
else:
|
|
212
244
|
temp_fd, path = tempfile.mkstemp(prefix=prefix, suffix=suffix)
|
|
213
245
|
os.close(temp_fd)
|
|
214
|
-
|
|
246
|
+
ExitHandler.register(lambda: os.remove(path))
|
|
215
247
|
|
|
216
248
|
return path
|
|
217
249
|
|
|
@@ -222,7 +254,7 @@ def process_scoped_temporary_directory(args: CommonConfig, prefix: t.Optional[st
|
|
|
222
254
|
path = os.path.join(tempfile.gettempdir(), f'{prefix or tempfile.gettempprefix()}{generate_name()}{suffix or ""}')
|
|
223
255
|
else:
|
|
224
256
|
path = tempfile.mkdtemp(prefix=prefix, suffix=suffix)
|
|
225
|
-
|
|
257
|
+
ExitHandler.register(lambda: remove_tree(path))
|
|
226
258
|
|
|
227
259
|
return path
|
|
228
260
|
|
|
@@ -296,7 +328,7 @@ def get_injector_path() -> str:
|
|
|
296
328
|
"""Remove the temporary injector directory."""
|
|
297
329
|
remove_tree(injector_path)
|
|
298
330
|
|
|
299
|
-
|
|
331
|
+
ExitHandler.register(cleanup_injector)
|
|
300
332
|
|
|
301
333
|
return injector_path
|
|
302
334
|
|
|
@@ -354,7 +386,7 @@ def get_python_path(interpreter: str) -> str:
|
|
|
354
386
|
verified_chmod(python_path, MODE_DIRECTORY)
|
|
355
387
|
|
|
356
388
|
if not PYTHON_PATHS:
|
|
357
|
-
|
|
389
|
+
ExitHandler.register(cleanup_python_paths)
|
|
358
390
|
|
|
359
391
|
PYTHON_PATHS[interpreter] = python_path
|
|
360
392
|
|
|
@@ -364,7 +396,7 @@ def get_python_path(interpreter: str) -> str:
|
|
|
364
396
|
def create_temp_dir(prefix: t.Optional[str] = None, suffix: t.Optional[str] = None, base_dir: t.Optional[str] = None) -> str:
|
|
365
397
|
"""Create a temporary directory that persists until the current process exits."""
|
|
366
398
|
temp_path = tempfile.mkdtemp(prefix=prefix or 'tmp', suffix=suffix or '', dir=base_dir)
|
|
367
|
-
|
|
399
|
+
ExitHandler.register(remove_tree, temp_path)
|
|
368
400
|
return temp_path
|
|
369
401
|
|
|
370
402
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|