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:
|
||||
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
|
||||
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):
|
||||
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')
|
||||
|
||||
msg = "'{0!r}' is not a string and conversion is not allowed".format(value)
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@
|
|||
- assert:
|
||||
that:
|
||||
- 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
|
||||
apt_repository:
|
||||
|
|
|
|||
|
|
@ -188,29 +188,6 @@
|
|||
c_list: []
|
||||
c_raw: ~
|
||||
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
|
||||
block:
|
||||
- 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 }}"))
|
||||
|
||||
|
||||
@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:
|
||||
"""Verify that filters which return a generator are converted to a list while under the filter's JinjaCallContext."""
|
||||
variables = dict(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from ansible.module_utils.common.validation import check_type_str, _check_type_s
|
|||
|
||||
TEST_CASES = (
|
||||
('string', 'string'),
|
||||
(None, '',), # 2.19+ relaxed restriction on None<->empty for backward compatibility
|
||||
(100, '100'),
|
||||
(1.5, '1.5'),
|
||||
({'k1': 'v1'}, "{'k1': 'v1'}"),
|
||||
|
|
@ -25,7 +26,7 @@ def test_check_type_str(value, expected):
|
|||
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):
|
||||
with pytest.raises(TypeError) as e:
|
||||
_check_type_str_no_conversion(value)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user