summaryrefslogtreecommitdiffstats
path: root/lib/ansible/template/native_helpers.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/template/native_helpers.py')
-rw-r--r--lib/ansible/template/native_helpers.py144
1 files changed, 144 insertions, 0 deletions
diff --git a/lib/ansible/template/native_helpers.py b/lib/ansible/template/native_helpers.py
new file mode 100644
index 0000000..343e10c
--- /dev/null
+++ b/lib/ansible/template/native_helpers.py
@@ -0,0 +1,144 @@
+# Copyright: (c) 2018, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import ast
+from itertools import islice, chain
+from types import GeneratorType
+
+from ansible.module_utils._text import to_text
+from ansible.module_utils.six import string_types
+from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+from ansible.utils.native_jinja import NativeJinjaText
+from ansible.utils.unsafe_proxy import wrap_var
+
+
+_JSON_MAP = {
+ "true": True,
+ "false": False,
+ "null": None,
+}
+
+
+class Json2Python(ast.NodeTransformer):
+ def visit_Name(self, node):
+ if node.id not in _JSON_MAP:
+ return node
+ return ast.Constant(value=_JSON_MAP[node.id])
+
+
+def ansible_eval_concat(nodes):
+ """Return a string of concatenated compiled nodes. Throw an undefined error
+ if any of the nodes is undefined.
+
+ If the result of concat appears to be a dictionary, list or bool,
+ try and convert it to such using literal_eval, the same mechanism as used
+ in jinja2_native.
+
+ Used in Templar.template() when jinja2_native=False and convert_data=True.
+ """
+ head = list(islice(nodes, 2))
+
+ if not head:
+ return ''
+
+ if len(head) == 1:
+ out = head[0]
+
+ if isinstance(out, NativeJinjaText):
+ return out
+
+ out = to_text(out)
+ else:
+ if isinstance(nodes, GeneratorType):
+ nodes = chain(head, nodes)
+ out = ''.join([to_text(v) for v in nodes])
+
+ # if this looks like a dictionary, list or bool, convert it to such
+ if out.startswith(('{', '[')) or out in ('True', 'False'):
+ unsafe = hasattr(out, '__UNSAFE__')
+ try:
+ out = ast.literal_eval(
+ ast.fix_missing_locations(
+ Json2Python().visit(
+ ast.parse(out, mode='eval')
+ )
+ )
+ )
+ except (ValueError, SyntaxError, MemoryError):
+ pass
+ else:
+ if unsafe:
+ out = wrap_var(out)
+
+ return out
+
+
+def ansible_concat(nodes):
+ """Return a string of concatenated compiled nodes. Throw an undefined error
+ if any of the nodes is undefined. Other than that it is equivalent to
+ Jinja2's default concat function.
+
+ Used in Templar.template() when jinja2_native=False and convert_data=False.
+ """
+ return ''.join([to_text(v) for v in nodes])
+
+
+def ansible_native_concat(nodes):
+ """Return a native Python type from the list of compiled nodes. If the
+ result is a single node, its value is returned. Otherwise, the nodes are
+ concatenated as strings. If the result can be parsed with
+ :func:`ast.literal_eval`, the parsed value is returned. Otherwise, the
+ string is returned.
+
+ https://github.com/pallets/jinja/blob/master/src/jinja2/nativetypes.py
+ """
+ head = list(islice(nodes, 2))
+
+ if not head:
+ return None
+
+ if len(head) == 1:
+ out = head[0]
+
+ # TODO send unvaulted data to literal_eval?
+ if isinstance(out, AnsibleVaultEncryptedUnicode):
+ return out.data
+
+ if isinstance(out, NativeJinjaText):
+ # Sometimes (e.g. ``| string``) we need to mark variables
+ # in a special way so that they remain strings and are not
+ # passed into literal_eval.
+ # See:
+ # https://github.com/ansible/ansible/issues/70831
+ # https://github.com/pallets/jinja/issues/1200
+ # https://github.com/ansible/ansible/issues/70831#issuecomment-664190894
+ return out
+
+ # short-circuit literal_eval for anything other than strings
+ if not isinstance(out, string_types):
+ return out
+ else:
+ if isinstance(nodes, GeneratorType):
+ nodes = chain(head, nodes)
+ out = ''.join([to_text(v) for v in nodes])
+
+ try:
+ evaled = ast.literal_eval(
+ # In Python 3.10+ ast.literal_eval removes leading spaces/tabs
+ # from the given string. For backwards compatibility we need to
+ # parse the string ourselves without removing leading spaces/tabs.
+ ast.parse(out, mode='eval')
+ )
+ except (ValueError, SyntaxError, MemoryError):
+ return out
+
+ if isinstance(evaled, string_types):
+ quote = out[0]
+ return f'{quote}{evaled}{quote}'
+
+ return evaled