diff --git a/changelogs/fragments/75131-fix-rekey_on_member-lazy-evaluation.yaml b/changelogs/fragments/75131-fix-rekey_on_member-lazy-evaluation.yaml new file mode 100644 index 00000000000..23585033f0b --- /dev/null +++ b/changelogs/fragments/75131-fix-rekey_on_member-lazy-evaluation.yaml @@ -0,0 +1,2 @@ +bugfixes: + - rekey_on_member - handle undefined positional arguments better. diff --git a/docs/docsite/rst/dev_guide/developing_plugins.rst b/docs/docsite/rst/dev_guide/developing_plugins.rst index 08aec93f6c8..7d941f58b35 100644 --- a/docs/docsite/rst/dev_guide/developing_plugins.rst +++ b/docs/docsite/rst/dev_guide/developing_plugins.rst @@ -36,7 +36,10 @@ You should return errors encountered during plugin execution by raising ``Ansibl except Exception as e: raise AnsibleError('Something happened, this was original exception: %s' % to_native(e)) +Since Ansible evaluates variables only when they are needed, filter and test plugins should propagate the exceptions ``jinja2.exceptions.UndefinedError`` and ``AnsibleUndefinedVariable`` to ensure undefined variables are only fatal when necessary. + Check the different `AnsibleError objects `_ and see which one applies best to your situation. +Check the section on the specific plugin type you're developing for type-specific error handling details. String encoding =============== @@ -306,6 +309,17 @@ Filter plugins manipulate data. They are a feature of Jinja2 and are also availa Filter plugins do not use the standard configuration and documentation system described above. +Since Ansible evaluates variables only when they are needed, filter plugins should propagate the exceptions ``jinja2.exceptions.UndefinedError`` and ``AnsibleUndefinedVariable`` to ensure undefined variables are only fatal when necessary. + +.. code-block:: python + + try: + cause_an_exception(with_undefined_variable) + except jinja2.exceptions.UndefinedError as e: + raise AnsibleUndefinedVariable("Something happened, this was the original exception: %s" % to_native(e)) + except Exception as e: + raise AnsibleFilterError("Something happened, this was the original exception: %s" % to_native(e)) + For example filter plugins, see the source code for the `filter plugins included with Ansible Core `_. .. _developing_inventory_plugins: @@ -433,6 +447,17 @@ Test plugins verify data. They are a feature of Jinja2 and are also available in Test plugins do not use the standard configuration and documentation system described above. +Since Ansible evaluates variables only when they are needed, test plugins should propagate the exceptions ``jinja2.exceptions.UndefinedError`` and ``AnsibleUndefinedVariable`` to ensure undefined variables are only fatal when necessary. + +.. code-block:: python + + try: + cause_an_exception(with_undefined_variable) + except jinja2.exceptions.UndefinedError as e: + raise AnsibleUndefinedVariable("Something happened, this was the original exception: %s" % to_native(e)) + except Exception as e: + raise AnsibleFilterError("Something happened, this was the original exception: %s" % to_native(e)) + For example test plugins, see the source code for the `test plugins included with Ansible Core `_. .. _developing_vars_plugins: diff --git a/lib/ansible/plugins/filter/mathstuff.py b/lib/ansible/plugins/filter/mathstuff.py index 1a545538892..d2ea3f6df59 100644 --- a/lib/ansible/plugins/filter/mathstuff.py +++ b/lib/ansible/plugins/filter/mathstuff.py @@ -210,6 +210,9 @@ def rekey_on_member(data, key, duplicates='error'): new_obj = {} + # Ensure the positional args are defined - raise jinja2.exceptions.UndefinedError if not + bool(data) and bool(key) + if isinstance(data, Mapping): iterate_over = data.values() elif isinstance(data, Iterable) and not isinstance(data, (text_type, binary_type)): diff --git a/test/integration/targets/filter_mathstuff/host_vars/localhost.yml b/test/integration/targets/filter_mathstuff/host_vars/localhost.yml new file mode 100644 index 00000000000..1f5a9e0319d --- /dev/null +++ b/test/integration/targets/filter_mathstuff/host_vars/localhost.yml @@ -0,0 +1 @@ +foo: test diff --git a/test/integration/targets/filter_mathstuff/tasks/main.yml b/test/integration/targets/filter_mathstuff/tasks/main.yml index 93a65727f05..019f00e4c26 100644 --- a/test/integration/targets/filter_mathstuff/tasks/main.yml +++ b/test/integration/targets/filter_mathstuff/tasks/main.yml @@ -301,6 +301,18 @@ - rekey_on_member_exc5_res is failed - '"is not unique, cannot correctly turn into dict" in rekey_on_member_exc5_res.msg' +- name: test undefined positional args for rekey_on_member are properly handled + vars: + all_vars: "{{ hostvars[inventory_hostname] }}" + test_var: "{{ all_vars.foo }}" + block: + - include_vars: + file: defined_later.yml + - assert: + that: "test_var == 'test'" + - assert: + that: "rekeyed == {'value': {'test': 'value'}}" + # TODO: For some reason, the coverage tool isn't accounting for the last test # so add another "last test" to fake it... - assert: diff --git a/test/integration/targets/filter_mathstuff/vars/defined_later.yml b/test/integration/targets/filter_mathstuff/vars/defined_later.yml new file mode 100644 index 00000000000..dfb2421b61f --- /dev/null +++ b/test/integration/targets/filter_mathstuff/vars/defined_later.yml @@ -0,0 +1,3 @@ +do_rekey: + - test: value +rekeyed: "{{ do_rekey | rekey_on_member(defined_later) }}" diff --git a/test/integration/targets/filter_mathstuff/vars/main.yml b/test/integration/targets/filter_mathstuff/vars/main.yml new file mode 100644 index 00000000000..bb61e12ee3c --- /dev/null +++ b/test/integration/targets/filter_mathstuff/vars/main.yml @@ -0,0 +1 @@ +defined_later: "{{ test_var }}"