Resolve misc DTFIX0/1 (#85247)

* complete DTFIX0 after eval

* sunder-prefix Marker.concrete_subclasses

* re-home Jinja plugin decorators public API

* low-hanging/already fixed DTFIX cases

Co-authored-by: Matt Clay <matt@mystile.com>

---------

Co-authored-by: Matt Clay <matt@mystile.com>
pull/85252/head
Matt Davis 6 months ago committed by GitHub
parent 8f2622c39f
commit df0b417f2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1050,7 +1050,7 @@ def _finalize_template_result(o: t.Any, mode: FinalizeMode) -> t.Any:
if o_type in _FINALIZE_FAST_PATH_EXACT_ITERABLE_TYPES: # silently convert known sequence types to list
return _finalize_collection(o, mode, _finalize_list, list)
if o_type in Marker.concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
if o_type in Marker._concrete_subclasses: # this early return assumes handle_marker follows our variable type rules
return TemplateContext.current().templar.marker_behavior.handle_marker(o)
if mode is not FinalizeMode.TOP_LEVEL: # unsupported type (do not raise)

@ -62,7 +62,7 @@ class Marker(StrictUndefined, Tripwire):
__slots__ = ('_marker_template_source',)
concrete_subclasses: t.ClassVar[set[type[Marker]]] = set()
_concrete_subclasses: t.ClassVar[set[type[Marker]]] = set()
def __init__(
self,
@ -136,7 +136,7 @@ class Marker(StrictUndefined, Tripwire):
def __init_subclass__(cls, **kwargs) -> None:
if not inspect.isabstract(cls):
_untaggable_types.add(cls)
cls.concrete_subclasses.add(cls)
cls._concrete_subclasses.add(cls)
@classmethod
def _init_class(cls):
@ -277,16 +277,12 @@ class VaultExceptionMarker(ExceptionMarker):
def get_first_marker_arg(args: c.Sequence, kwargs: dict[str, t.Any]) -> Marker | None:
"""Utility method to inspect plugin args and return the first `Marker` encountered, otherwise `None`."""
# DTFIX0: this may or may not need to be public API, move back to utils or once usage is wrapped in a decorator?
for arg in iter_marker_args(args, kwargs):
return arg
return None
# CAUTION: This function is exposed in public API as ansible.template.get_first_marker_arg.
return next(iter_marker_args(args, kwargs), None)
def iter_marker_args(args: c.Sequence, kwargs: dict[str, t.Any]) -> t.Generator[Marker]:
"""Utility method to iterate plugin args and yield any `Marker` encountered."""
# DTFIX0: this may or may not need to be public API, move back to utils or once usage is wrapped in a decorator?
for arg in itertools.chain(args, kwargs.values()):
if isinstance(arg, Marker):
yield arg
@ -301,7 +297,7 @@ class JinjaCallContext(NotifiableAccessContextBase):
_mask = True
def __init__(self, accept_lazy_markers: bool) -> None:
self._type_interest = frozenset() if accept_lazy_markers else frozenset(Marker.concrete_subclasses)
self._type_interest = frozenset() if accept_lazy_markers else frozenset(Marker._concrete_subclasses)
def _notify(self, o: Marker) -> t.NoReturn:
o.trip()

@ -43,7 +43,7 @@ _KNOWN_TYPES: t.Final[set[type]] = (
TemplateModule, # example: '{% import "importme.j2" as im %}{{ im | type_debug }}'
}
| set(PASS_THROUGH_SCALAR_VAR_TYPES)
| set(Marker.concrete_subclasses)
| set(Marker._concrete_subclasses)
)
"""
These types are known to the templating system.

@ -2,7 +2,7 @@ from __future__ import annotations
import typing as t
from ansible.plugins import accept_args_markers
from ansible.template import accept_args_markers
@accept_args_markers

@ -230,8 +230,6 @@ class _RawTaskResult(_BaseTaskResult):
class CallbackTaskResult(_BaseTaskResult):
"""Public contract of TaskResult """
# DTFIX1: find a better home for this since it's public API
@property
def _result(self) -> _c.MutableMapping[str, t.Any]:
"""Use the `result` property when supporting only ansible-core 2.19 or later."""

@ -4,10 +4,8 @@
from __future__ import annotations as _annotations
# from ansible.utils.display import Display as _Display
# DTFIX1: The pylint deprecated checker does not detect `Display().deprecated` calls, of which we have many.
#
#
# deprecated: description='deprecate ajson' core_version='2.23'
# _Display().deprecated(
# msg='The `ansible.parsing.ajson` module is deprecated.',

@ -1520,7 +1520,7 @@ class VaultHelper:
tags = AnsibleTagHelper.tags(ciphertext) # ciphertext has tags but value does not
elif value_type is EncryptedString:
ciphertext = value._ciphertext
elif value_type in _jinja_common.Marker.concrete_subclasses: # avoid wasteful raise/except of Marker when calling get_tag below
elif value_type in _jinja_common.Marker._concrete_subclasses: # avoid wasteful raise/except of Marker when calling get_tag below
ciphertext = None
elif vaulted_value := VaultedValue.get_tag(value):
ciphertext = vaulted_value.ciphertext

@ -45,9 +45,6 @@ class Taggable:
return ds
if isinstance(ds, str):
# DTFIX0: this allows each individual tag to be templated, but prevents the use of commas in templates, is that what we want?
# DTFIX0: this can return empty tags (including a list of nothing but empty tags), is that correct?
# DTFIX0: the original code seemed to attempt to preserve `ds` if there were no commas, but it never ran, what should it actually do?
return [AnsibleTagHelper.tag_copy(ds, item.strip()) for item in ds.split(',')]
raise AnsibleError('tags must be specified as a list', obj=ds)

@ -189,28 +189,3 @@ class AnsibleJinja2Plugin(AnsiblePlugin, metaclass=abc.ABCMeta):
@property
def j2_function(self) -> t.Callable:
return self._function
_TCallable = t.TypeVar('_TCallable', bound=t.Callable)
def accept_args_markers(plugin: _TCallable) -> _TCallable:
"""
A decorator to mark a Jinja plugin as capable of handling `Marker` values for its top-level arguments.
Non-decorated plugin invocation is skipped when a top-level argument is a `Marker`, with the first such value substituted as the plugin result.
This ensures that only plugins which understand `Marker` instances for top-level arguments will encounter them.
"""
plugin.accept_args_markers = True
return plugin
def accept_lazy_markers(plugin: _TCallable) -> _TCallable:
"""
A decorator to mark a Jinja plugin as capable of handling `Marker` values retrieved from lazy containers.
Non-decorated plugins will trigger a `MarkerError` exception when attempting to retrieve a `Marker` from a lazy container.
This ensures that only plugins which understand lazy retrieval of `Marker` instances will encounter them.
"""
plugin.accept_lazy_markers = True
return plugin

@ -280,8 +280,6 @@ class CallbackBase(AnsiblePlugin):
# that want to further modify the result, or use custom serialization
return abridged_result
# DTFIX0: Switch to stock json/yaml serializers here? We should always have a transformed plain-types result.
if result_format == 'json':
return json.dumps(abridged_result, cls=_fallback_to_str.Encoder, indent=indent, ensure_ascii=False, sort_keys=sort_keys)

@ -34,7 +34,7 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.common.yaml import yaml_load, yaml_load_all
from ansible.parsing.yaml.dumper import AnsibleDumper
from ansible.plugins import accept_args_markers, accept_lazy_markers
from ansible.template import accept_args_markers, accept_lazy_markers
from ansible._internal._templating._jinja_common import MarkerError, UndefinedMarker, validate_arg_type
from ansible.utils.display import Display
from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE
@ -824,5 +824,3 @@ class FilterModule(object):
'reject': wrapped_reject,
'rejectattr': wrapped_rejectattr,
}
# DTFIX1: 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
from ansible.errors import AnsibleError
from ansible.module_utils.common.text.converters import to_native, to_bytes
from ansible.plugins import accept_args_markers
from ansible._internal._templating._jinja_common import get_first_marker_arg, VaultExceptionMarker
from ansible._internal._templating._jinja_common import VaultExceptionMarker
from ansible._internal._datatag._tags import VaultedValue
from ansible.parsing.vault import is_encrypted, VaultSecret, VaultLib, VaultHelper
from ansible import template as _template
from ansible.utils.display import Display
display = Display()
@ -43,12 +43,12 @@ def do_vault(data, secret, salt=None, vault_id='filter_default', wrap_object=Fal
return vault
@accept_args_markers
@_template.accept_args_markers
def do_unvault(vault, secret, vault_id='filter_default', vaultid=None):
if isinstance(vault, VaultExceptionMarker):
vault = vault._disarm()
if (first_marker := get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
if (first_marker := _template.get_first_marker_arg((vault, secret, vault_id, vaultid), {})) is not None:
return first_marker
if not isinstance(secret, (str, bytes)):

@ -149,6 +149,7 @@ from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
from ansible._internal._templating import _jinja_common
from ansible._internal._templating import _jinja_plugins
from ansible import template as _template
from ansible.utils.path import unfrackpath
from ansible.utils.display import Display
from ansible.module_utils.datatag import native_type_name
@ -222,7 +223,7 @@ class LookupModule(LookupBase):
return total_search
def run(self, terms: list, variables=None, **kwargs):
if (first_marker := _jinja_common.get_first_marker_arg((), kwargs)) is not None:
if (first_marker := _template.get_first_marker_arg((), kwargs)) is not None:
first_marker.trip()
if _jinja_plugins._LookupContext.current().invoked_as_with:

@ -31,7 +31,7 @@ from ansible import errors
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
from ansible._internal._templating._jinja_common import Marker, UndefinedMarker
from ansible.module_utils.parsing.convert_bool import boolean
from ansible.plugins import accept_args_markers
from ansible.template import accept_args_markers
from ansible.parsing.vault import is_encrypted_file, VaultHelper, VaultLib
from ansible.utils.display import Display
from ansible.utils.version import SemanticVersion

@ -427,3 +427,31 @@ def is_trusted_as_template(value: object) -> bool:
This function should not be needed for production code, but may be useful in unit tests.
"""
return isinstance(value, _TRUSTABLE_TYPES) and _tags.TrustedAsTemplate.is_tagged_on(value)
_TCallable = _t.TypeVar('_TCallable', bound=_t.Callable)
def accept_args_markers(plugin: _TCallable) -> _TCallable:
"""
A decorator to mark a Jinja plugin as capable of handling `Marker` values for its top-level arguments.
Non-decorated plugin invocation is skipped when a top-level argument is a `Marker`, with the first such value substituted as the plugin result.
This ensures that only plugins which understand `Marker` instances for top-level arguments will encounter them.
"""
plugin.accept_args_markers = True
return plugin
def accept_lazy_markers(plugin: _TCallable) -> _TCallable:
"""
A decorator to mark a Jinja plugin as capable of handling `Marker` values retrieved from lazy containers.
Non-decorated plugins will trigger a `MarkerError` exception when attempting to retrieve a `Marker` from a lazy container.
This ensures that only plugins which understand lazy retrieval of `Marker` instances will encounter them.
"""
plugin.accept_lazy_markers = True
return plugin
get_first_marker_arg = _jinja_common.get_first_marker_arg

@ -44,15 +44,27 @@
- that: 1 == 1
- that: a_var == "one"
- that: '{{ a_var == "one" }}'
# DTFIX1: loops are not lazily templated, so this is not possible today, but could be in the future
# - that: |
# "hi mom" == '{{ "hi mom" }}'
- that:
- 1 == 1
- 2 == 2
- a_var == "one"
- '{{ a_var == "one" }}'
- name: arg splat loopable failure case
assert: '{{ item }}'
args:
quiet: yes
ignore_errors: true
loop:
- that: |
"hi mom" == '{{ "hi mom" }}' # not implemented; requires lazy loop templating
register: loop_fail
- name: check results from previous assert failures
assert:
that:
- loop_fail is failed
- loop_fail.results[0].msg is contains "untrusted"
- name: arg splat fail cases
assert: '{{ item }}'

Loading…
Cancel
Save