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.

432 lines
20 KiB

4 years ago
# vim: fileencoding=utf-8
#
from collections import OrderedDict
from jinja2 import Environment, PackageLoader
from typing import Callable, List, Tuple, Union
4 years ago
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):
def __init__(self, message: str, executable: bool = False):
super().__init__(message)
self.executable: bool = executable
class Format:
FORMAT: str = 'none'
CALCULATE_VERSION: Union[str, None] = None
SHEBANG_PATTERN: str = r"^(?P<shebang>#!\s*[\w\d\/]+\n)"
def __init__(self, processing_methods: List[Callable]):
self._processing_methods: List[Callable] = processing_methods
self._document_dictionary: OrderedDict = OrderedDict()
self._item_to_add: OrderedDict = OrderedDict()
4 years ago
self.TEMPLATES_DIRECTORY: str = 'templates'
4 years ago
self._fatal_error_flag: bool = False
self._ready_to_update: bool = False
self._match: bool = False
4 years ago
self._need_finish: bool = False
self._comments_processing: bool = False
4 years ago
self._join_before: bool = False
self._join_before_in_areas: bool = False
4 years ago
# для отладки.
self._line_timer: int = 0
4 years ago
def _lines_to_dictionary(self, document_lines: List[str]) -> None:
'''Основной метод для парсинга документа. Принимает список строк,
к каждой строке применяет парсеры, определенные для некоторого формата.
Первый парсер, которому удается разобрать строку используется для
формирования словаря.'''
4 years ago
for line in document_lines:
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: str) -> None:
'''Метод для парсинга xml файлов.
Файлы xml предварительно не разбиваются на строки, а разбираются с
помощью модуля lxml. Перевод в словарь осуществляется методами формата,
рекурсивно вызывающимися в зависимости от типа тега.'''
4 years ago
root = fromstring(xml_document_text)
self._document_dictionary = self._processing_methods[root.tag](root)
def print_dictionary(self) -> None:
'''Метод для отладки.'''
4 years ago
pprint(self._document_dictionary)
def join_template(self, template: "Format"):
'''Метод запускающий наложение шаблона.'''
4 years ago
self._join(self._document_dictionary,
template._document_dictionary,
self._join_before)
def _get_list_of_logic_lines(self, text: str) -> List[str]:
'''Метод разбивающий документ на список логических строк -- то есть
учитывающий при разбиении возможность разбиение одной строки на
несколько с помощью бэкслеша. В некоторых форматах переопределен.'''
4 years ago
list_of_lines = []
lines_to_join = []
for line in text.splitlines():
line = line.lstrip()
if line.rstrip() == '':
4 years ago
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: OrderedDict,
template: OrderedDict, join_before: bool):
'''Основной метод для наложения шаблонов путем объединения их словарей
выполняемого рекурсивно.'''
4 years ago
if template == OrderedDict():
return
4 years ago
if join_before:
forwarded_items = OrderedDict()
4 years ago
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:]
else:
# Сюда надо вставить исключение.
pass
4 years ago
# Если соответствующего элемента нет в оригинале -- пропускаем.
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:
# Я пока еще не понял почему, но должно быть так:
if not original[key_value] and not template[key_value]:
continue
4 years ago
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: "Format") -> "Format":
'''Метод для запуска генерации шаблонов путем сравнения пары исходных
файлов.'''
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: OrderedDict,
dict_2: OrderedDict
) -> Tuple[OrderedDict, set]:
'''Основной метод для генерации шаблонов путем сравнения пары исходных
файлов. Работает рекурсивно.'''
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) -> str:
'''Метод для получения текста документа. Использует 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) -> bool:
'''Метод для проверки флага 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) -> bool:
'''Метод для проверки флага 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: str,
template_path: str,
already_changed: bool = False,
check_shebang: bool = False
) -> Tuple[str, str]:
'''Метод для создания заголовка измененного файла и удаления его из
текста исходного файла.'''
template_paths = []
if check_shebang:
# Удаление #!
shebang_regex = re.compile(self.SHEBANG_PATTERN)
shebang_result = shebang_regex.search(input_text)
if shebang_result is not None:
print("string:", shebang_result.string)
print("groupdict:", shebang_result.groupdict())
shebang = shebang_result.groupdict()['shebang']
input_text = shebang_regex.sub("", input_text)
else:
shebang = ""
header_pattern = self._get_header_pattern()
header_regex = re.compile(header_pattern)
parsing_result = header_regex.search(input_text)
if already_changed and self.comment_symbol and parsing_result:
for template in parsing_result.\
groupdict()['template_paths'].strip().split('\n'):
if template.startswith(self.comment_symbol):
template = template[len(self.comment_symbol):]
template_paths.append(template.strip())
template_paths.append(template_path)
header = self._make_header(template_paths)
document_text = re.sub(header_pattern, '', input_text)
if check_shebang:
return header, document_text, shebang
else:
return header, document_text
def _make_header(self, template_paths: list) -> str:
if not self.comment_symbol:
return ""
elif self.comment_symbol in ("xml", "XML"):
return ("<?xml version='1.0' encoding='UTF-8'?>\n" +
'<!--\n' +
'Modified by Calculate Utilities {}\n' +
'Processed template files:\n' +
'\n'.join(template_paths) + '\n' +
'-->\n').format(self.CALCULATE_VERSION)
else:
return ('{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)
def _get_header_pattern(self) -> str:
if self.comment_symbol in {"xml", "XML"}:
return (r'<!--\n' +
r'\s*Modified by Calculate Utilities [\d\w\.]*\n' +
r'\s*Processed template files:\n' +
r'\s*(?P<template_paths>(\s*[/\w\d\-_\.]*\n)+)' +
r'-->\n?')
else:
return (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)
def __bool__(self) -> bool:
return bool(self._document_dictionary)