You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

347 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# vim: fileencoding=utf-8
#
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={}):
'''Переопределение функции из для поиска значений переменных из jinja2.
Ищет переменные в datavars.'''
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):
'''Класс контекста позволяющий искать значения datavars и сохранять их.'''
_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 ConditionFailed(TemplateSyntaxError):
pass
class ParametersContainer(MutableMapping):
'''Класс для хранения параметров, взятых из шаблона, и передачи
их шаблонизатору.'''
def __init__(self, parameters_dictionary={}):
self.__parameters = parameters_dictionary
def set_parameters(self, *args, **kwargs):
parameters = dict(*args, **kwargs)
self.__parameters.update(parameters)
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 '<ParametersContainer: {0}>'.format(self.__parameters)
@property
def parameters(self):
return self.__parameters
class CalculateExtension(Extension):
'''Класс расширения для jinja2, поддерживающий теги calculate-шаблонов.'''
_parameters_set = set()
_datavars = Variables()
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}
self.environment = environment
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):
'''Метод для разбора тега save, сохраняющего значение указанной
переменной datavars.'''
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):
'''Метод для разбора тега calculate, содержащего значения параметров и
условия выполнения шаблона.'''
pairs_list = []
expect_comma_flag = False
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'
and self.stream.current.value in self._parameters_set
and self.stream.look().type != 'dot'
and self.stream.look().type not in
self.CONDITION_TOKENS_TYPES):
# разбираем параметр.
pairs_list.append(self.get_parameter_node())
elif (self.stream.current.type == 'name'
or self.stream.current.type == 'lparen'):
# разбираем условие. Если условие False -- кидаем исключение.
condition_result = self.get_condition_result()
if not condition_result:
raise ConditionFailed(
'Condition is failed',
lineno=self.stream.current.lineno
)
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,
nodes.ContextReference()],
lineno=lineno)
return nodes.Output([save_node], lineno=lineno)
def get_condition_result(self):
'''Метод для разбора условий из тега calculate.'''
condition_list = []
# собираем исходный код условия из токенов.
# вероятно, следует придумать лучший способ.
while (self.stream.current.type != 'block_end' and
self.stream.current.type != 'comma'):
if self.stream.current.type == 'string':
condition_list.append("'{}'".format(
self.stream.current.value
))
elif self.stream.current.type == 'dot':
self.stream.skip(1)
if self.stream.current.type == 'name':
next_name = '.' + self.stream.current.value
else:
raise TemplateSyntaxError(
'Variable name is not correct.',
lineno=self.stream.current.lineno
)
condition_list[-1] = condition_list[-1] + next_name
else:
condition_list.append(
str(self.stream.current.value)
)
self.stream.skip(1)
condition = ' '.join(condition_list)
# компилируем исходный код условия и получаем результат его вычисления.
cond_expr = self.environment.compile_expression(condition)
condition_result = cond_expr(__datavars__=self._datavars)
return condition_result
def save_variable(self, variable_name, right_value, target_file, context):
'''Метод для сохранения значений переменных указанных в теге save.'''
# временная реализация.
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):
'''Метод для разбра параметров, содержащихся в теге calculate.'''
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 -- обновляем множенство значений env
# контекста вo время парсинга.
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, context):
'''Метод для сохранения значений параметров.'''
context.parent['__parameters__'].set_parameters(parameters_dictionary)
return ''
class TemplateEngine:
def __init__(self, directory_path='/',
parameters_set=set(),
env_set=set(),
datavars_module=Variables()):
CalculateExtension._parameters_set = parameters_set
CalculateExtension._datavars = datavars_module
self._datavars_module = datavars_module
self._parameters_object = ParametersContainer()
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 = ParametersContainer(parameters_dictionary={})
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 = ParametersContainer(parameters_dictionary={})
self._template_text = template.render(
__datavars__=self._datavars_module,
__parameters__=self._parameters_object
)
@property
def parameters(self):
return self._parameters_object.parameters
@property
def template_text(self):
text, self._template_text = self._template_text, ''
return text