mirror of
https://github.com/zebrajr/ansible.git
synced 2025-12-06 00:19:48 +01:00
Backward-compatible None handling in template concat and argspec str (#85652)
* templating coerces None to empty string on multi-node result * avoid simple cases of embedded `None` in multi-node string concatenated template results ala <=2.18 * single-node template results preserve NoneType * add None->empty str equivalency to argspec validation * fix integration tests * remove conversion error message check from apt_repository test * remove error message check on `None` value for required str argspec in roles_arg_spec test (now logically-equivalent to empty string) * explanatory comment for None->empty str coalesce
This commit is contained in:
parent
76748b8478
commit
e3c9908679
3
changelogs/fragments/concat_coerce_none_to_empty.yml
Normal file
3
changelogs/fragments/concat_coerce_none_to_empty.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
bugfixes:
|
||||||
|
- templating - Multi-node template results coerce embedded ``None`` nodes to empty string (instead of rendering literal ``None`` to the output).
|
||||||
|
- argspec validation - The ``str`` argspec type treats ``None`` values as empty string for better consistency with pre-2.19 templating conversions.
|
||||||
|
|
@ -753,7 +753,7 @@ class AnsibleEnvironment(SandboxedEnvironment):
|
||||||
except MarkerError as ex:
|
except MarkerError as ex:
|
||||||
return ex.source # return the first Marker encountered
|
return ex.source # return the first Marker encountered
|
||||||
|
|
||||||
return ''.join([to_text(v) for v in node_list])
|
return ''.join([to_text(v) for v in node_list if v is not None]) # skip concat on `None`-valued nodes to avoid literal "None" in template results
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _access_const(const_template: t.LiteralString) -> t.Any:
|
def _access_const(const_template: t.LiteralString) -> t.Any:
|
||||||
|
|
|
||||||
|
|
@ -374,7 +374,10 @@ def check_type_str(value, allow_conversion=True, param=None, prefix=''):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
if allow_conversion and value is not None:
|
if value is None:
|
||||||
|
return '' # approximate pre-2.19 templating None->empty str equivalency here for backward compatibility
|
||||||
|
|
||||||
|
if allow_conversion:
|
||||||
return to_native(value, errors='surrogate_or_strict')
|
return to_native(value, errors='surrogate_or_strict')
|
||||||
|
|
||||||
msg = "'{0!r}' is not a string and conversion is not allowed".format(value)
|
msg = "'{0!r}' is not a string and conversion is not allowed".format(value)
|
||||||
|
|
|
||||||
|
|
@ -301,7 +301,7 @@
|
||||||
- assert:
|
- assert:
|
||||||
that:
|
that:
|
||||||
- result is failed
|
- result is failed
|
||||||
- result.msg.startswith("argument 'repo' is of type NoneType and we were unable to convert to str")
|
- result.msg == 'Please set argument \'repo\' to a non-empty value'
|
||||||
|
|
||||||
- name: Test apt_repository with an empty value for repo
|
- name: Test apt_repository with an empty value for repo
|
||||||
apt_repository:
|
apt_repository:
|
||||||
|
|
|
||||||
|
|
@ -188,29 +188,6 @@
|
||||||
c_list: []
|
c_list: []
|
||||||
c_raw: ~
|
c_raw: ~
|
||||||
tasks:
|
tasks:
|
||||||
- name: test type coercion fails on None for required str
|
|
||||||
block:
|
|
||||||
- name: "Test import_role of role C (missing a_str)"
|
|
||||||
import_role:
|
|
||||||
name: c
|
|
||||||
vars:
|
|
||||||
a_str: ~
|
|
||||||
- fail:
|
|
||||||
msg: "Should not get here"
|
|
||||||
rescue:
|
|
||||||
- debug:
|
|
||||||
var: ansible_failed_result
|
|
||||||
- name: "Validate import_role failure"
|
|
||||||
assert:
|
|
||||||
that:
|
|
||||||
# NOTE: a bug here that prevents us from getting ansible_failed_task
|
|
||||||
- ansible_failed_result.argument_errors == [error]
|
|
||||||
- ansible_failed_result.argument_spec_data == a_main_spec
|
|
||||||
vars:
|
|
||||||
error: >-
|
|
||||||
argument 'a_str' is of type NoneType and we were unable to convert to str:
|
|
||||||
'None' is not a string and conversion is not allowed
|
|
||||||
|
|
||||||
- name: test type coercion fails on None for required int
|
- name: test type coercion fails on None for required int
|
||||||
block:
|
block:
|
||||||
- name: "Test import_role of role C (missing c_int)"
|
- name: "Test import_role of role C (missing c_int)"
|
||||||
|
|
|
||||||
|
|
@ -1080,6 +1080,16 @@ def test_marker_from_test_plugin() -> None:
|
||||||
assert TemplateEngine(variables=dict(something=TRUST.tag("{{ nope }}"))).template(TRUST.tag("{{ (something is eq {}) is undefined }}"))
|
assert TemplateEngine(variables=dict(something=TRUST.tag("{{ nope }}"))).template(TRUST.tag("{{ (something is eq {}) is undefined }}"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("template,expected", (
|
||||||
|
("{{ none }}", None), # concat sees one node, NoneType result is preserved
|
||||||
|
("{% if False %}{% endif %}", None), # concat sees one node, NoneType result is preserved
|
||||||
|
("{{''}}{% if False %}{% endif %}", ""), # multiple blocks with an embedded None result, concat is in play, the result is an empty string
|
||||||
|
("hey {{ none }}", "hey "), # composite template, the result is an empty string
|
||||||
|
))
|
||||||
|
def test_none_concat(template: str, expected: object) -> None:
|
||||||
|
assert TemplateEngine().template(TRUST.tag(template)) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_filter_generator() -> None:
|
def test_filter_generator() -> None:
|
||||||
"""Verify that filters which return a generator are converted to a list while under the filter's JinjaCallContext."""
|
"""Verify that filters which return a generator are converted to a list while under the filter's JinjaCallContext."""
|
||||||
variables = dict(
|
variables = dict(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ from ansible.module_utils.common.validation import check_type_str, _check_type_s
|
||||||
|
|
||||||
TEST_CASES = (
|
TEST_CASES = (
|
||||||
('string', 'string'),
|
('string', 'string'),
|
||||||
|
(None, '',), # 2.19+ relaxed restriction on None<->empty for backward compatibility
|
||||||
(100, '100'),
|
(100, '100'),
|
||||||
(1.5, '1.5'),
|
(1.5, '1.5'),
|
||||||
({'k1': 'v1'}, "{'k1': 'v1'}"),
|
({'k1': 'v1'}, "{'k1': 'v1'}"),
|
||||||
|
|
@ -25,7 +26,7 @@ def test_check_type_str(value, expected):
|
||||||
assert expected == check_type_str(value)
|
assert expected == check_type_str(value)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('value, expected', TEST_CASES[1:])
|
@pytest.mark.parametrize('value, expected', TEST_CASES[2:])
|
||||||
def test_check_type_str_no_conversion(value, expected):
|
def test_check_type_str_no_conversion(value, expected):
|
||||||
with pytest.raises(TypeError) as e:
|
with pytest.raises(TypeError) as e:
|
||||||
_check_type_str_no_conversion(value)
|
_check_type_str_no_conversion(value)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user