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.

369 lines
18 KiB

4 years ago
# vim: fileencoding=utf-8
#
from collections import OrderedDict
from jinja2 import Environment, PackageLoader
from pprint import pprint
from copy import copy
import re
4 years ago
try:
from lxml.etree.ElementTree import fromstring
except ImportError:
from xml.etree.ElementTree import fromstring
class FormatError(Exception):
pass
4 years ago
class BaseFormat():
FORMAT = 'none'
CALCULATE_VERSION = None
4 years ago
def __init__(self, processing_methods):
self._processing_methods = processing_methods
self._document_dictionary = OrderedDict()
self._item_to_add = OrderedDict()
self.TEMPLATES_DIRECTORY = 'templates'
self._fatal_error_flag = False
self._ready_to_update = False
self._match = False
self._need_finish = False
self._comments_processing = False
self._join_before = False
self._join_before_in_areas = False
# для отладки.
self._line_timer = 0
def _lines_to_dictionary(self, document_lines):
'''Основной метод для парсинга документа. Принимает список строк,
к каждой строке применяет парсеры, определенные для некоторого формата.
Первый парсер, которому удается разобрать строку используется для
формирования словаря.'''
4 years ago
# print('Lines processing...')
for line in document_lines:
# print(self._line_timer, '\t', line)
for processing_method in self._processing_methods:
try:
processing_method(line)
except FormatError as error:
4 years ago
self._document_dictionary = OrderedDict()
raise FormatError("can not parse line: {}, reason: {}".
format(line, str(error)))
4 years ago
if self._is_match():
if self._is_ready_to_update():
self._document_dictionary.update(self._item_to_add)
break
else:
# Действия если не удалось разобрать строку.
self._document_dictionary = OrderedDict()
raise FormatError('can not parse line: {}'.
format(line))
4 years ago
if self._need_finish:
self._finish_method()
def _parse_xml_to_dictionary(self, xml_document_text):
'''Метод для парсинга xml файлов.
Файлы xml предварительно не разбиваются на строки, а разбираются с
помощью модуля lxml. Перевод в словарь осуществляется методами формата,
рекурсивно вызывающимися в зависимости от типа тега.'''
4 years ago
root = fromstring(xml_document_text)
self._document_dictionary = self._processing_methods[root.tag](root)
def print_dictionary(self):
'''Метод для отладки.'''
4 years ago
pprint(self._document_dictionary)
def join_template(self, template):
'''Метод запускающий наложение шаблона.'''
4 years ago
self._join(self._document_dictionary,
template._document_dictionary,
self._join_before)
def _get_list_of_logic_lines(self, text):
'''Метод разбивающий документ на список логических строк -- то есть
учитывающий при разбиении возможность разбиение одной строки на
несколько с помощью бэкслеша. В некоторых форматах переопределен.'''
4 years ago
list_of_lines = []
lines_to_join = []
for line in text.splitlines():
line = line.strip()
if line == '':
continue
if not line.endswith("\\"):
lines_to_join.append(line)
joined_line = "".join(lines_to_join)
list_of_lines.append(joined_line)
lines_to_join = []
else:
lines_to_join.append(line[:-1])
return list_of_lines
def _join(self, original, template, join_before):
'''Основной метод для наложения шаблонов путем объединения их словарей
выполняемого рекурсивно.'''
4 years ago
if template == OrderedDict():
return
if join_before:
forwarded_items = OrderedDict()
for key_value in template:
if key_value[0] == '!':
# Удаление соответствующего элемента из original.
# Сначала получаем ключ без символа действия.
4 years ago
if isinstance(key_value, tuple):
item_to_delete = ('',) + key_value[1:]
elif isinstance(key_value, str):
item_to_delete = key_value[1:]
# Удаляем соответствующий элемент, если он есть в оригинале.
4 years ago
if item_to_delete in original.keys():
del(original[item_to_delete])
4 years ago
elif key_value[0] == '-':
# Замена соответствующего элемента из original.
# Сначала получаем ключ без символа действия.
4 years ago
if isinstance(key_value, tuple):
item_to_replace = ('',) + key_value[1:]
elif isinstance(key_value, str):
item_to_replace = key_value[1:]
# Если соответствующего элемента нет в оригинале -- пропускаем.
4 years ago
if item_to_replace not in original.keys():
continue
# Если секция для замены в шаблоне пустая -- удаляем
# соответствующую секцию.
4 years ago
if isinstance(template[key_value], dict) and\
template[key_value] == OrderedDict():
original.pop(item_to_replace)
continue
# Если символ замены стоит перед параметром, а не перед
# секцией -- просто заменяем значение параметра.
if not isinstance(template[key_value], dict):
original[item_to_replace] = template[key_value]
continue
# Если обработка комментариев включена -- сохраняем
# комментарии к заменяемой секции.
4 years ago
if self._comments_processing:
if '#' in original[item_to_replace]:
replacement = OrderedDict({'#':
original[item_to_replace]
['#']}
)
# накладываем словарь шаблона на пустой словарь, чтобы
# выполнить все управляющие элементы, которые
# могут туда попасть.
self._join(replacement,
template[key_value],
self._join_before_in_areas)
4 years ago
else:
replacement = OrderedDict()
self._join(replacement,
template[key_value],
self._join_before_in_areas)
4 years ago
# Если после наложения шаблона словарь замены оказался
# пустым -- удаляем соотвествующий элемент в оригинале.
if (replacement == OrderedDict() or
replacement.keys() == {'#'}):
del(original[item_to_replace])
else:
original[item_to_replace] = replacement
4 years ago
else:
original[item_to_replace] = OrderedDict()
self._join(original[item_to_replace],
template[key_value],
self._join_before_in_areas)
if (original[item_to_replace] == OrderedDict() or
original[item_to_replace].keys() == {'#'}):
del(original[item_to_replace])
4 years ago
elif key_value not in original.keys():
if isinstance(template[key_value], dict):
dictionary_to_add = OrderedDict()
self._join(dictionary_to_add,
template[key_value],
self._join_before_in_areas)
if dictionary_to_add != OrderedDict():
if not join_before:
original[key_value] = dictionary_to_add
else:
forwarded_items[key_value] = dictionary_to_add
else:
if not join_before:
original[key_value] = template[key_value]
else:
forwarded_items[key_value] = template[key_value]
else:
if isinstance(original[key_value], dict) and \
isinstance(template[key_value], dict):
self._join(original[key_value],
template[key_value],
self._join_before_in_areas)
else:
if self._comments_processing:
original[key_value][-1] = template[key_value][-1]
else:
original[key_value] = template[key_value]
if join_before:
for key_value in reversed(forwarded_items.keys()):
original[key_value] = forwarded_items[key_value]
original.move_to_end(key_value, last=False)
def make_template(self, template):
'''Метод для запуска генерации шаблонов путем сравнения пары исходных
файлов.'''
full_diff, set_to_check = self.compare_dictionaries(
self._document_dictionary,
template._document_dictionary
)
template_object = copy(self)
template_object._document_dictionary = full_diff
return template_object
def compare_dictionaries(self, dict_1, dict_2):
'''Основной метод для генерации шаблонов путем сравнения пары исходных
файлов. Работает рекурсивно.'''
to_remove_dictionary = OrderedDict()
to_add_dictionary = OrderedDict()
to_replace_dictionary = OrderedDict()
unchanged_set = set()
to_remove = dict_1.keys() - dict_2.keys()
if '#' in to_remove:
to_remove.remove('#')
for key in dict_1:
if key in to_remove:
if isinstance(key, tuple):
new_key = ('!', *key[1:])
else:
new_key = '!{}'.format(key)
if isinstance(dict_1[key], dict):
to_remove_dictionary.update({new_key: dict_1[key]})
else:
if self._comments_processing:
to_remove_dictionary.update({new_key:
[dict_1[key][-1]]})
else:
to_remove_dictionary.update({new_key: dict_1[key]})
to_add = dict_2.keys() - dict_1.keys()
if '#' in to_add:
to_add.remove('#')
for key in dict_2:
if key in to_add:
if isinstance(dict_2[key], dict):
section = dict_2[key].copy()
if '#' in section:
section.remove('#')
to_add_dictionary.update({key: section})
else:
if self._comments_processing:
to_add_dictionary.update({key: [dict_2[key][-1]]})
else:
to_add_dictionary.update({key: dict_2[key]})
intersect = dict_1.keys() & dict_2.keys()
for key in intersect:
if (isinstance(dict_1[key], dict) and
isinstance(dict_2[key], dict) and
dict_1[key] != dict_2[key]):
diff, set_to_check = self.compare_dictionaries(dict_1[key],
dict_2[key])
if set_to_check:
to_add_dictionary.update({key: diff})
else:
if isinstance(key, tuple):
new_key = ('-', *key[1:])
else:
new_key = '-{}'.format(key)
to_replace_dictionary.update({new_key:
dict_2[key]})
elif dict_1[key] != dict_2[key]:
if self._comments_processing:
to_add_dictionary.update({key: [dict_2[key][-1]]})
else:
to_add_dictionary.update({key: dict_2[key]})
else:
unchanged_set.add(key)
full_diff = OrderedDict()
full_diff.update(**to_remove_dictionary,
**to_replace_dictionary,
**to_add_dictionary)
return full_diff, unchanged_set
@property
def document_text(self):
'''Метод для получения текста документа. Использует jinja2 для
рендеринга документа.'''
4 years ago
file_loader = PackageLoader('calculate.templates.format',
self.TEMPLATES_DIRECTORY)
formats_environment = Environment(loader=file_loader,
trim_blocks=True,
lstrip_blocks=True)
formats_environment.globals.update(zip=zip)
formats_environment.add_extension('jinja2.ext.do')
template = formats_environment.get_template(self.FORMAT)
4 years ago
document_text = template.render(
document_dictionary=self._document_dictionary
)
return '{}{}'.format(self.header, document_text)
4 years ago
def _finish_method(self):
'''Метод для выполнения заключительных действий парсинга.
Переопределяется в форматах. Вызывается при self._need_finish = True'''
4 years ago
pass
def _is_ready_to_update(self):
'''Метод для проверки флага self._ready_to_update, указывающего, что
сформированная форматом секция документа, находящаяся в
self._item_to_add, может быть добавлена в словарь документа.'''
is_ready, self._ready_to_update = self._ready_to_update, False
4 years ago
return is_ready
def _is_match(self):
'''Метод для проверки флага self._is_match, указывающего что текущий
парсер, использованный форматом, смог распарсить строку и использовать
другие парсеры не нужно.'''
4 years ago
is_match, self._match = self._match, False
return is_match
def _get_header_and_document_text(self, input_text,
template_path,
already_changed=False):
'''Метод для создания заголовка измененного файла и удаления его из
текста исходного файла.'''
header_pattern = (r'^{0}' + r'-' * 79 + r'\n' +
r'{0} Modified by Calculate Utilities [\d\w\.]*\n' +
r'{0} Processed template files:\n' +
r'(?P<template_paths>({0}\s*[/\w\d\-_\.]*\n)+)' +
r'{0}' + r'-' * 79 + r'\n?').format(
self.comment_symbol)
template_paths = [template_path]
if already_changed:
header_regex = re.compile(header_pattern)
parsing_result = header_regex.search(input_text)
template_paths.extend(parsing_result.groupdict()[
'template_paths'].strip().split('\n'))
header = ('{0}' + '-' * 79 + '\n' +
'{0} Modified by Calculate Utilities {1}\n' +
'{0} Processed template files:\n' +
'{0} ' + '\n{0} '.join(template_paths) + '\n' +
'{0}' + '-' * 79 + '\n').format(self.comment_symbol,
self.CALCULATE_VERSION)
document_text = re.sub(header_pattern, '', input_text)
return header, document_text