From 00fe38215c839a8d7d02e9554b68c9f82f52ceea Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 12 Aug 2025 16:57:19 -0700 Subject: [PATCH] Fix slicing of tuples in templating (#85608) * Fix slicing of tuples in templating * Improve lazy container test coverage --- changelogs/fragments/template-tuple-fix.yml | 2 ++ .../_internal/_templating/_lazy_containers.py | 10 +++---- .../templating/test_lazy_containers.py | 27 ++++++++++++++++++- 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 changelogs/fragments/template-tuple-fix.yml diff --git a/changelogs/fragments/template-tuple-fix.yml b/changelogs/fragments/template-tuple-fix.yml new file mode 100644 index 00000000000..e82760976d8 --- /dev/null +++ b/changelogs/fragments/template-tuple-fix.yml @@ -0,0 +1,2 @@ +bugfixes: + - templating - Fix slicing of tuples in templating (https://github.com/ansible/ansible/issues/85606). diff --git a/lib/ansible/_internal/_templating/_lazy_containers.py b/lib/ansible/_internal/_templating/_lazy_containers.py index 1d19e88c645..0f4b41bde9e 100644 --- a/lib/ansible/_internal/_templating/_lazy_containers.py +++ b/lib/ansible/_internal/_templating/_lazy_containers.py @@ -229,8 +229,6 @@ class _AnsibleLazyTemplateDict(_AnsibleTaggedDict, _AnsibleLazyTemplateMixin): __slots__ = _AnsibleLazyTemplateMixin._SLOTS def __init__(self, contents: t.Iterable | _LazyValueSource, /, **kwargs) -> None: - _AnsibleLazyTemplateMixin.__init__(self, contents) - if isinstance(contents, _AnsibleLazyTemplateDict): super().__init__(dict.items(contents), **kwargs) elif isinstance(contents, _LazyValueSource): @@ -238,6 +236,8 @@ class _AnsibleLazyTemplateDict(_AnsibleTaggedDict, _AnsibleLazyTemplateMixin): else: raise UnsupportedConstructionMethodError() + _AnsibleLazyTemplateMixin.__init__(self, contents) + def get(self, key: t.Any, default: t.Any = None) -> t.Any: if (value := super().get(key, _NoKeySentinel)) is _NoKeySentinel: return default @@ -372,8 +372,6 @@ class _AnsibleLazyTemplateList(_AnsibleTaggedList, _AnsibleLazyTemplateMixin): __slots__ = _AnsibleLazyTemplateMixin._SLOTS def __init__(self, contents: t.Iterable | _LazyValueSource, /) -> None: - _AnsibleLazyTemplateMixin.__init__(self, contents) - if isinstance(contents, _AnsibleLazyTemplateList): super().__init__(list.__iter__(contents)) elif isinstance(contents, _LazyValueSource): @@ -381,6 +379,8 @@ class _AnsibleLazyTemplateList(_AnsibleTaggedList, _AnsibleLazyTemplateMixin): else: raise UnsupportedConstructionMethodError() + _AnsibleLazyTemplateMixin.__init__(self, contents) + def __getitem__(self, key: t.SupportsIndex | slice, /) -> t.Any: if type(key) is slice: # pylint: disable=unidiomatic-typecheck return _AnsibleLazyTemplateList(_LazyValueSource(source=super().__getitem__(key), templar=self._templar, lazy_options=self._lazy_options)) @@ -567,7 +567,7 @@ class _AnsibleLazyAccessTuple(_AnsibleTaggedTuple, _AnsibleLazyTemplateMixin): def __getitem__(self, key: t.SupportsIndex | slice, /) -> t.Any: if type(key) is slice: # pylint: disable=unidiomatic-typecheck - return _AnsibleLazyAccessTuple(super().__getitem__(key)) + return _AnsibleLazyAccessTuple(_LazyValueSource(source=super().__getitem__(key), templar=self._templar, lazy_options=self._lazy_options)) value = super().__getitem__(key) diff --git a/test/units/_internal/templating/test_lazy_containers.py b/test/units/_internal/templating/test_lazy_containers.py index 87d4f11e9ff..ef0004b7279 100644 --- a/test/units/_internal/templating/test_lazy_containers.py +++ b/test/units/_internal/templating/test_lazy_containers.py @@ -16,7 +16,8 @@ from ansible._internal._templating._jinja_common import CapturedExceptionMarker, from ansible._internal._datatag._tags import Origin, TrustedAsTemplate from ansible._internal._templating._utils import TemplateContext, LazyOptions from ansible._internal._templating._engine import TemplateEngine, TemplateOptions -from ansible._internal._templating._lazy_containers import _AnsibleLazyTemplateMixin, _AnsibleLazyTemplateList, _AnsibleLazyTemplateDict, _LazyValue +from ansible._internal._templating._lazy_containers import _AnsibleLazyTemplateMixin, _AnsibleLazyTemplateList, _AnsibleLazyTemplateDict, _LazyValue, \ + _AnsibleLazyAccessTuple, UnsupportedConstructionMethodError from ansible.module_utils._internal._datatag import AnsibleTaggedObject from ...module_utils.datatag.test_datatag import ExampleSingletonTag @@ -299,6 +300,7 @@ def test_lazy_list_adapter_operators(template, variables, expected) -> None: ('type(d1)(d1)', dict(a=_LazyValue(1), c=_LazyValue(1)), _AnsibleLazyTemplateDict), # _AnsibleLazyTemplateDict.__init__ copy ('l1.copy()', [_LazyValue(1)], _AnsibleLazyTemplateList), # _AnsibleLazyTemplateList.copy ('type(l1)(l1)', [_LazyValue(1)], _AnsibleLazyTemplateList), # _AnsibleLazyTemplateList.__init__ copy + ('type(t1)(t1)', (1,), _AnsibleLazyAccessTuple), ('copy.copy(l1)', [_LazyValue(1)], _AnsibleLazyTemplateList), ('copy.copy(d1)', dict(a=_LazyValue(1), c=_LazyValue(1)), _AnsibleLazyTemplateDict), ('copy.deepcopy(l1)', [_LazyValue(1)], _AnsibleLazyTemplateList), # __AnsibleLazyTemplateList.__deepcopy__ @@ -308,6 +310,7 @@ def test_lazy_list_adapter_operators(template, variables, expected) -> None: ('list(reversed(l1))', [1], list), # _AnsibleLazyTemplateList.__reversed__ ('list(reversed(d1))', ['c', 'a'], list), # dict.__reversed__ - keys only ('l1[:]', [_LazyValue(1)], _AnsibleLazyTemplateList), # __getitem__ (slice) + ('t1[:]', (1,), _AnsibleLazyAccessTuple), # __getitem__ (slice) ('d1["a"]', 1, int), # __getitem__ ('d1.get("a")', 1, int), # get ('l1[0]', 1, int), # __getitem__ @@ -366,6 +369,9 @@ def test_lazy_list_adapter_operators(template, variables, expected) -> None: ('tuple() + l1', 'can only concatenate tuple (not "_AnsibleLazyTemplateList") to tuple', TypeError), # __radd__ (relies on tuple.__add__) ('tuple() + d1', 'can only concatenate tuple (not "_AnsibleLazyTemplateDict") to tuple', TypeError), # relies on tuple.__add__ ('l1.pop(42)', "pop index out of range", IndexError), + ('type(l1)([])', 'Direct construction of lazy containers is not supported.', UnsupportedConstructionMethodError), + ('type(t1)([])', 'Direct construction of lazy containers is not supported.', UnsupportedConstructionMethodError), + ('type(d1)({})', 'Direct construction of lazy containers is not supported.', UnsupportedConstructionMethodError), ], ids=str) def test_lazy_container_operators(expression: str, expected_value: t.Any, expected_type: type) -> None: """ @@ -387,6 +393,7 @@ def test_lazy_container_operators(expression: str, expected_value: t.Any, expect l1x=[TRUST.tag('{{ one }}')], l2=[TRUST.tag('{{ two }}')], l2f=l2f, + t1=(TRUST.tag('{{ one }}'),), d1=dict(a=TRUST.tag('{{ one }}'), c=TRUST.tag('{{ one }}')), d1x=dict(a=TRUST.tag('{{ one }}'), c=TRUST.tag('{{ one }}')), d2=dict(b=TRUST.tag('{{ two }}'), c=TRUST.tag('{{ two }}')), @@ -436,6 +443,15 @@ def test_lazy_container_operators(expression: str, expected_value: t.Any, expect actual_list_types: list[type] = [type(value) for value in list.__iter__(result)] assert actual_list_types == expected_list_types + elif issubclass(expected_type, tuple): + assert isinstance(result, tuple) # redundant, but assists mypy in understanding the type + + expected_tuple_types = [type(value) for value in expected_value] + expected_result = expected_value + + actual_tuple_types: list[type] = [type(value) for value in tuple.__iter__(result)] + + assert actual_tuple_types == expected_tuple_types elif issubclass(expected_type, dict): assert isinstance(result, dict) # redundant, but assists mypy in understanding the type @@ -867,3 +883,12 @@ def test_lazy_copies(value: list | dict, deep: bool, template_context: TemplateC assert all((base_type.__getitem__(copied, key) is base_type.__getitem__(original, key)) != deep for key in keys) assert (copied._templar is original._templar) != deep assert (copied._lazy_options is original._lazy_options) != deep + + +def test_lazy_template_mixin_init() -> None: + """ + Verify `_AnsibleLazyTemplateMixin` checks the __init__ arg type. + This code path is not normally reachable, since types which use it perform the same check before invoking the mixin. + """ + with pytest.raises(UnsupportedConstructionMethodError): + _AnsibleLazyTemplateMixin(t.cast(t.Any, None))