Simplify AnsibleJ2Vars by using ChainMap for vars (#78713)

Co-authored-by: Matt Martz <matt@sivel.net>
pull/79570/head
Martin Krizek 2 years ago committed by GitHub
parent 245d516911
commit 60f76436c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,2 @@
minor_changes:
- "``AnsibleJ2Vars`` class that acts as a storage for all variables for templating purposes now uses ``collections.ChainMap`` internally."

@ -1,25 +1,7 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
# # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# This file is part of Ansible
# from collections import ChainMap
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from collections.abc import Mapping
from jinja2.utils import missing from jinja2.utils import missing
@ -30,99 +12,65 @@ from ansible.module_utils._text import to_native
__all__ = ['AnsibleJ2Vars'] __all__ = ['AnsibleJ2Vars']
class AnsibleJ2Vars(Mapping): def _process_locals(_l):
''' if _l is None:
Helper class to template all variable content before jinja2 sees it. This is return {}
done by hijacking the variable storage that jinja2 uses, and overriding __contains__ return {
and __getitem__ to look like a dict. Added bonus is avoiding duplicating the large k: v for k, v in _l.items()
hashes that inject tends to be. if v is not missing
and k not in {'context', 'environment', 'template'} # NOTE is this really needed?
}
To facilitate using builtin jinja2 things like range, globals are also handled here.
'''
def __init__(self, templar, globals, locals=None): class AnsibleJ2Vars(ChainMap):
''' """Helper variable storage class that allows for nested variables templating: `foo: "{{ bar }}"`."""
Initializes this object with a valid Templar() object, as
well as several dictionaries of variables representing
different scopes (in jinja2 terminology).
'''
def __init__(self, templar, globals, locals=None):
self._templar = templar self._templar = templar
self._globals = globals super().__init__(
self._locals = dict() _process_locals(locals), # first mapping has the highest precedence
if isinstance(locals, dict): self._templar.available_variables,
for key, val in locals.items(): globals,
if val is not missing: )
if key[:2] == 'l_':
self._locals[key[2:]] = val
elif key not in ('context', 'environment', 'template'):
self._locals[key] = val
def __contains__(self, k):
if k in self._locals:
return True
if k in self._templar.available_variables:
return True
if k in self._globals:
return True
return False
def __iter__(self):
keys = set()
keys.update(self._templar.available_variables, self._locals, self._globals)
return iter(keys)
def __len__(self):
keys = set()
keys.update(self._templar.available_variables, self._locals, self._globals)
return len(keys)
def __getitem__(self, varname): def __getitem__(self, varname):
if varname in self._locals: variable = super().__getitem__(varname)
return self._locals[varname]
if varname in self._templar.available_variables:
variable = self._templar.available_variables[varname]
elif varname in self._globals:
return self._globals[varname]
else:
raise KeyError("undefined variable: %s" % varname)
# HostVars is special, return it as-is, as is the special variable
# 'vars', which contains the vars structure
from ansible.vars.hostvars import HostVars from ansible.vars.hostvars import HostVars
if isinstance(variable, dict) and varname == "vars" or isinstance(variable, HostVars) or hasattr(variable, '__UNSAFE__'): if (varname == "vars" and isinstance(variable, dict)) or isinstance(variable, HostVars) or hasattr(variable, '__UNSAFE__'):
return variable return variable
else:
value = None try:
try: return self._templar.template(variable)
value = self._templar.template(variable) except AnsibleUndefinedVariable as e:
except AnsibleUndefinedVariable as e: # Instead of failing here prematurely, return an Undefined
# Instead of failing here prematurely, return an Undefined # object which fails only after its first usage allowing us to
# object which fails only after its first usage allowing us to # do lazy evaluation and passing it into filters/tests that
# do lazy evaluation and passing it into filters/tests that # operate on such objects.
# operate on such objects. return self._templar.environment.undefined(
return self._templar.environment.undefined( hint=f"{variable}: {e.message}",
hint=f"{variable}: {e.message}", name=varname,
name=varname, exc=AnsibleUndefinedVariable,
exc=AnsibleUndefinedVariable, )
) except Exception as e:
except Exception as e: msg = getattr(e, 'message', None) or to_native(e)
msg = getattr(e, 'message', None) or to_native(e) raise AnsibleError(
raise AnsibleError("An unhandled exception occurred while templating '%s'. " f"An unhandled exception occurred while templating '{to_native(variable)}'. "
"Error was a %s, original message: %s" % (to_native(variable), type(e), msg)) f"Error was a {type(e)}, original message: {msg}"
)
return value
def add_locals(self, locals): def add_locals(self, locals):
''' """If locals are provided, create a copy of self containing those
If locals are provided, create a copy of self containing those
locals in addition to what is already in this variable proxy. locals in addition to what is already in this variable proxy.
''' """
if locals is None: if locals is None:
return self return self
current_locals = self.maps[0]
current_globals = self.maps[2]
# prior to version 2.9, locals contained all of the vars and not just the current # prior to version 2.9, locals contained all of the vars and not just the current
# local vars so this was not necessary for locals to propagate down to nested includes # local vars so this was not necessary for locals to propagate down to nested includes
new_locals = self._locals | locals new_locals = current_locals | locals
return AnsibleJ2Vars(self._templar, self._globals, locals=new_locals) return AnsibleJ2Vars(self._templar, current_globals, locals=new_locals)

@ -19,23 +19,16 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from units.compat import unittest from ansible.template import Templar
from unittest.mock import MagicMock
from ansible.template.vars import AnsibleJ2Vars from ansible.template.vars import AnsibleJ2Vars
class TestVars(unittest.TestCase): def test_globals_empty():
def setUp(self): assert isinstance(dict(AnsibleJ2Vars(Templar(None), {})), dict)
self.mock_templar = MagicMock(name='mock_templar')
def test_globals_empty(self):
ajvars = AnsibleJ2Vars(self.mock_templar, {})
res = dict(ajvars)
self.assertIsInstance(res, dict)
def test_globals(self): def test_globals():
res = dict(AnsibleJ2Vars(self.mock_templar, {'foo': 'bar', 'blip': [1, 2, 3]})) res = dict(AnsibleJ2Vars(Templar(None), {'foo': 'bar', 'blip': [1, 2, 3]}))
self.assertIsInstance(res, dict) assert isinstance(res, dict)
self.assertIn('foo', res) assert 'foo' in res
self.assertEqual(res['foo'], 'bar') assert res['foo'] == 'bar'

Loading…
Cancel
Save