diff --git a/changelogs/fragments/when-eval-native-py310.yml b/changelogs/fragments/when-eval-native-py310.yml new file mode 100644 index 00000000000..f90b948add2 --- /dev/null +++ b/changelogs/fragments/when-eval-native-py310.yml @@ -0,0 +1,2 @@ +bugfixes: + - Fix ``when`` evaluation on Native Jinja and Python 3.10. diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py index 5653437941c..08219e43460 100644 --- a/lib/ansible/playbook/conditional.py +++ b/lib/ansible/playbook/conditional.py @@ -28,7 +28,7 @@ from jinja2.exceptions import UndefinedError from ansible import constants as C from ansible.errors import AnsibleError, AnsibleUndefinedVariable from ansible.module_utils.six import text_type -from ansible.module_utils._text import to_native +from ansible.module_utils._text import to_native, to_text from ansible.playbook.attribute import FieldAttribute from ansible.utils.display import Display @@ -192,8 +192,15 @@ class Conditional: raise AnsibleError("Invalid conditional detected: %s" % to_native(e)) # and finally we generate and template the presented string and look at the resulting string + # NOTE The spaces around True and False are intentional to short-circuit safe_eval and avoid + # its expensive calls. presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional - val = templar.template(presented, disable_lookups=disable_lookups).strip() + # NOTE Convert the result to text to account for both native and non-native jinja. + # NOTE The templated result of `presented` is string on native jinja as well prior to Python 3.10. + # ast.literal_eval on Python 3.10 removes leading whitespaces so " True " becomes bool True + # as opposed to Python 3.9 and lower where the same would result in IndentationError and + # string " True " would be returned by Templar. + val = to_text(templar.template(presented, disable_lookups=disable_lookups)).strip() if val == "True": return True elif val == "False": diff --git a/test/units/template/test_native_concat.py b/test/units/template/test_native_concat.py index db85a73b92b..4164bc4578c 100644 --- a/test/units/template/test_native_concat.py +++ b/test/units/template/test_native_concat.py @@ -5,24 +5,45 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import importlib +import sys + import pytest from ansible import constants as C from ansible.errors import AnsibleUndefinedVariable - -# need to mock DEFAULT_JINJA2_NATIVE here so native modules are imported -# correctly within the template module -C.DEFAULT_JINJA2_NATIVE = True -from ansible.template import Templar +from ansible.playbook.conditional import Conditional from units.mock.loader import DictDataLoader +@pytest.fixture +def native_template_mod(monkeypatch): + monkeypatch.delitem(sys.modules, 'ansible.template') + monkeypatch.setattr(C, 'DEFAULT_JINJA2_NATIVE', True) + return importlib.import_module('ansible.template') + + # https://github.com/ansible/ansible/issues/52158 -def test_undefined_variable(): +def test_undefined_variable(native_template_mod): fake_loader = DictDataLoader({}) variables = {} - templar = Templar(loader=fake_loader, variables=variables) + templar = native_template_mod.Templar(loader=fake_loader, variables=variables) + assert isinstance(templar.environment, native_template_mod.AnsibleNativeEnvironment) with pytest.raises(AnsibleUndefinedVariable): templar.template("{{ missing }}") + + +def test_cond_eval(native_template_mod): + fake_loader = DictDataLoader({}) + # True must be stored in a variable to trigger templating. Using True + # directly would be caught by optimization for bools to short-circuit + # templating. + variables = {"foo": True} + templar = native_template_mod.Templar(loader=fake_loader, variables=variables) + assert isinstance(templar.environment, native_template_mod.AnsibleNativeEnvironment) + + cond = Conditional(loader=fake_loader) + cond.when = ["foo"] + assert cond.evaluate_conditional(templar, variables)