# 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 ''.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 ''.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