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
@@ -1,6 +1,6 @@
|
|
1
1
|
"""Internal utilities for serialization and deserialization."""
|
2
2
|
|
3
|
-
# DTFIX-
|
3
|
+
# DTFIX-FUTURE: most of this isn't JSON specific, find a better home
|
4
4
|
|
5
5
|
from __future__ import annotations
|
6
6
|
|
@@ -144,20 +144,20 @@ class AnsibleVariableVisitor:
|
|
144
144
|
value = self._template_engine.transform(value)
|
145
145
|
value_type = type(value)
|
146
146
|
|
147
|
-
#
|
147
|
+
# DTFIX3: need to handle native copy for keys too
|
148
148
|
if self.convert_to_native_values and isinstance(value, _datatag.AnsibleTaggedObject):
|
149
149
|
value = value._native_copy()
|
150
150
|
value_type = type(value)
|
151
151
|
|
152
152
|
result: _T
|
153
153
|
|
154
|
-
#
|
155
|
-
# keep in mind the allowed types for keys is a more restrictive set than for values (str and
|
156
|
-
#
|
154
|
+
# DTFIX3: the visitor is ignoring dict/mapping keys except for debugging and schema-aware checking, it should be doing type checks on keys
|
155
|
+
# keep in mind the allowed types for keys is a more restrictive set than for values (str and tagged str only, not EncryptedString)
|
156
|
+
# DTFIX5: some type lists being consulted (the ones from datatag) are probably too permissive, and perhaps should not be dynamic
|
157
157
|
|
158
158
|
if (result := self._early_visit(value, value_type)) is not _sentinel:
|
159
159
|
pass
|
160
|
-
#
|
160
|
+
# DTFIX7: de-duplicate and optimize; extract inline generator expressions and fallback function or mapping for native type calculation?
|
161
161
|
elif value_type in _ANSIBLE_ALLOWED_MAPPING_VAR_TYPES: # check mappings first, because they're also collections
|
162
162
|
with self: # supports StateTrackingMixIn
|
163
163
|
result = AnsibleTagHelper.tag_copy(value, ((k, self._visit(k, v)) for k, v in value.items()), value_type=value_type)
|
@@ -46,6 +46,8 @@ class _Profile(_profiles._JSONSerializationProfile):
|
|
46
46
|
_datetime.datetime: _datatag.AnsibleSerializableDateTime,
|
47
47
|
}
|
48
48
|
|
49
|
+
cls.handle_key = cls._handle_key_str_fallback # legacy stdlib-compatible key behavior
|
50
|
+
|
49
51
|
|
50
52
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
51
53
|
_profile = _Profile
|
@@ -12,7 +12,7 @@ from . import _legacy
|
|
12
12
|
class _InventoryVariableVisitor(_legacy._LegacyVariableVisitor, _json.StateTrackingMixIn):
|
13
13
|
"""State-tracking visitor implementation that only applies trust to `_meta.hostvars` and `vars` inventory values."""
|
14
14
|
|
15
|
-
#
|
15
|
+
# DTFIX5: does the variable visitor need to support conversion of sequence/mapping for inventory?
|
16
16
|
|
17
17
|
@property
|
18
18
|
def _allow_trust(self) -> bool:
|
@@ -152,9 +152,11 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
152
152
|
'__ansible_vault': cls.deserialize_vault,
|
153
153
|
}
|
154
154
|
|
155
|
+
cls.handle_key = cls._handle_key_str_fallback # type: ignore[method-assign] # legacy stdlib-compatible key behavior
|
156
|
+
|
155
157
|
@classmethod
|
156
158
|
def pre_serialize(cls, encoder: Encoder, o: _t.Any) -> _t.Any:
|
157
|
-
#
|
159
|
+
# DTFIX7: these conversion args probably aren't needed
|
158
160
|
avv = cls.visitor_type(invert_trust=True, convert_mapping_to_dict=True, convert_sequence_to_list=True, convert_custom_scalars=True)
|
159
161
|
|
160
162
|
return avv.visit(o)
|
@@ -165,16 +167,6 @@ class _Profile(_profiles._JSONSerializationProfile["Encoder", "Decoder"]):
|
|
165
167
|
|
166
168
|
return avv.visit(o)
|
167
169
|
|
168
|
-
@classmethod
|
169
|
-
def handle_key(cls, k: _t.Any) -> _t.Any:
|
170
|
-
if isinstance(k, str):
|
171
|
-
return k
|
172
|
-
|
173
|
-
# DTFIX-RELEASE: decide if this is a deprecation warning, error, or what?
|
174
|
-
# Non-string variable names have been disallowed by set_fact and other things since at least 2021.
|
175
|
-
# DTFIX-RELEASE: document why this behavior is here, also verify the legacy tagless use case doesn't need this same behavior
|
176
|
-
return str(k)
|
177
|
-
|
178
170
|
|
179
171
|
class Encoder(_profiles.AnsibleProfileJSONEncoder):
|
180
172
|
_profile = _Profile
|
File without changes
|
@@ -0,0 +1,91 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import atexit
|
4
|
+
import os
|
5
|
+
import subprocess
|
6
|
+
|
7
|
+
from ansible import constants as C
|
8
|
+
from ansible._internal._errors import _alarm_timeout
|
9
|
+
from ansible._internal._ssh._ssh_agent import SshAgentClient
|
10
|
+
from ansible.cli import display
|
11
|
+
from ansible.errors import AnsibleError
|
12
|
+
from ansible.module_utils.common.process import get_bin_path
|
13
|
+
|
14
|
+
_SSH_AGENT_STDOUT_READ_TIMEOUT = 5 # seconds
|
15
|
+
|
16
|
+
|
17
|
+
def launch_ssh_agent() -> None:
|
18
|
+
"""If configured via `SSH_AGENT`, launch an ssh-agent for Ansible's use and/or verify access to an existing one."""
|
19
|
+
try:
|
20
|
+
_launch_ssh_agent()
|
21
|
+
except Exception as ex:
|
22
|
+
raise AnsibleError("Failed to launch ssh agent.") from ex
|
23
|
+
|
24
|
+
|
25
|
+
def _launch_ssh_agent() -> None:
|
26
|
+
ssh_agent_cfg = C.config.get_config_value('SSH_AGENT')
|
27
|
+
|
28
|
+
match ssh_agent_cfg:
|
29
|
+
case 'none':
|
30
|
+
display.debug('SSH_AGENT set to none')
|
31
|
+
return
|
32
|
+
case 'auto':
|
33
|
+
try:
|
34
|
+
ssh_agent_bin = get_bin_path(C.config.get_config_value('SSH_AGENT_EXECUTABLE'))
|
35
|
+
except ValueError as e:
|
36
|
+
raise AnsibleError('SSH_AGENT set to auto, but cannot find ssh-agent binary.') from e
|
37
|
+
|
38
|
+
ssh_agent_dir = os.path.join(C.DEFAULT_LOCAL_TMP, 'ssh_agent')
|
39
|
+
os.mkdir(ssh_agent_dir, 0o700)
|
40
|
+
sock = os.path.join(ssh_agent_dir, 'agent.sock')
|
41
|
+
display.vvv('SSH_AGENT: starting...')
|
42
|
+
|
43
|
+
try:
|
44
|
+
p = subprocess.Popen(
|
45
|
+
[ssh_agent_bin, '-D', '-s', '-a', sock],
|
46
|
+
stdin=subprocess.PIPE,
|
47
|
+
stdout=subprocess.PIPE,
|
48
|
+
stderr=subprocess.PIPE,
|
49
|
+
text=True,
|
50
|
+
)
|
51
|
+
except OSError as e:
|
52
|
+
raise AnsibleError('Could not start ssh-agent.') from e
|
53
|
+
|
54
|
+
atexit.register(p.terminate)
|
55
|
+
|
56
|
+
help_text = f'The ssh-agent {ssh_agent_bin!r} might be an incompatible agent.'
|
57
|
+
expected_stdout = 'SSH_AUTH_SOCK'
|
58
|
+
|
59
|
+
try:
|
60
|
+
with _alarm_timeout.AnsibleTimeoutError.alarm_timeout(_SSH_AGENT_STDOUT_READ_TIMEOUT):
|
61
|
+
stdout = p.stdout.read(len(expected_stdout))
|
62
|
+
except _alarm_timeout.AnsibleTimeoutError as e:
|
63
|
+
display.error_as_warning(
|
64
|
+
msg=f'Timed out waiting for expected stdout {expected_stdout!r} from ssh-agent.',
|
65
|
+
exception=e,
|
66
|
+
help_text=help_text,
|
67
|
+
)
|
68
|
+
else:
|
69
|
+
if stdout != expected_stdout:
|
70
|
+
display.warning(
|
71
|
+
msg=f'The ssh-agent output {stdout!r} did not match expected {expected_stdout!r}.',
|
72
|
+
help_text=help_text,
|
73
|
+
)
|
74
|
+
|
75
|
+
if p.poll() is not None:
|
76
|
+
raise AnsibleError(
|
77
|
+
message='The ssh-agent terminated prematurely.',
|
78
|
+
help_text=f'{help_text}\n\nReturn Code: {p.returncode}\nStandard Error:\n{p.stderr.read()}',
|
79
|
+
)
|
80
|
+
|
81
|
+
display.vvv(f'SSH_AGENT: ssh-agent[{p.pid}] started and bound to {sock}')
|
82
|
+
case _:
|
83
|
+
sock = ssh_agent_cfg
|
84
|
+
|
85
|
+
try:
|
86
|
+
with SshAgentClient(sock) as client:
|
87
|
+
client.list()
|
88
|
+
except Exception as e:
|
89
|
+
raise AnsibleError(f'Could not communicate with ssh-agent using auth sock {sock!r}.') from e
|
90
|
+
|
91
|
+
os.environ['SSH_AUTH_SOCK'] = os.environ['ANSIBLE_SSH_AGENT'] = sock
|
@@ -106,21 +106,19 @@ class SshAgentFailure(RuntimeError):
|
|
106
106
|
# NOTE: Classes below somewhat represent "Data Type Representations Used in the SSH Protocols"
|
107
107
|
# as specified by RFC4251
|
108
108
|
|
109
|
+
|
109
110
|
@t.runtime_checkable
|
110
111
|
class SupportsToBlob(t.Protocol):
|
111
|
-
def to_blob(self) -> bytes:
|
112
|
-
...
|
112
|
+
def to_blob(self) -> bytes: ...
|
113
113
|
|
114
114
|
|
115
115
|
@t.runtime_checkable
|
116
116
|
class SupportsFromBlob(t.Protocol):
|
117
117
|
@classmethod
|
118
|
-
def from_blob(cls, blob: memoryview | bytes) -> t.Self:
|
119
|
-
...
|
118
|
+
def from_blob(cls, blob: memoryview | bytes) -> t.Self: ...
|
120
119
|
|
121
120
|
@classmethod
|
122
|
-
def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]:
|
123
|
-
...
|
121
|
+
def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]: ...
|
124
122
|
|
125
123
|
|
126
124
|
def _split_blob(blob: memoryview | bytes, length: int) -> tuple[memoryview | bytes, memoryview | bytes]:
|
@@ -304,10 +302,12 @@ class PrivateKeyMsg(Msg):
|
|
304
302
|
return EcdsaPrivateKeyMsg(
|
305
303
|
getattr(KeyAlgo, f'ECDSA{key_size}'),
|
306
304
|
unicode_string(f'nistp{key_size}'),
|
307
|
-
binary_string(
|
308
|
-
|
309
|
-
|
310
|
-
|
305
|
+
binary_string(
|
306
|
+
private_key.public_key().public_bytes(
|
307
|
+
encoding=serialization.Encoding.X962,
|
308
|
+
format=serialization.PublicFormat.UncompressedPoint,
|
309
|
+
)
|
310
|
+
),
|
311
311
|
mpint(ecdsa_pn.private_value),
|
312
312
|
)
|
313
313
|
case Ed25519PrivateKey():
|
@@ -318,7 +318,7 @@ class PrivateKeyMsg(Msg):
|
|
318
318
|
private_bytes = private_key.private_bytes(
|
319
319
|
encoding=serialization.Encoding.Raw,
|
320
320
|
format=serialization.PrivateFormat.Raw,
|
321
|
-
encryption_algorithm=serialization.NoEncryption()
|
321
|
+
encryption_algorithm=serialization.NoEncryption(),
|
322
322
|
)
|
323
323
|
return Ed25519PrivateKeyMsg(
|
324
324
|
KeyAlgo.ED25519,
|
@@ -376,14 +376,14 @@ class Ed25519PrivateKeyMsg(PrivateKeyMsg):
|
|
376
376
|
@dataclasses.dataclass
|
377
377
|
class PublicKeyMsg(Msg):
|
378
378
|
@staticmethod
|
379
|
-
def get_dataclass(
|
380
|
-
|
381
|
-
) -> type[t.Union[
|
379
|
+
def get_dataclass(type: KeyAlgo) -> type[
|
380
|
+
t.Union[
|
382
381
|
RSAPublicKeyMsg,
|
383
382
|
EcdsaPublicKeyMsg,
|
384
383
|
Ed25519PublicKeyMsg,
|
385
|
-
DSAPublicKeyMsg
|
386
|
-
|
384
|
+
DSAPublicKeyMsg,
|
385
|
+
]
|
386
|
+
]:
|
387
387
|
match type:
|
388
388
|
case KeyAlgo.RSA:
|
389
389
|
return RSAPublicKeyMsg
|
@@ -401,29 +401,14 @@ class PublicKeyMsg(Msg):
|
|
401
401
|
type: KeyAlgo = self.type
|
402
402
|
match type:
|
403
403
|
case KeyAlgo.RSA:
|
404
|
-
return RSAPublicNumbers(
|
405
|
-
self.e,
|
406
|
-
self.n
|
407
|
-
).public_key()
|
404
|
+
return RSAPublicNumbers(self.e, self.n).public_key()
|
408
405
|
case KeyAlgo.ECDSA256 | KeyAlgo.ECDSA384 | KeyAlgo.ECDSA521:
|
409
406
|
curve = _ECDSA_KEY_TYPE[KeyAlgo(type)]
|
410
|
-
return EllipticCurvePublicKey.from_encoded_point(
|
411
|
-
curve(),
|
412
|
-
self.Q
|
413
|
-
)
|
407
|
+
return EllipticCurvePublicKey.from_encoded_point(curve(), self.Q)
|
414
408
|
case KeyAlgo.ED25519:
|
415
|
-
return Ed25519PublicKey.from_public_bytes(
|
416
|
-
self.enc_a
|
417
|
-
)
|
409
|
+
return Ed25519PublicKey.from_public_bytes(self.enc_a)
|
418
410
|
case KeyAlgo.DSA:
|
419
|
-
return DSAPublicNumbers(
|
420
|
-
self.y,
|
421
|
-
DSAParameterNumbers(
|
422
|
-
self.p,
|
423
|
-
self.q,
|
424
|
-
self.g
|
425
|
-
)
|
426
|
-
).public_key()
|
411
|
+
return DSAPublicNumbers(self.y, DSAParameterNumbers(self.p, self.q, self.g)).public_key()
|
427
412
|
case _:
|
428
413
|
raise NotImplementedError(type)
|
429
414
|
|
@@ -437,32 +422,32 @@ class PublicKeyMsg(Msg):
|
|
437
422
|
mpint(dsa_pn.parameter_numbers.p),
|
438
423
|
mpint(dsa_pn.parameter_numbers.q),
|
439
424
|
mpint(dsa_pn.parameter_numbers.g),
|
440
|
-
mpint(dsa_pn.y)
|
425
|
+
mpint(dsa_pn.y),
|
441
426
|
)
|
442
427
|
case EllipticCurvePublicKey():
|
443
428
|
return EcdsaPublicKeyMsg(
|
444
429
|
getattr(KeyAlgo, f'ECDSA{public_key.curve.key_size}'),
|
445
430
|
unicode_string(f'nistp{public_key.curve.key_size}'),
|
446
|
-
binary_string(
|
447
|
-
|
448
|
-
|
449
|
-
|
431
|
+
binary_string(
|
432
|
+
public_key.public_bytes(
|
433
|
+
encoding=serialization.Encoding.X962,
|
434
|
+
format=serialization.PublicFormat.UncompressedPoint,
|
435
|
+
)
|
436
|
+
),
|
450
437
|
)
|
451
438
|
case Ed25519PublicKey():
|
452
439
|
return Ed25519PublicKeyMsg(
|
453
440
|
KeyAlgo.ED25519,
|
454
|
-
binary_string(
|
455
|
-
|
456
|
-
|
457
|
-
|
441
|
+
binary_string(
|
442
|
+
public_key.public_bytes(
|
443
|
+
encoding=serialization.Encoding.Raw,
|
444
|
+
format=serialization.PublicFormat.Raw,
|
445
|
+
)
|
446
|
+
),
|
458
447
|
)
|
459
448
|
case RSAPublicKey():
|
460
449
|
rsa_pn: RSAPublicNumbers = public_key.public_numbers()
|
461
|
-
return RSAPublicKeyMsg(
|
462
|
-
KeyAlgo.RSA,
|
463
|
-
mpint(rsa_pn.e),
|
464
|
-
mpint(rsa_pn.n)
|
465
|
-
)
|
450
|
+
return RSAPublicKeyMsg(KeyAlgo.RSA, mpint(rsa_pn.e), mpint(rsa_pn.n))
|
466
451
|
case _:
|
467
452
|
raise NotImplementedError(public_key)
|
468
453
|
|
@@ -473,10 +458,7 @@ class PublicKeyMsg(Msg):
|
|
473
458
|
msg.comments = unicode_string('')
|
474
459
|
k = msg.to_blob()
|
475
460
|
digest.update(k)
|
476
|
-
return binascii.b2a_base64(
|
477
|
-
digest.digest(),
|
478
|
-
newline=False
|
479
|
-
).rstrip(b'=').decode('utf-8')
|
461
|
+
return binascii.b2a_base64(digest.digest(), newline=False).rstrip(b'=').decode('utf-8')
|
480
462
|
|
481
463
|
|
482
464
|
@dataclasses.dataclass(order=True, slots=True)
|
@@ -519,9 +501,7 @@ class KeyList(Msg):
|
|
519
501
|
|
520
502
|
def __post_init__(self) -> None:
|
521
503
|
if self.nkeys != len(self.keys):
|
522
|
-
raise SshAgentFailure(
|
523
|
-
"agent: invalid number of keys received for identities list"
|
524
|
-
)
|
504
|
+
raise SshAgentFailure("agent: invalid number of keys received for identities list")
|
525
505
|
|
526
506
|
|
527
507
|
@dataclasses.dataclass(order=True, slots=True)
|
@@ -535,8 +515,7 @@ class PublicKeyMsgList(Msg):
|
|
535
515
|
return len(self.keys)
|
536
516
|
|
537
517
|
@classmethod
|
538
|
-
def from_blob(cls, blob: memoryview | bytes) -> t.Self:
|
539
|
-
...
|
518
|
+
def from_blob(cls, blob: memoryview | bytes) -> t.Self: ...
|
540
519
|
|
541
520
|
@classmethod
|
542
521
|
def consume_from_blob(cls, blob: memoryview | bytes) -> tuple[t.Self, memoryview | bytes]:
|
@@ -546,22 +525,16 @@ class PublicKeyMsgList(Msg):
|
|
546
525
|
key_blob, key_blob_length, comment_blob = cls._consume_field(blob)
|
547
526
|
|
548
527
|
peek_key_algo, _length, _blob = cls._consume_field(key_blob)
|
549
|
-
pub_key_msg_cls = PublicKeyMsg.get_dataclass(
|
550
|
-
KeyAlgo(bytes(peek_key_algo).decode('utf-8'))
|
551
|
-
)
|
528
|
+
pub_key_msg_cls = PublicKeyMsg.get_dataclass(KeyAlgo(bytes(peek_key_algo).decode('utf-8')))
|
552
529
|
|
553
530
|
_fv, comment_blob_length, blob = cls._consume_field(comment_blob)
|
554
|
-
key_plus_comment = (
|
555
|
-
prev_blob[4: (4 + key_blob_length) + (4 + comment_blob_length)]
|
556
|
-
)
|
531
|
+
key_plus_comment = prev_blob[4 : (4 + key_blob_length) + (4 + comment_blob_length)]
|
557
532
|
|
558
533
|
args.append(pub_key_msg_cls.from_blob(key_plus_comment))
|
559
534
|
return cls(args), b""
|
560
535
|
|
561
536
|
@staticmethod
|
562
|
-
def _consume_field(
|
563
|
-
blob: memoryview | bytes
|
564
|
-
) -> tuple[memoryview | bytes, uint32, memoryview | bytes]:
|
537
|
+
def _consume_field(blob: memoryview | bytes) -> tuple[memoryview | bytes, uint32, memoryview | bytes]:
|
565
538
|
length = uint32.from_blob(blob[:4])
|
566
539
|
blob = blob[4:]
|
567
540
|
data, rest = _split_blob(blob, length)
|
@@ -581,10 +554,10 @@ class SshAgentClient:
|
|
581
554
|
return self
|
582
555
|
|
583
556
|
def __exit__(
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
557
|
+
self,
|
558
|
+
exc_type: type[BaseException] | None,
|
559
|
+
exc_value: BaseException | None,
|
560
|
+
traceback: types.TracebackType | None,
|
588
561
|
) -> None:
|
589
562
|
self.close()
|
590
563
|
|
@@ -598,34 +571,25 @@ class SshAgentClient:
|
|
598
571
|
return resp
|
599
572
|
|
600
573
|
def remove_all(self) -> None:
|
601
|
-
self.send(
|
602
|
-
ProtocolMsgNumbers.SSH_AGENTC_REMOVE_ALL_IDENTITIES.to_blob()
|
603
|
-
)
|
574
|
+
self.send(ProtocolMsgNumbers.SSH_AGENTC_REMOVE_ALL_IDENTITIES.to_blob())
|
604
575
|
|
605
576
|
def remove(self, public_key: CryptoPublicKey) -> None:
|
606
577
|
key_blob = PublicKeyMsg.from_public_key(public_key).to_blob()
|
607
|
-
self.send(
|
608
|
-
ProtocolMsgNumbers.SSH_AGENTC_REMOVE_IDENTITY.to_blob() +
|
609
|
-
uint32(len(key_blob)).to_blob() + key_blob
|
610
|
-
)
|
578
|
+
self.send(ProtocolMsgNumbers.SSH_AGENTC_REMOVE_IDENTITY.to_blob() + uint32(len(key_blob)).to_blob() + key_blob)
|
611
579
|
|
612
580
|
def add(
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
581
|
+
self,
|
582
|
+
private_key: CryptoPrivateKey,
|
583
|
+
comments: str | None = None,
|
584
|
+
lifetime: int | None = None,
|
585
|
+
confirm: bool | None = None,
|
618
586
|
) -> None:
|
619
587
|
key_msg = PrivateKeyMsg.from_private_key(private_key)
|
620
588
|
key_msg.comments = unicode_string(comments or '')
|
621
589
|
if lifetime:
|
622
|
-
key_msg.constraints += constraints(
|
623
|
-
[ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_LIFETIME]
|
624
|
-
).to_blob() + uint32(lifetime).to_blob()
|
590
|
+
key_msg.constraints += constraints([ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_LIFETIME]).to_blob() + uint32(lifetime).to_blob()
|
625
591
|
if confirm:
|
626
|
-
key_msg.constraints += constraints(
|
627
|
-
[ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_CONFIRM]
|
628
|
-
).to_blob()
|
592
|
+
key_msg.constraints += constraints([ProtocolMsgNumbers.SSH_AGENT_CONSTRAIN_CONFIRM]).to_blob()
|
629
593
|
|
630
594
|
if key_msg.constraints:
|
631
595
|
msg = ProtocolMsgNumbers.SSH_AGENTC_ADD_ID_CONSTRAINED.to_blob()
|
@@ -638,9 +602,7 @@ class SshAgentClient:
|
|
638
602
|
req = ProtocolMsgNumbers.SSH_AGENTC_REQUEST_IDENTITIES.to_blob()
|
639
603
|
r = memoryview(bytearray(self.send(req)))
|
640
604
|
if r[0] != ProtocolMsgNumbers.SSH_AGENT_IDENTITIES_ANSWER:
|
641
|
-
raise SshAgentFailure(
|
642
|
-
'agent: non-identities answer received for identities list'
|
643
|
-
)
|
605
|
+
raise SshAgentFailure('agent: non-identities answer received for identities list')
|
644
606
|
return KeyList.from_blob(r[1:])
|
645
607
|
|
646
608
|
def __contains__(self, public_key: CryptoPublicKey) -> bool:
|
@@ -649,7 +611,7 @@ class SshAgentClient:
|
|
649
611
|
|
650
612
|
|
651
613
|
@functools.cache
|
652
|
-
def
|
614
|
+
def key_data_into_crypto_objects(key_data: bytes, passphrase: bytes | None) -> tuple[CryptoPrivateKey, CryptoPublicKey, str]:
|
653
615
|
private_key = serialization.ssh.load_ssh_private_key(key_data, passphrase)
|
654
616
|
public_key = private_key.public_key()
|
655
617
|
fingerprint = PublicKeyMsg.from_public_key(public_key).fingerprint
|
@@ -1,10 +1,12 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
|
3
|
+
import importlib.metadata
|
4
|
+
|
5
|
+
jinja2_version = importlib.metadata.version('jinja2')
|
4
6
|
|
5
7
|
# DTFIX-FUTURE: sanity test to ensure this doesn't drift from requirements
|
6
8
|
_MINIMUM_JINJA_VERSION = (3, 1)
|
7
|
-
_CURRENT_JINJA_VERSION = tuple(map(int,
|
9
|
+
_CURRENT_JINJA_VERSION = tuple(map(int, jinja2_version.split('.', maxsplit=2)[:2]))
|
8
10
|
|
9
11
|
if _CURRENT_JINJA_VERSION < _MINIMUM_JINJA_VERSION:
|
10
|
-
raise RuntimeError(f'Jinja version {".".join(map(str, _MINIMUM_JINJA_VERSION))} or higher is required (current version {
|
12
|
+
raise RuntimeError(f'Jinja version {".".join(map(str, _MINIMUM_JINJA_VERSION))} or higher is required (current version {jinja2_version}).')
|
@@ -60,6 +60,7 @@ class DeprecatedAccessAuditContext(NotifiableAccessContextBase):
|
|
60
60
|
date=item.deprecated.date,
|
61
61
|
obj=item.template,
|
62
62
|
deprecator=item.deprecated.deprecator,
|
63
|
+
formatted_traceback=item.deprecated.formatted_traceback,
|
63
64
|
)
|
64
65
|
|
65
66
|
return result
|
@@ -79,7 +80,7 @@ class DeprecatedAccessAuditContext(NotifiableAccessContextBase):
|
|
79
80
|
# DTFIX-FUTURE: ascend the template stack to try and find the nearest string source template
|
80
81
|
origin = Origin.get_tag(template)
|
81
82
|
|
82
|
-
# DTFIX-
|
83
|
+
# DTFIX-FUTURE: this should probably use a synthesized description value on the tag
|
83
84
|
# it is reachable from the data_tagging_controller test: ../playbook_output_validator/filter.py actual_stdout.txt actual_stderr.txt
|
84
85
|
# -[DEPRECATION WARNING]: `something_old` is deprecated, don't use it! This feature will be removed in version 1.2.3.
|
85
86
|
# +[DEPRECATION WARNING]: While processing '<<container>>': `something_old` is deprecated, don't use it! This feature will be removed in ...
|
@@ -75,7 +75,7 @@ class TemplateOptions:
|
|
75
75
|
value_for_omit: object = Omit
|
76
76
|
escape_backslashes: bool = True
|
77
77
|
preserve_trailing_newlines: bool = True
|
78
|
-
# DTFIX-
|
78
|
+
# DTFIX-FUTURE: these aren't really overrides anymore, rename the dataclass and this field
|
79
79
|
# also mention in docstring this has no effect unless used to template a string
|
80
80
|
overrides: TemplateOverrides = TemplateOverrides.DEFAULT
|
81
81
|
|
@@ -122,7 +122,6 @@ class TemplateEngine:
|
|
122
122
|
return new_engine
|
123
123
|
|
124
124
|
def extend(self, marker_behavior: MarkerBehavior | None = None) -> t.Self:
|
125
|
-
# DTFIX-RELEASE: bikeshed name, supported features
|
126
125
|
new_templar = type(self)(
|
127
126
|
loader=self._loader,
|
128
127
|
variables=self._variables,
|
@@ -187,7 +186,7 @@ class TemplateEngine:
|
|
187
186
|
@property
|
188
187
|
def available_variables(self) -> dict[str, t.Any] | ChainMap[str, t.Any]:
|
189
188
|
"""Available variables this instance will use when templating."""
|
190
|
-
#
|
189
|
+
# DTFIX3: ensure that we're always accessing this as a shallow container-level snapshot, and eliminate uses of anything
|
191
190
|
# that directly mutates this value. _new_context may resolve this for us?
|
192
191
|
if self._variables is None:
|
193
192
|
self._variables = self._variables_factory() if self._variables_factory else {}
|
@@ -235,7 +234,7 @@ class TemplateEngine:
|
|
235
234
|
|
236
235
|
def template(
|
237
236
|
self,
|
238
|
-
variable: t.Any, # DTFIX-
|
237
|
+
variable: t.Any, # DTFIX-FUTURE: once we settle the new/old API boundaries, rename this (here and in other methods)
|
239
238
|
*,
|
240
239
|
options: TemplateOptions = TemplateOptions.DEFAULT,
|
241
240
|
mode: TemplateMode = TemplateMode.DEFAULT,
|
@@ -49,6 +49,7 @@ from ._jinja_common import (
|
|
49
49
|
TruncationMarker,
|
50
50
|
validate_arg_type,
|
51
51
|
JinjaCallContext,
|
52
|
+
_SandboxMode,
|
52
53
|
)
|
53
54
|
from ._jinja_plugins import JinjaPluginIntercept, _query, _lookup, _now, _wrap_plugin_output, get_first_marker_arg, _DirectCall, _jinja_const_template_warning
|
54
55
|
from ._lazy_containers import (
|
@@ -71,6 +72,11 @@ from ansible.vars.hostvars import HostVars, HostVarsVars
|
|
71
72
|
from ...module_utils.datatag import native_type_name
|
72
73
|
|
73
74
|
JINJA2_OVERRIDE = '#jinja2:'
|
75
|
+
"""
|
76
|
+
String values prefixed with this sequence are interpreted as templates, even without template delimiters.
|
77
|
+
The values following this prefix up to the first newline are parsed as Jinja2 template overrides.
|
78
|
+
To include this literal value at the start of a string, a space or other character must precede it.
|
79
|
+
"""
|
74
80
|
|
75
81
|
display = Display()
|
76
82
|
|
@@ -304,7 +310,7 @@ class AnsibleTemplate(Template):
|
|
304
310
|
_python_source_temp_path: pathlib.Path | None = None
|
305
311
|
|
306
312
|
def __del__(self):
|
307
|
-
# DTFIX-
|
313
|
+
# DTFIX-FUTURE: this still isn't working reliably; something else must be keeping the template object alive
|
308
314
|
if self._python_source_temp_path:
|
309
315
|
self._python_source_temp_path.unlink(missing_ok=True)
|
310
316
|
|
@@ -497,7 +503,7 @@ def create_template_error(ex: Exception, variable: t.Any, is_expression: bool) -
|
|
497
503
|
return exception_to_raise
|
498
504
|
|
499
505
|
|
500
|
-
#
|
506
|
+
# DTFIX3: implement CapturedExceptionMarker deferral support on call (and lookup), filter/test plugins, etc.
|
501
507
|
# also update the protomatter integration test once this is done (the test was written differently since this wasn't done yet)
|
502
508
|
|
503
509
|
_BUILTIN_FILTER_ALIASES: dict[str, str] = {}
|
@@ -583,10 +589,17 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
|
|
583
589
|
|
584
590
|
return template_obj
|
585
591
|
|
592
|
+
def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool:
|
593
|
+
# deprecated: description="remove relaxed template sandbox mode support" core_version="2.23"
|
594
|
+
if _TemplateConfig.sandbox_mode == _SandboxMode.ALLOW_UNSAFE_ATTRIBUTES:
|
595
|
+
return True
|
596
|
+
|
597
|
+
return super().is_safe_attribute(obj, attr, value)
|
598
|
+
|
586
599
|
@property
|
587
600
|
def lexer(self) -> AnsibleLexer:
|
588
601
|
"""Return/cache an AnsibleLexer with settings from the current AnsibleEnvironment"""
|
589
|
-
# DTFIX-
|
602
|
+
# DTFIX-FUTURE: optimization - we should pre-generate the default cached lexer before forking, not leave it to chance (e.g. simple playbooks)
|
590
603
|
key = tuple(getattr(self, name) for name in _TEMPLATE_OVERRIDE_FIELD_NAMES)
|
591
604
|
|
592
605
|
lex = self._lexer_cache.get(key)
|
@@ -610,7 +623,7 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
|
|
610
623
|
Without this, `_wrap_filter` will wrap `args` and `kwargs` in templating lazy containers.
|
611
624
|
This provides consistency with plugin output handling by preventing auto-templating of trusted templates passed in native containers.
|
612
625
|
"""
|
613
|
-
# DTFIX-
|
626
|
+
# DTFIX-FUTURE: need better logic to handle non-list/non-dict inputs for args/kwargs
|
614
627
|
args = _AnsibleLazyTemplateMixin._try_create(list(args or []), LazyOptions.SKIP_TEMPLATES)
|
615
628
|
kwargs = _AnsibleLazyTemplateMixin._try_create(kwargs, LazyOptions.SKIP_TEMPLATES)
|
616
629
|
|
@@ -630,7 +643,7 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
|
|
630
643
|
Without this, `_wrap_test` will wrap `args` and `kwargs` in templating lazy containers.
|
631
644
|
This provides consistency with plugin output handling by preventing auto-templating of trusted templates passed in native containers.
|
632
645
|
"""
|
633
|
-
# DTFIX-
|
646
|
+
# DTFIX-FUTURE: need better logic to handle non-list/non-dict inputs for args/kwargs
|
634
647
|
args = _AnsibleLazyTemplateMixin._try_create(list(args or []), LazyOptions.SKIP_TEMPLATES)
|
635
648
|
kwargs = _AnsibleLazyTemplateMixin._try_create(kwargs, LazyOptions.SKIP_TEMPLATES)
|
636
649
|
|
@@ -701,7 +714,6 @@ class AnsibleEnvironment(ImmutableSandboxedEnvironment):
|
|
701
714
|
# this code is complemented by our tweaked CodeGenerator _output_const_repr that ensures that literal constants
|
702
715
|
# in templates aren't double-repr'd in the generated code
|
703
716
|
if len(node_list) == 1:
|
704
|
-
# DTFIX-RELEASE: determine if we should do managed access here (we *should* have hit them all during templating/resolve, but ?)
|
705
717
|
return node_list[0]
|
706
718
|
|
707
719
|
# In order to ensure that all markers are tripped, do a recursive finalize before we repr (otherwise we can end up
|
@@ -856,9 +868,6 @@ def _flatten_and_lazify_vars(mapping: c.Mapping) -> t.Iterable[c.Mapping]:
|
|
856
868
|
for m in mapping.maps:
|
857
869
|
yield from _flatten_and_lazify_vars(m)
|
858
870
|
elif mapping_type is _AnsibleLazyTemplateDict:
|
859
|
-
if not mapping:
|
860
|
-
# DTFIX-RELEASE: handle or remove?
|
861
|
-
raise Exception("we didn't think it was possible to have an empty lazy here...")
|
862
871
|
yield mapping
|
863
872
|
elif mapping_type in (dict, _AnsibleTaggedDict):
|
864
873
|
# don't propagate empty dictionary layers
|
@@ -882,10 +891,6 @@ def _new_context(
|
|
882
891
|
layers = []
|
883
892
|
|
884
893
|
if jinja_locals:
|
885
|
-
# DTFIX-RELEASE: if we can't trip this in coverage, kill it off?
|
886
|
-
if type(jinja_locals) is not dict: # pylint: disable=unidiomatic-typecheck
|
887
|
-
raise NotImplementedError("locals must be a dict")
|
888
|
-
|
889
894
|
# Omit values set to Jinja's internal `missing` sentinel; they are locals that have not yet been
|
890
895
|
# initialized in the current context, and should not be exposed to child contexts. e.g.: {% import 'a' as b with context %}.
|
891
896
|
# The `b` local will be `missing` in the `a` context and should not be propagated as a local to the child context we're creating.
|
@@ -978,7 +983,7 @@ def _finalize_list(o: t.Any, mode: FinalizeMode) -> t.Iterator[t.Any]:
|
|
978
983
|
|
979
984
|
|
980
985
|
def _maybe_finalize_scalar(o: t.Any) -> t.Any:
|
981
|
-
#
|
986
|
+
# DTFIX5: this should check all supported scalar subclasses, not just JSON ones (also, does the JSON serializer handle these cases?)
|
982
987
|
for target_type in _json_subclassable_scalar_types:
|
983
988
|
if not isinstance(o, target_type):
|
984
989
|
continue
|
@@ -1028,7 +1033,7 @@ def _finalize_collection(
|
|
1028
1033
|
|
1029
1034
|
def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
|
1030
1035
|
"""Recurse the template result, rendering any encountered templates, converting containers to non-lazy versions."""
|
1031
|
-
#
|
1036
|
+
# DTFIX5: add tests to ensure this method doesn't drift from allowed types
|
1032
1037
|
o_type = type(o)
|
1033
1038
|
|
1034
1039
|
# DTFIX-FUTURE: provide an optional way to check for trusted templates leaking out of templating (injected, but not passed through templar.template)
|
@@ -1045,7 +1050,7 @@ def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
|
|
1045
1050
|
if o_type in _FINALIZE_FAST_PATH_EXACT_ITERABLE_TYPES: # silently convert known sequence types to list
|
1046
1051
|
return _finalize_collection(o, mode, _finalize_list, list)
|
1047
1052
|
|
1048
|
-
if o_type in Marker.
|
1053
|
+
if o_type in Marker._concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
|
1049
1054
|
return TemplateContext.current().templar.marker_behavior.handle_marker(o)
|
1050
1055
|
|
1051
1056
|
if mode is not FinalizeMode.TOP_LEVEL: # unsupported type (do not raise)
|