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

# 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