parent
e8b21f22ca
commit
7db61908ae
@ -0,0 +1,298 @@
|
||||
from jinja2.ext import Extension
|
||||
from jinja2 import Environment, FileSystemLoader, TemplateSyntaxError, nodes
|
||||
from jinja2.utils import missing
|
||||
from jinja2.runtime import Context, Undefined
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
|
||||
class Variables(MutableMapping):
|
||||
'''Класс заглушка вместо модуля переменных.'''
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.__attrs = dict(*args, **kwargs)
|
||||
|
||||
def __next__(self):
|
||||
iterator = iter(self.__attrs)
|
||||
return next(iterator)
|
||||
|
||||
def __getattribute__(self, name):
|
||||
if name == '_Variables__attrs':
|
||||
return super().__getattribute__(name)
|
||||
try:
|
||||
return self.__attrs[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.__attrs[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self.__attrs[name] = value
|
||||
|
||||
def __delitem__(self, name):
|
||||
del self.__attrs[name]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__attrs)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__attrs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Variables {}>'.format(self.__attrs)
|
||||
|
||||
|
||||
def resolve_or_missing(context, key, missing=missing, env={}):
|
||||
datavars = context.parent['__datavars__']
|
||||
if key in context.vars:
|
||||
return context.vars[key]
|
||||
if key in context.parent:
|
||||
return context.parent[key]
|
||||
if key in datavars:
|
||||
return datavars[key]
|
||||
for name in env:
|
||||
if name in datavars and key in datavars[name]:
|
||||
return datavars[name][key]
|
||||
return missing
|
||||
|
||||
|
||||
class CalculateContext(Context):
|
||||
_env_set = set()
|
||||
|
||||
def resolve(self, key):
|
||||
if self._legacy_resolve_mode:
|
||||
rv = resolve_or_missing(self, key,
|
||||
env=self._env_set)
|
||||
else:
|
||||
rv = self.resolve_or_missing(key)
|
||||
if rv is missing:
|
||||
return self.environment.undefined(name=key)
|
||||
return rv
|
||||
|
||||
def resolve_or_missing(self, key):
|
||||
if self._legacy_resolve_mode:
|
||||
rv = self.resolve(key)
|
||||
if isinstance(rv, Undefined):
|
||||
rv = missing
|
||||
return rv
|
||||
return resolve_or_missing(self, key,
|
||||
env=self._env_set)
|
||||
|
||||
|
||||
class Parameters(MutableMapping):
|
||||
'''Класс для хранения параметров и условий, взятых из шаблона, и передачи
|
||||
их шаблонизатору.'''
|
||||
def __init__(self, parameters_dictionary={}, condition=True):
|
||||
self.__parameters = parameters_dictionary
|
||||
self.__condition = condition
|
||||
|
||||
def set_parameters(self, *args, **kwargs):
|
||||
parameters = dict(*args, **kwargs)
|
||||
self.__parameters.update(parameters)
|
||||
|
||||
def set_condition(self, condition):
|
||||
self.__condition = self.__condition and condition
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.__parameters[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self.__parameters[name] = value
|
||||
|
||||
def __delitem__(self, name):
|
||||
del self.__parameters[name]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__parameters)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__parameters)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Parameters: {0}, condition={1}>'.format(self.__parameters,
|
||||
self.__condition)
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
return self.__condition
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return self.__parameters
|
||||
|
||||
|
||||
class CalculateExtension(Extension):
|
||||
_parameters_set = set()
|
||||
|
||||
def __init__(self, environment):
|
||||
self.tags = {'calculate', 'save', 'set_var'}
|
||||
self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'}
|
||||
self.LITERAL_TOKENS_TYPES = {'string', 'integer', 'float'}
|
||||
self.TARGET_FILES_SET = {'grp', 'system', 'etc', 'local', 'remote'}
|
||||
|
||||
self.parse_methods = {'calculate': self.parse_calculate,
|
||||
'save': self.parse_save}
|
||||
|
||||
def parse(self, parser):
|
||||
self.parser = parser
|
||||
self.stream = parser.stream
|
||||
tag_token = self.stream.current.value
|
||||
return [self.parse_methods[tag_token]()]
|
||||
|
||||
def parse_save(self):
|
||||
lineno = next(self.stream).lineno
|
||||
target_file = nodes.Const('', lineno=lineno)
|
||||
|
||||
if self.stream.skip_if('dot'):
|
||||
target_file_name = self.stream.expect('name').value
|
||||
if target_file_name in self.TARGET_FILES_SET:
|
||||
target_file = nodes.Const(target_file_name)
|
||||
else:
|
||||
TemplateSyntaxError("Unknown target file '{}'".
|
||||
format(target_file_name),
|
||||
lineno=lineno)
|
||||
|
||||
# получаем список из имени переменной.
|
||||
module_name = self.stream.expect('name').value
|
||||
variable_name = [nodes.Const(module_name, lineno=lineno)]
|
||||
while self.stream.skip_if('dot'):
|
||||
name = self.stream.expect('name').value
|
||||
variable_name.append(nodes.Const(name, lineno=lineno))
|
||||
variable_name = nodes.List(variable_name, lineno=lineno)
|
||||
|
||||
if self.stream.skip_if('assign'):
|
||||
right_value = self.parser.parse_expression(with_condexpr=True)
|
||||
save_variable_node = self.call_method('save_variable',
|
||||
[variable_name,
|
||||
right_value,
|
||||
target_file,
|
||||
nodes.ContextReference()],
|
||||
lineno=lineno)
|
||||
return nodes.Output([save_variable_node], lineno=lineno)
|
||||
else:
|
||||
TemplateSyntaxError("'=' is expected in 'save' tag.",
|
||||
lineno=lineno)
|
||||
|
||||
def parse_calculate(self):
|
||||
pairs_list = []
|
||||
expect_comma_flag = False
|
||||
conditions = nodes.Const(True)
|
||||
|
||||
lineno = next(self.stream).lineno
|
||||
|
||||
while self.stream.current.type != 'block_end':
|
||||
if expect_comma_flag:
|
||||
self.stream.expect('comma')
|
||||
|
||||
if self.stream.current.type == 'name':
|
||||
if (self.stream.current.value in self._parameters_set and
|
||||
self.stream.look().type != 'dot'):
|
||||
pairs_list.append(self.get_parameter_node())
|
||||
else:
|
||||
conditions = nodes.And(
|
||||
self.parser.parse_expression(
|
||||
with_condexpr=True
|
||||
),
|
||||
conditions
|
||||
)
|
||||
else:
|
||||
raise TemplateSyntaxError('Name is expected in calculate tag.',
|
||||
lineno=self.stream.current.lineno)
|
||||
expect_comma_flag = True
|
||||
dictionary_node = nodes.Dict(pairs_list)
|
||||
save_node = self.call_method('save_parameters',
|
||||
[dictionary_node,
|
||||
conditions,
|
||||
nodes.ContextReference()],
|
||||
lineno=lineno)
|
||||
return nodes.Output([save_node], lineno=lineno)
|
||||
|
||||
def save_variable(self, variable_name, right_value, target_file, context):
|
||||
'''Временный метод для сохранения значений переменных.'''
|
||||
datavars = context.parent['__datavars__']
|
||||
module_name = variable_name[0]
|
||||
namespaces = variable_name[1:-1]
|
||||
variable_name = variable_name[-1]
|
||||
|
||||
if module_name in datavars:
|
||||
variables_module = datavars[module_name]
|
||||
for namespace in namespaces:
|
||||
if namespace not in variables_module:
|
||||
variables_module[namespace] = Variables()
|
||||
variables_module = variables_module[namespace]
|
||||
variables_module[variable_name] = right_value
|
||||
else:
|
||||
AttributeError("Unknown variables module '{}'".
|
||||
format(module_name))
|
||||
return ''
|
||||
|
||||
def get_parameter_node(self):
|
||||
lineno = self.stream.current.lineno
|
||||
parameter_name = self.stream.expect('name').value
|
||||
parameter_name_node = nodes.Const(parameter_name, lineno=lineno)
|
||||
if self.stream.skip_if('assign'):
|
||||
parameter_value = self.stream.current.value
|
||||
parameter_rvalue = self.parser.parse_expression(with_condexpr=True)
|
||||
if parameter_name == 'env':
|
||||
env_names = parameter_value.split(',')
|
||||
for name in env_names:
|
||||
CalculateContext._env_set.add(name.strip())
|
||||
else:
|
||||
parameter_rvalue = nodes.Const(True, lineno=lineno)
|
||||
return nodes.Pair(parameter_name_node, parameter_rvalue)
|
||||
|
||||
def save_parameters(cls, parameters_dictionary, compare_result, context):
|
||||
context.parent['__parameters__'].set_parameters(parameters_dictionary)
|
||||
context.parent['__parameters__'].set_condition(compare_result)
|
||||
return ''
|
||||
|
||||
|
||||
class TemplateEngine():
|
||||
def __init__(self, directory_path='/',
|
||||
parameters_set=set(),
|
||||
env_set=set(),
|
||||
datavars_module=Variables()):
|
||||
CalculateExtension._parameters_set = parameters_set
|
||||
|
||||
self._datavars_module = datavars_module
|
||||
self._parameters_object = Parameters()
|
||||
self._template_text = ''
|
||||
|
||||
self.environment = Environment(loader=FileSystemLoader(directory_path),
|
||||
extensions=[CalculateExtension])
|
||||
self.environment.context_class = CalculateContext
|
||||
|
||||
def change_directory(self, directory_path):
|
||||
self.environment.loader = FileSystemLoader(directory_path)
|
||||
|
||||
def process_template(self, template_path, env=set()):
|
||||
CalculateContext._env_set = env
|
||||
template = self.environment.get_template(template_path)
|
||||
self._parameters_object = Parameters(parameters_dictionary={},
|
||||
condition=True)
|
||||
self._template_text = template.render(
|
||||
__datavars__=self._datavars_module,
|
||||
__parameters__=self._parameters_object
|
||||
)
|
||||
|
||||
def process_template_from_string(self, string, env=set()):
|
||||
CalculateContext._env_set = env
|
||||
template = self.environment.from_string(string)
|
||||
self._parameters_object = Parameters(parameters_dictionary={},
|
||||
condition=True)
|
||||
self._template_text = template.render(
|
||||
__datavars__=self._datavars_module,
|
||||
__parameters__=self._parameters_object
|
||||
)
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
return self._parameters_object.condition
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
return self._parameters_object.parameters
|
||||
|
||||
@property
|
||||
def template_text(self):
|
||||
text, self._template_text = self._template_text, ''
|
||||
return text
|
@ -0,0 +1,120 @@
|
||||
import pytest
|
||||
from calculate.templates.template_engine import TemplateEngine, Variables
|
||||
|
||||
|
||||
PARAMETERS_SET = {'name', 'path', 'append', 'chmod', 'chown',
|
||||
'autoupdate', 'env', 'link', 'force', 'symbolic',
|
||||
'format', 'comment', 'protected', 'mirror', 'run',
|
||||
'exec', 'env', 'stretch', 'convert', 'dotall',
|
||||
'multiline', 'dconf', 'package', 'merge',
|
||||
'postmerge', 'action'}
|
||||
|
||||
|
||||
@pytest.mark.template_engine
|
||||
class TestUtils():
|
||||
def test_if_an_input_template_contains_calculate_tag_with_some_parameters__the_template_engine_object_will_collect_its_parameters(self):
|
||||
input_template = '''{% calculate name = 'filename', path = '/etc/path', force %}'''
|
||||
parameters = {'name': 'filename', 'path': '/etc/path', 'force': True}
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
output_parameters = template_engine.parameters
|
||||
assert output_parameters == parameters
|
||||
|
||||
def test_if_an_input_template_binded_with_datavars_module__variables_available_in_a_template(self):
|
||||
input_template = '''{% calculate name = vars.var_1, path = vars.var_2, autoupdate %}'''
|
||||
parameters = {'name': 'filename', 'path': '/etc/path',
|
||||
'autoupdate': True}
|
||||
datavars_module = Variables({'vars':
|
||||
Variables({'var_1': 'filename',
|
||||
'var_2': '/etc/path'})})
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET,
|
||||
datavars_module=datavars_module)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
output_parameters = template_engine.parameters
|
||||
assert output_parameters == parameters
|
||||
|
||||
def test_if_an_input_template_contains_env_parameter_in_which_module_name_is_assigned__the_variables_from_this_module_can_be_used_in_template_without_determining_of_their_module(self):
|
||||
input_template = '''{% calculate name = vars.var_1, path = var_3, env = 'other_vars' %}'''
|
||||
parameters = {'name': 'filename', 'path': '/etc/other_path',
|
||||
'env': 'other_vars'}
|
||||
datavars_module = Variables({'vars':
|
||||
Variables({'var_1': 'filename',
|
||||
'var_2': '/etc/path'}),
|
||||
'other_vars':
|
||||
Variables({'var_3': '/etc/other_path'})})
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET,
|
||||
datavars_module=datavars_module)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
output_parameters = template_engine.parameters
|
||||
assert output_parameters == parameters
|
||||
|
||||
def test_if_an_input_template_contains_condition__the_template_engine_object_will_contain_the_value_of_its_condition(self):
|
||||
input_template = '''{% calculate vars.var_1 < vars.var_2 or (not (var_3 == 'required status') and vars.var_4), env = 'vars' %}'''
|
||||
datavars_module = Variables({'vars':
|
||||
Variables({'var_1': 12,
|
||||
'var_2': 1.2,
|
||||
'var_3': 'unrequired status',
|
||||
'var_4': True})})
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET,
|
||||
datavars_module=datavars_module)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
condition = template_engine.condition
|
||||
assert condition is True
|
||||
|
||||
def test_if_an_input_template_contains_several_calculate_tags__the_template_engine_will_parse_them_all_and_will_contain_all_parameters_and_result_of_all_conditions(self):
|
||||
input_template = '''{% calculate name = vars.var_1, var_4 < var_5 %}
|
||||
{% calculate path = var_3, var_6 == 'value' %}
|
||||
{% calculate env = 'other_vars'%}'''
|
||||
parameters = {'name': 'filename', 'path': '/etc/other_path',
|
||||
'env': 'other_vars'}
|
||||
datavars_module = Variables({'vars':
|
||||
Variables({'var_1': 'filename',
|
||||
'var_2': '/etc/path'}),
|
||||
'other_vars':
|
||||
Variables({'var_3': '/etc/other_path',
|
||||
'var_4': 12, 'var_5': 1.2,
|
||||
'var_6': 'value'})})
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET,
|
||||
datavars_module=datavars_module)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
condition = template_engine.condition
|
||||
output_parameters = template_engine.parameters
|
||||
assert condition is False and output_parameters == parameters
|
||||
|
||||
def test_if_an_input_template_contains_variables_in_its_text__the_rendered_text_will_contain_values_of_this_variables(self):
|
||||
input_template = '''{% calculate name = 'filename', force -%}
|
||||
parameter_1 = {{ vars_2.var_3 }}
|
||||
{% calculate env = 'vars_1' -%}
|
||||
parameter_2 = {{ vars_2.var_4 }}
|
||||
parameter_3 = {{ var_1 }}'''
|
||||
output_text = '''parameter_1 = value_1
|
||||
parameter_2 = value_2
|
||||
parameter_3 = 12'''
|
||||
datavars_module = Variables({'vars_1':
|
||||
Variables({'var_1': 12,
|
||||
'var_2': 1.2}),
|
||||
'vars_2':
|
||||
Variables({'var_3': 'value_1',
|
||||
'var_4': 'value_2'})})
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET,
|
||||
datavars_module=datavars_module)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
text = template_engine.template_text
|
||||
assert text == output_text
|
||||
|
||||
def test_if_value_of_variable_is_set_using_save_tag__the_new_value_of_variable_can_be_used_in_template(self):
|
||||
input_template = '''{% calculate name = 'filename', force -%}
|
||||
{% save vars_1.var_3 = 13 -%}
|
||||
parameter = {{ vars_1.var_3 - 1 + vars_1.var_2 * 2 }}
|
||||
{% save.session vars_1.var_4 = True -%}
|
||||
bool parameter = {{ vars_1.var_4 }}'''
|
||||
output_text = '''parameter = 14.4
|
||||
bool parameter = True'''
|
||||
datavars_module = Variables({'vars_1':
|
||||
Variables({'var_1': 12,
|
||||
'var_2': 1.2})})
|
||||
template_engine = TemplateEngine(parameters_set=PARAMETERS_SET,
|
||||
datavars_module=datavars_module)
|
||||
template_engine.process_template_from_string(input_template)
|
||||
text = template_engine.template_text
|
||||
assert text == output_text
|
Loading…
Reference in new issue