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
@@ -47,13 +47,13 @@ options:
|
|
47
47
|
- The key from input dictionary used to generate groups.
|
48
48
|
default_value:
|
49
49
|
description:
|
50
|
-
- The default value when the host variable's value is an empty string.
|
50
|
+
- The default value when the host variable's value is V(None) or an empty string.
|
51
51
|
- This option is mutually exclusive with O(keyed_groups[].trailing_separator).
|
52
52
|
type: str
|
53
53
|
version_added: '2.12'
|
54
54
|
trailing_separator:
|
55
55
|
description:
|
56
|
-
- Set this option to V(false) to omit the O(keyed_groups[].separator) after the host variable when the value is an empty string.
|
56
|
+
- Set this option to V(false) to omit the O(keyed_groups[].separator) after the host variable when the value is V(None) or an empty string.
|
57
57
|
- This option is mutually exclusive with O(keyed_groups[].default_value).
|
58
58
|
type: bool
|
59
59
|
default: true
|
ansible/plugins/filter/core.py
CHANGED
@@ -32,10 +32,10 @@ from ansible.module_utils.common.json import get_encoder, get_decoder
|
|
32
32
|
from ansible.module_utils.six import string_types, integer_types, text_type
|
33
33
|
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
34
34
|
from ansible.module_utils.common.collections import is_sequence
|
35
|
-
from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
|
36
35
|
from ansible.parsing.yaml.dumper import AnsibleDumper
|
37
|
-
from ansible.
|
36
|
+
from ansible.template import accept_args_markers, accept_lazy_markers
|
38
37
|
from ansible._internal._templating._jinja_common import MarkerError, UndefinedMarker, validate_arg_type
|
38
|
+
from ansible._internal._yaml import _loader as _yaml_loader
|
39
39
|
from ansible.utils.display import Display
|
40
40
|
from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
|
41
41
|
from ansible.utils.hashing import md5s, checksum_s
|
@@ -47,13 +47,13 @@ display = Display()
|
|
47
47
|
UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')
|
48
48
|
|
49
49
|
|
50
|
-
|
50
|
+
@accept_lazy_markers
|
51
|
+
def to_yaml(a, *_args, default_flow_style: bool | None = None, **kwargs) -> str:
|
51
52
|
"""Serialize input as terse flow-style YAML."""
|
52
|
-
|
53
|
-
|
54
|
-
return yaml.dump(a, Dumper=dumper, allow_unicode=True, default_flow_style=default_flow_style, **kwargs)
|
53
|
+
return yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kwargs)
|
55
54
|
|
56
55
|
|
56
|
+
@accept_lazy_markers
|
57
57
|
def to_nice_yaml(a, indent=4, *_args, default_flow_style=False, **kwargs) -> str:
|
58
58
|
"""Serialize input as verbose multi-line YAML."""
|
59
59
|
return to_yaml(a, indent=indent, default_flow_style=default_flow_style, **kwargs)
|
@@ -98,6 +98,7 @@ _valid_bool_false = {'no', 'off', 'false', '0'}
|
|
98
98
|
def to_bool(value: object) -> bool:
|
99
99
|
"""Convert well-known input values to a boolean value."""
|
100
100
|
value_to_check: object
|
101
|
+
|
101
102
|
if isinstance(value, str):
|
102
103
|
value_to_check = value.lower() # accept mixed case variants
|
103
104
|
elif isinstance(value, int): # bool is also an int
|
@@ -105,14 +106,17 @@ def to_bool(value: object) -> bool:
|
|
105
106
|
else:
|
106
107
|
value_to_check = value
|
107
108
|
|
108
|
-
|
109
|
-
|
109
|
+
try:
|
110
|
+
if value_to_check in _valid_bool_true:
|
111
|
+
return True
|
110
112
|
|
111
|
-
|
112
|
-
|
113
|
+
if value_to_check in _valid_bool_false:
|
114
|
+
return False
|
113
115
|
|
114
|
-
|
115
|
-
|
116
|
+
# if we're still here, the value is unsupported- always fire a deprecation warning
|
117
|
+
result = value_to_check == 1 # backwards compatibility with the old code which checked: value in ('yes', 'on', '1', 'true', 1)
|
118
|
+
except TypeError:
|
119
|
+
result = False
|
116
120
|
|
117
121
|
# NB: update the doc string to reflect reality once this fallback is removed
|
118
122
|
display.deprecated(
|
@@ -247,20 +251,24 @@ def regex_escape(string, re_type='python'):
|
|
247
251
|
|
248
252
|
|
249
253
|
def from_yaml(data):
|
250
|
-
if
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
return
|
254
|
+
if data is None:
|
255
|
+
return None
|
256
|
+
|
257
|
+
if isinstance(data, str):
|
258
|
+
return yaml.load(data, Loader=_yaml_loader.AnsibleInstrumentedLoader) # type: ignore[arg-type]
|
259
|
+
|
260
|
+
display.deprecated(f"The from_yaml filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
|
255
261
|
return data
|
256
262
|
|
257
263
|
|
258
264
|
def from_yaml_all(data):
|
259
|
-
if
|
260
|
-
#
|
261
|
-
|
262
|
-
|
263
|
-
return
|
265
|
+
if data is None:
|
266
|
+
return [] # backward compatibility; ensure consistent result between classic/native Jinja for None/empty string input
|
267
|
+
|
268
|
+
if isinstance(data, str):
|
269
|
+
return yaml.load_all(data, Loader=_yaml_loader.AnsibleInstrumentedLoader) # type: ignore[arg-type]
|
270
|
+
|
271
|
+
display.deprecated(f"The from_yaml_all filter ignored non-string input of type {native_type_name(data)!r}.", version='2.23', obj=data)
|
264
272
|
return data
|
265
273
|
|
266
274
|
|
@@ -338,7 +346,7 @@ def to_uuid(string, namespace=UUID_NAMESPACE_ANSIBLE):
|
|
338
346
|
@accept_args_markers
|
339
347
|
def mandatory(a: object, msg: str | None = None) -> object:
|
340
348
|
"""Make a variable mandatory."""
|
341
|
-
# DTFIX-
|
349
|
+
# DTFIX-FUTURE: deprecate this filter; there are much better ways via undef, etc...
|
342
350
|
# also remember to remove unit test checking for _undefined_name
|
343
351
|
if isinstance(a, UndefinedMarker):
|
344
352
|
if msg is not None:
|
@@ -654,7 +662,7 @@ def _cleansed_groupby(*args, **kwargs):
|
|
654
662
|
|
655
663
|
return res
|
656
664
|
|
657
|
-
# DTFIX-
|
665
|
+
# DTFIX-FUTURE: make these dumb wrappers more dynamic
|
658
666
|
|
659
667
|
|
660
668
|
@accept_args_markers
|
@@ -806,7 +814,6 @@ class FilterModule(object):
|
|
806
814
|
'groupby': _cleansed_groupby,
|
807
815
|
|
808
816
|
# Jinja builtins that need special arg handling
|
809
|
-
# DTFIX-RELEASE: document these now that they're overridden, or hide them so they don't show up as undocumented
|
810
817
|
'd': ansible_default, # replaces the implementation instead of wrapping it
|
811
818
|
'default': ansible_default, # replaces the implementation instead of wrapping it
|
812
819
|
'map': wrapped_map,
|
@@ -815,5 +822,3 @@ class FilterModule(object):
|
|
815
822
|
'reject': wrapped_reject,
|
816
823
|
'rejectattr': wrapped_rejectattr,
|
817
824
|
}
|
818
|
-
|
819
|
-
# DTFIX-RELEASE: document protomatter plugins, or hide them from ansible-doc/galaxy (not related to this code, but needed some place to put this comment)
|
@@ -4,10 +4,10 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
from ansible.errors import AnsibleError
|
6
6
|
from ansible.module_utils.common.text.converters import to_native, to_bytes
|
7
|
-
from ansible.
|
8
|
-
from ansible._internal._templating._jinja_common import get_first_marker_arg, VaultExceptionMarker
|
7
|
+
from ansible._internal._templating._jinja_common import VaultExceptionMarker
|
9
8
|
from ansible._internal._datatag._tags import VaultedValue
|
10
9
|
from ansible.parsing.vault import is_encrypted, VaultSecret, VaultLib, VaultHelper
|
10
|
+
from ansible import template as _template
|
11
11
|
from ansible.utils.display import Display
|
12
12
|
|
13
13
|
display = Display()
|
@@ -21,7 +21,11 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
|
|
21
21
|
raise TypeError(f"Can only vault strings, instead we got {type(data)}.")
|
22
22
|
|
23
23
|
if vaultid is not None:
|
24
|
-
display.deprecated(
|
24
|
+
display.deprecated(
|
25
|
+
msg="Use of undocumented `vaultid`.",
|
26
|
+
version="2.20",
|
27
|
+
help_text="Use `vault_id` instead.",
|
28
|
+
)
|
25
29
|
|
26
30
|
if vault_id == 'filter_default':
|
27
31
|
vault_id = vaultid
|
@@ -43,12 +47,12 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
|
|
43
47
|
return vault
|
44
48
|
|
45
49
|
|
46
|
-
@accept_args_markers
|
50
|
+
@_template.accept_args_markers
|
47
51
|
def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
|
48
52
|
if isinstance(vault, VaultExceptionMarker):
|
49
53
|
vault = vault._disarm()
|
50
54
|
|
51
|
-
if (first_marker := get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
|
55
|
+
if (first_marker := _template.get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
|
52
56
|
return first_marker
|
53
57
|
|
54
58
|
if not isinstance(secret, (str, bytes)):
|
@@ -58,7 +62,11 @@ def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
|
|
58
62
|
raise TypeError(f"Vault should be in the form of a string, instead we got {type(vault)}.")
|
59
63
|
|
60
64
|
if vaultid is not None:
|
61
|
-
display.deprecated(
|
65
|
+
display.deprecated(
|
66
|
+
msg="Use of undocumented `vaultid`.",
|
67
|
+
version="2.20",
|
68
|
+
help_text="Use `vault_id` instead.",
|
69
|
+
)
|
62
70
|
|
63
71
|
if vault_id == 'filter_default':
|
64
72
|
vault_id = vaultid
|
@@ -334,7 +334,6 @@ class Cacheable(_plugin_info.HasPluginInfo, _ConfigurablePlugin):
|
|
334
334
|
|
335
335
|
def _get_cache_prefix(self, path: str) -> str:
|
336
336
|
"""Return a predictable unique key based on the given path."""
|
337
|
-
# DTFIX-RELEASE: choose a better hashing approach
|
338
337
|
return 'k' + hashlib.sha256(f'{self.ansible_name}{path}'.encode(), usedforsecurity=False).hexdigest()[:6]
|
339
338
|
|
340
339
|
def clear_cache(self) -> None:
|
@@ -402,6 +401,8 @@ class Constructable(_BaseInventoryPlugin):
|
|
402
401
|
|
403
402
|
def _add_host_to_keyed_groups(self, keys, variables, host, strict=False, fetch_hostvars=True):
|
404
403
|
""" helper to create groups for plugins based on variable values and add the corresponding hosts to it"""
|
404
|
+
should_default_value = (None, '')
|
405
|
+
|
405
406
|
if keys and isinstance(keys, list):
|
406
407
|
for keyed in keys:
|
407
408
|
if keyed and isinstance(keyed, dict):
|
@@ -418,7 +419,9 @@ class Constructable(_BaseInventoryPlugin):
|
|
418
419
|
trailing_separator = keyed.get('trailing_separator')
|
419
420
|
if trailing_separator is not None and default_value_name is not None:
|
420
421
|
raise AnsibleParserError("parameters are mutually exclusive for keyed groups: default_value|trailing_separator")
|
421
|
-
|
422
|
+
|
423
|
+
use_default = key in should_default_value and default_value_name is not None
|
424
|
+
if key or use_default:
|
422
425
|
prefix = keyed.get('prefix', '')
|
423
426
|
sep = keyed.get('separator', '_')
|
424
427
|
raw_parent_name = keyed.get('parent_group', None)
|
@@ -434,23 +437,21 @@ class Constructable(_BaseInventoryPlugin):
|
|
434
437
|
continue
|
435
438
|
|
436
439
|
new_raw_group_names = []
|
437
|
-
if
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
else:
|
442
|
-
new_raw_group_names.append(key)
|
440
|
+
if use_default:
|
441
|
+
new_raw_group_names.append(default_value_name)
|
442
|
+
elif isinstance(key, string_types):
|
443
|
+
new_raw_group_names.append(key)
|
443
444
|
elif isinstance(key, list):
|
444
445
|
for name in key:
|
445
446
|
# if list item is empty, 'default_value' will be used as group name
|
446
|
-
if name
|
447
|
+
if name in should_default_value and default_value_name is not None:
|
447
448
|
new_raw_group_names.append(default_value_name)
|
448
449
|
else:
|
449
450
|
new_raw_group_names.append(name)
|
450
451
|
elif isinstance(key, Mapping):
|
451
452
|
for (gname, gval) in key.items():
|
452
453
|
bare_name = '%s%s%s' % (gname, sep, gval)
|
453
|
-
if gval
|
454
|
+
if gval in should_default_value:
|
454
455
|
# key's value is empty
|
455
456
|
if default_value_name is not None:
|
456
457
|
bare_name = '%s%s%s' % (gname, sep, default_value_name)
|
@@ -367,7 +367,7 @@ def run_command(path: str, options: list[str], origin: Origin) -> tuple[str, str
|
|
367
367
|
if stderr and not stderr.endswith('\n'):
|
368
368
|
stderr += '\n'
|
369
369
|
|
370
|
-
# DTFIX-
|
370
|
+
# DTFIX-FUTURE: another use case for the "not quite help text, definitely not message" diagnostic output on errors
|
371
371
|
stderr_help_text = f'Standard error from inventory script:\n{stderr}' if stderr.strip() else None
|
372
372
|
|
373
373
|
if sp.returncode != 0:
|
ansible/plugins/list.py
CHANGED
@@ -4,6 +4,7 @@
|
|
4
4
|
from __future__ import annotations
|
5
5
|
|
6
6
|
|
7
|
+
import dataclasses
|
7
8
|
import os
|
8
9
|
|
9
10
|
from ansible import context
|
@@ -14,6 +15,7 @@ from ansible.module_utils.common.text.converters import to_native, to_bytes
|
|
14
15
|
from ansible.plugins import loader
|
15
16
|
from ansible.utils.display import Display
|
16
17
|
from ansible.utils.collection_loader._collection_finder import _get_collection_path
|
18
|
+
from ansible._internal._templating._jinja_plugins import get_jinja_builtin_plugin_descriptions
|
17
19
|
|
18
20
|
display = Display()
|
19
21
|
|
@@ -25,6 +27,20 @@ IGNORE = {
|
|
25
27
|
}
|
26
28
|
|
27
29
|
|
30
|
+
@dataclasses.dataclass(kw_only=True, frozen=True, slots=True)
|
31
|
+
class _PluginDocMetadata:
|
32
|
+
"""Information about a plugin."""
|
33
|
+
|
34
|
+
name: str
|
35
|
+
"""The fully qualified name of the plugin."""
|
36
|
+
path: bytes | None = None
|
37
|
+
"""The path to the plugin file, or None if not available."""
|
38
|
+
plugin_obj: object | None = None
|
39
|
+
"""The loaded plugin object, or None if not loaded."""
|
40
|
+
jinja_builtin_short_description: str | None = None
|
41
|
+
"""The short description of the plugin if it is a Jinja builtin, otherwise None."""
|
42
|
+
|
43
|
+
|
28
44
|
def get_composite_name(collection, name, path, depth):
|
29
45
|
resolved_collection = collection
|
30
46
|
if '.' not in name:
|
@@ -42,7 +58,7 @@ def get_composite_name(collection, name, path, depth):
|
|
42
58
|
return '.'.join(composite)
|
43
59
|
|
44
60
|
|
45
|
-
def _list_plugins_from_paths(ptype, dirs, collection, depth=0):
|
61
|
+
def _list_plugins_from_paths(ptype, dirs, collection, depth=0, docs=False):
|
46
62
|
# TODO: update to use importlib.resources
|
47
63
|
|
48
64
|
plugins = {}
|
@@ -77,14 +93,15 @@ def _list_plugins_from_paths(ptype, dirs, collection, depth=0):
|
|
77
93
|
continue
|
78
94
|
|
79
95
|
# actually recurse dirs
|
80
|
-
plugins.update(_list_plugins_from_paths(ptype, [to_native(full_path)], collection, depth=depth + 1))
|
96
|
+
plugins.update(_list_plugins_from_paths(ptype, [to_native(full_path)], collection, depth=depth + 1, docs=docs))
|
81
97
|
else:
|
82
98
|
if any([
|
83
99
|
plugin in C.IGNORE_FILES, # general files to ignore
|
84
100
|
to_native(b_ext) in C.REJECT_EXTS, # general extensions to ignore
|
85
|
-
b_ext in (b'.yml', b'.yaml', b'.json'), # ignore docs files
|
101
|
+
b_ext in (b'.yml', b'.yaml', b'.json'), # ignore docs files
|
86
102
|
plugin in IGNORE.get(bkey, ()), # plugin in reject list
|
87
103
|
os.path.islink(full_path), # skip aliases, author should document in 'aliases' field
|
104
|
+
not docs and b_ext in (b''), # ignore no ext when looking for docs files
|
88
105
|
]):
|
89
106
|
continue
|
90
107
|
|
@@ -116,21 +133,37 @@ def _list_j2_plugins_from_file(collection, plugin_path, ptype, plugin_name):
|
|
116
133
|
return file_plugins
|
117
134
|
|
118
135
|
|
119
|
-
def list_collection_plugins(ptype, collections, search_paths=None):
|
136
|
+
def list_collection_plugins(ptype: str, collections: dict[str, bytes], search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
|
137
|
+
# Kept for backwards compatibility.
|
138
|
+
return {
|
139
|
+
name: (info.path, info.plugin_obj)
|
140
|
+
for name, info in _list_collection_plugins_with_info(ptype, collections).items()
|
141
|
+
}
|
142
|
+
|
143
|
+
|
144
|
+
def _list_collection_plugins_with_info(
|
145
|
+
ptype: str,
|
146
|
+
collections: dict[str, bytes],
|
147
|
+
) -> dict[str, _PluginDocMetadata]:
|
120
148
|
# TODO: update to use importlib.resources
|
121
149
|
|
122
|
-
# starts at {plugin_name: filepath, ...}, but changes at the end
|
123
|
-
plugins = {}
|
124
150
|
try:
|
125
151
|
ploader = getattr(loader, '{0}_loader'.format(ptype))
|
126
152
|
except AttributeError:
|
127
153
|
raise AnsibleError(f"Cannot list plugins, incorrect plugin type {ptype!r} supplied.") from None
|
128
154
|
|
155
|
+
builtin_jinja_plugins = {}
|
156
|
+
plugin_paths = {}
|
157
|
+
|
129
158
|
# get plugins for each collection
|
130
|
-
for collection in collections.
|
159
|
+
for collection, path in collections.items():
|
131
160
|
if collection == 'ansible.builtin':
|
132
161
|
# dirs from ansible install, but not configured paths
|
133
162
|
dirs = [d.path for d in ploader._get_paths_with_context() if d.internal]
|
163
|
+
|
164
|
+
if ptype in ('filter', 'test'):
|
165
|
+
builtin_jinja_plugins = get_jinja_builtin_plugin_descriptions(ptype)
|
166
|
+
|
134
167
|
elif collection == 'ansible.legacy':
|
135
168
|
# configured paths + search paths (should include basedirs/-M)
|
136
169
|
dirs = [d.path for d in ploader._get_paths_with_context() if not d.internal]
|
@@ -139,7 +172,7 @@ def list_collection_plugins(ptype, collections, search_paths=None):
|
|
139
172
|
else:
|
140
173
|
# search path in this case is for locating collection itselfA
|
141
174
|
b_ptype = to_bytes(C.COLLECTION_PTYPE_COMPAT.get(ptype, ptype))
|
142
|
-
dirs = [to_native(os.path.join(
|
175
|
+
dirs = [to_native(os.path.join(path, b'plugins', b_ptype))]
|
143
176
|
# acr = AnsibleCollectionRef.try_parse_fqcr(collection, ptype)
|
144
177
|
# if acr:
|
145
178
|
# dirs = acr.subdirs
|
@@ -147,30 +180,51 @@ def list_collection_plugins(ptype, collections, search_paths=None):
|
|
147
180
|
|
148
181
|
# raise Exception('bad acr for %s, %s' % (collection, ptype))
|
149
182
|
|
150
|
-
|
183
|
+
plugin_paths.update(_list_plugins_from_paths(ptype, dirs, collection, docs=True))
|
151
184
|
|
152
|
-
|
185
|
+
plugins = {}
|
153
186
|
if ptype in ('module',):
|
154
187
|
# no 'invalid' tests for modules
|
155
|
-
for plugin in
|
156
|
-
plugins[plugin] = (
|
188
|
+
for plugin, plugin_path in plugin_paths.items():
|
189
|
+
plugins[plugin] = _PluginDocMetadata(name=plugin, path=plugin_path)
|
157
190
|
else:
|
158
191
|
# detect invalid plugin candidates AND add loaded object to return data
|
159
|
-
for plugin in
|
192
|
+
for plugin, plugin_path in plugin_paths.items():
|
160
193
|
pobj = None
|
161
194
|
try:
|
162
195
|
pobj = ploader.get(plugin, class_only=True)
|
163
196
|
except Exception as e:
|
164
|
-
display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype,
|
197
|
+
display.vvv("The '{0}' {1} plugin could not be loaded from '{2}': {3}".format(plugin, ptype, plugin_path, to_native(e)))
|
165
198
|
|
166
|
-
|
167
|
-
|
199
|
+
plugins[plugin] = _PluginDocMetadata(
|
200
|
+
name=plugin,
|
201
|
+
path=plugin_path,
|
202
|
+
plugin_obj=pobj,
|
203
|
+
jinja_builtin_short_description=builtin_jinja_plugins.get(plugin),
|
204
|
+
)
|
205
|
+
|
206
|
+
# Add in any builtin Jinja2 plugins that have not been shadowed in Ansible.
|
207
|
+
plugins.update(
|
208
|
+
(plugin_name, _PluginDocMetadata(name=plugin_name, jinja_builtin_short_description=plugin_description))
|
209
|
+
for plugin_name, plugin_description in builtin_jinja_plugins.items() if plugin_name not in plugins
|
210
|
+
)
|
168
211
|
|
169
|
-
# {plugin_name: (filepath, class), ...}
|
170
212
|
return plugins
|
171
213
|
|
172
214
|
|
173
|
-
def list_plugins(ptype, collections=None, search_paths=None):
|
215
|
+
def list_plugins(ptype: str, collections: list[str] | None = None, search_paths: list[str] | None = None) -> dict[str, tuple[bytes, object | None]]:
|
216
|
+
# Kept for backwards compatibility.
|
217
|
+
return {
|
218
|
+
name: (info.path, info.plugin_obj)
|
219
|
+
for name, info in _list_plugins_with_info(ptype, collections, search_paths).items()
|
220
|
+
}
|
221
|
+
|
222
|
+
|
223
|
+
def _list_plugins_with_info(
|
224
|
+
ptype: str,
|
225
|
+
collections: list[str] = None,
|
226
|
+
search_paths: list[str] | None = None,
|
227
|
+
) -> dict[str, _PluginDocMetadata]:
|
174
228
|
if isinstance(collections, str):
|
175
229
|
collections = [collections]
|
176
230
|
|
@@ -195,7 +249,7 @@ def list_plugins(ptype, collections=None, search_paths=None):
|
|
195
249
|
raise AnsibleError(f"Cannot use supplied collection {collection!r}.") from ex
|
196
250
|
|
197
251
|
if plugin_collections:
|
198
|
-
plugins.update(
|
252
|
+
plugins.update(_list_collection_plugins_with_info(ptype, plugin_collections))
|
199
253
|
|
200
254
|
return plugins
|
201
255
|
|
ansible/plugins/loader.py
CHANGED
@@ -26,6 +26,7 @@ from ansible import __version__ as ansible_version
|
|
26
26
|
from ansible import _internal, constants as C
|
27
27
|
from ansible.errors import AnsibleError, AnsiblePluginCircularRedirect, AnsiblePluginRemovedError, AnsibleCollectionUnsupportedVersionError
|
28
28
|
from ansible.module_utils.common.text.converters import to_bytes, to_text, to_native
|
29
|
+
from ansible.module_utils.datatag import deprecator_from_collection_name
|
29
30
|
from ansible.module_utils.six import string_types
|
30
31
|
from ansible.parsing.yaml.loader import AnsibleLoader
|
31
32
|
from ansible._internal._yaml._loader import AnsibleInstrumentedLoader
|
@@ -40,7 +41,6 @@ from . import _AnsiblePluginInfoMixin
|
|
40
41
|
from .filter import AnsibleJinja2Filter
|
41
42
|
from .test import AnsibleJinja2Test
|
42
43
|
from .._internal._plugins import _cache
|
43
|
-
from ..module_utils.common.messages import PluginInfo
|
44
44
|
|
45
45
|
# TODO: take the packaging dep, or vendor SpecifierSet?
|
46
46
|
|
@@ -202,7 +202,7 @@ class PluginLoadContext(object):
|
|
202
202
|
msg=warning_text,
|
203
203
|
date=removal_date,
|
204
204
|
version=removal_version,
|
205
|
-
deprecator=
|
205
|
+
deprecator=deprecator_from_collection_name(collection_name),
|
206
206
|
)
|
207
207
|
|
208
208
|
self.deprecated = True
|
@@ -611,7 +611,7 @@ class PluginLoader:
|
|
611
611
|
version=removal_version,
|
612
612
|
date=removal_date,
|
613
613
|
removed=True,
|
614
|
-
deprecator=
|
614
|
+
deprecator=deprecator_from_collection_name(acr.collection),
|
615
615
|
)
|
616
616
|
plugin_load_context.date = removal_date
|
617
617
|
plugin_load_context.version = removal_version
|
@@ -795,7 +795,7 @@ class PluginLoader:
|
|
795
795
|
except Exception as ex:
|
796
796
|
plugin_load_context.raw_error_list.append(ex)
|
797
797
|
|
798
|
-
# DTFIX-
|
798
|
+
# DTFIX-FUTURE: can we deprecate/remove these stringified versions?
|
799
799
|
if isinstance(ex, ImportError):
|
800
800
|
plugin_load_context.import_error_list.append(ex)
|
801
801
|
else:
|
@@ -955,7 +955,7 @@ class PluginLoader:
|
|
955
955
|
redirected_names: list[str] | None = None,
|
956
956
|
resolved: str | None = None,
|
957
957
|
) -> None:
|
958
|
-
# DTFIX-
|
958
|
+
# DTFIX-FUTURE: clean this up- standardize types, document, split/remove redundant bits
|
959
959
|
|
960
960
|
# set extra info on the module, in case we want it later
|
961
961
|
obj._original_path = path
|
@@ -1396,7 +1396,7 @@ class Jinja2Loader(PluginLoader):
|
|
1396
1396
|
msg=warning_text,
|
1397
1397
|
version=removal_version,
|
1398
1398
|
date=removal_date,
|
1399
|
-
deprecator=
|
1399
|
+
deprecator=deprecator_from_collection_name(acr.collection),
|
1400
1400
|
)
|
1401
1401
|
|
1402
1402
|
# check removal
|
@@ -1412,7 +1412,7 @@ class Jinja2Loader(PluginLoader):
|
|
1412
1412
|
version=removal_version,
|
1413
1413
|
date=removal_date,
|
1414
1414
|
removed=True,
|
1415
|
-
deprecator=
|
1415
|
+
deprecator=deprecator_from_collection_name(acr.collection),
|
1416
1416
|
)
|
1417
1417
|
|
1418
1418
|
raise AnsiblePluginRemovedError(exc_msg)
|
@@ -103,76 +103,26 @@ RETURN = """
|
|
103
103
|
elements: str
|
104
104
|
"""
|
105
105
|
|
106
|
-
import codecs
|
107
106
|
import csv
|
108
107
|
|
109
108
|
from collections.abc import MutableSequence
|
110
109
|
|
111
|
-
from ansible.errors import AnsibleError
|
110
|
+
from ansible.errors import AnsibleError
|
112
111
|
from ansible.parsing.splitter import parse_kv
|
113
112
|
from ansible.plugins.lookup import LookupBase
|
114
|
-
from ansible.module_utils.six import PY2
|
115
|
-
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
116
|
-
|
117
|
-
|
118
|
-
class CSVRecoder:
|
119
|
-
"""
|
120
|
-
Iterator that reads an encoded stream and encodes the input to UTF-8
|
121
|
-
"""
|
122
|
-
def __init__(self, f, encoding='utf-8'):
|
123
|
-
self.reader = codecs.getreader(encoding)(f)
|
124
|
-
|
125
|
-
def __iter__(self):
|
126
|
-
return self
|
127
|
-
|
128
|
-
def __next__(self):
|
129
|
-
return next(self.reader).encode("utf-8")
|
130
|
-
|
131
|
-
next = __next__ # For Python 2
|
132
|
-
|
133
|
-
|
134
|
-
class CSVReader:
|
135
|
-
"""
|
136
|
-
A CSV reader which will iterate over lines in the CSV file "f",
|
137
|
-
which is encoded in the given encoding.
|
138
|
-
"""
|
139
|
-
|
140
|
-
def __init__(self, f, dialect=csv.excel, encoding='utf-8', **kwds):
|
141
|
-
if PY2:
|
142
|
-
f = CSVRecoder(f, encoding)
|
143
|
-
else:
|
144
|
-
f = codecs.getreader(encoding)(f)
|
145
|
-
|
146
|
-
self.reader = csv.reader(f, dialect=dialect, **kwds)
|
147
|
-
|
148
|
-
def __next__(self):
|
149
|
-
row = next(self.reader)
|
150
|
-
return [to_text(s) for s in row]
|
151
|
-
|
152
|
-
next = __next__ # For Python 2
|
153
|
-
|
154
|
-
def __iter__(self):
|
155
|
-
return self
|
156
113
|
|
157
114
|
|
158
115
|
class LookupModule(LookupBase):
|
159
116
|
|
160
117
|
def read_csv(self, filename, key, delimiter, encoding='utf-8', dflt=None, col=1, keycol=0):
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
creader = CSVReader(f, delimiter=to_native(delimiter), encoding=encoding)
|
165
|
-
|
166
|
-
for row in creader:
|
167
|
-
if len(row) and row[keycol] == key:
|
118
|
+
with open(filename, encoding=encoding) as f:
|
119
|
+
for row in csv.reader(f, dialect=csv.excel, delimiter=delimiter):
|
120
|
+
if row and row[keycol] == key:
|
168
121
|
return row[col]
|
169
|
-
except Exception as e:
|
170
|
-
raise AnsibleError("csvfile: %s" % to_native(e))
|
171
122
|
|
172
123
|
return dflt
|
173
124
|
|
174
125
|
def run(self, terms, variables=None, **kwargs):
|
175
|
-
|
176
126
|
ret = []
|
177
127
|
|
178
128
|
self.set_options(var_options=variables, direct=kwargs)
|
@@ -192,23 +142,19 @@ class LookupModule(LookupBase):
|
|
192
142
|
key = kv['_raw_params']
|
193
143
|
|
194
144
|
# parameters override per term using k/v
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
raise AnsibleAssertionError('%s is not a valid option' % name)
|
202
|
-
|
203
|
-
self._deprecate_inline_kv()
|
204
|
-
self.set_option(name, value)
|
205
|
-
reset_params = True
|
145
|
+
reset_params = False
|
146
|
+
for name, value in kv.items():
|
147
|
+
if name == '_raw_params':
|
148
|
+
continue
|
149
|
+
if name not in paramvals:
|
150
|
+
raise ValueError(f'{name!r} is not a valid option')
|
206
151
|
|
207
|
-
|
208
|
-
|
152
|
+
self._deprecate_inline_kv()
|
153
|
+
self.set_option(name, value)
|
154
|
+
reset_params = True
|
209
155
|
|
210
|
-
|
211
|
-
|
156
|
+
if reset_params:
|
157
|
+
paramvals = self.get_options()
|
212
158
|
|
213
159
|
# default is just placeholder for real tab
|
214
160
|
if paramvals['delimiter'] == 'TAB':
|
@@ -218,8 +164,7 @@ class LookupModule(LookupBase):
|
|
218
164
|
var = self.read_csv(lookupfile, key, paramvals['delimiter'], paramvals['encoding'], paramvals['default'], paramvals['col'], paramvals['keycol'])
|
219
165
|
if var is not None:
|
220
166
|
if isinstance(var, MutableSequence):
|
221
|
-
|
222
|
-
ret.append(v)
|
167
|
+
ret.extend(var)
|
223
168
|
else:
|
224
169
|
ret.append(var)
|
225
170
|
|
@@ -149,6 +149,7 @@ from ansible.errors import AnsibleError
|
|
149
149
|
from ansible.plugins.lookup import LookupBase
|
150
150
|
from ansible._internal._templating import _jinja_common
|
151
151
|
from ansible._internal._templating import _jinja_plugins
|
152
|
+
from ansible import template as _template
|
152
153
|
from ansible.utils.path import unfrackpath
|
153
154
|
from ansible.utils.display import Display
|
154
155
|
from ansible.module_utils.datatag import native_type_name
|
@@ -222,7 +223,7 @@ class LookupModule(LookupBase):
|
|
222
223
|
return total_search
|
223
224
|
|
224
225
|
def run(self, terms: list, variables=None, **kwargs):
|
225
|
-
if (first_marker :=
|
226
|
+
if (first_marker := _template.get_first_marker_arg((), kwargs)) is not None:
|
226
227
|
first_marker.trip()
|
227
228
|
|
228
229
|
if _jinja_plugins._LookupContext.current().invoked_as_with:
|