From a5b710666deccb6dadf3db2bcb50ea84233f93ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=D0=BE=D0=B2=20=D0=94=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=81?= Date: Wed, 5 Aug 2020 19:56:19 +0300 Subject: [PATCH] Support of a tables creation and modification is added in the NamespaceIniFiller. Creation the variables and namespaces using calculate.ini is available only in the custom section now. --- calculate/utils/package.py | 6 +- calculate/vars/alt_datavars.py | 12 +- calculate/vars/alt_vars_loader.py | 211 ++++++++++++++++++++++-------- tests/vars/test_alt_vars.py | 77 ++++++++++- 4 files changed, 240 insertions(+), 66 deletions(-) diff --git a/calculate/utils/package.py b/calculate/utils/package.py index 05a673c..8b5149e 100644 --- a/calculate/utils/package.py +++ b/calculate/utils/package.py @@ -647,9 +647,9 @@ class Package: if (file_name != '/' and (file_name not in self.contents_dictionary or self.contents_dictionary[file_name]['type'] != 'dir')): - self.add_dir(os.path.dirname(file_name)) - contents_item = OrderedDict({'type': 'dir'}) - self.contents_dictionary[file_name] = contents_item + self.add_dir(os.path.dirname(file_name)) + contents_item = OrderedDict({'type': 'dir'}) + self.contents_dictionary[file_name] = contents_item def add_sym(self, file_name, target_path=None): '''Метод для добавления в CONTENTS символьных ссылок.''' diff --git a/calculate/vars/alt_datavars.py b/calculate/vars/alt_datavars.py index 0ab77a1..bd8da13 100644 --- a/calculate/vars/alt_datavars.py +++ b/calculate/vars/alt_datavars.py @@ -255,13 +255,20 @@ class Table: " with type '{}'".format(row, type(row))) - def get_table(self): + def get_table(self) -> List[dict]: return self._rows def add_row(self, row: dict): self._check_columns(row) self._rows.append(row) + def change_row(self, row: dict, index: int) -> None: + self._check_columns(row) + self._rows[index] = row + + def clear(self) -> None: + self._rows.clear() + def _check_columns(self, row: dict) -> None: '''Метод для проверки наличия в хэше только тех полей, которые соответствуют заданным для таблицы колонкам.''' @@ -291,6 +298,9 @@ class Table: return key in self._values return False + def __len__(self): + return len(self._rows) + class TableType(VariableType): name = 'table' diff --git a/calculate/vars/alt_vars_loader.py b/calculate/vars/alt_vars_loader.py index 41de746..0e69e03 100644 --- a/calculate/vars/alt_vars_loader.py +++ b/calculate/vars/alt_vars_loader.py @@ -7,14 +7,14 @@ import site from calculate.vars.datavars import Variable, Namespace, HashVariable,\ TableVariable, IniCreated, DefaultValue from calculate.vars.alt_datavars import NamespaceNode, VariableNode,\ - StringType, ListType, IntegerType,\ - FloatType, IniType + ListType, IntegerType,\ + FloatType, IniType, TableType from calculate.utils.gentoo import ProfileWalker from calculate.utils.fs import readFile from calculate.utils.files import list_directory from pyparsing import Literal, Word, ZeroOrMore, Group, Optional, restOfLine,\ empty, printables, OneOrMore, lineno, line, col, SkipTo,\ - LineEnd, Combine + LineEnd, Combine, nums from enum import Enum @@ -46,17 +46,27 @@ class CalculateIniParser: comment = comment_symbol + Optional(restOfLine) - section_name = Word(printables+'\t', excludeChars='[]') + section_name = (lbrack.suppress() + (~Word(nums) + + Word(printables+'\t', + excludeChars='[]')) + + rbrack.suppress()) value_name = Word(printables+'\t', excludeChars='=-+') # non_comma = Word(printables+'\t', excludeChars=',') clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress() + row_index = lbrack.suppress() + Word(nums) + rbrack.suppress() - section_start = Group(OneOrMore(lbrack.suppress() + section_name - + rbrack.suppress()) - + (clear_section | ~lbrack()) - + LineEnd().suppress()) + namespace_start = Group(OneOrMore(section_name) + + (clear_section | ~lbrack) + + LineEnd().suppress()) + + table_start = Group(OneOrMore(section_name) + + (row_index | clear_section | ~lbrack) + + LineEnd().suppress()) + + section_start = (namespace_start('namespace') | + table_start('table')) # Если содержимое ini-файла не предваряется заголовком секции, # значит эта строка ошибочна. @@ -80,6 +90,38 @@ class CalculateIniParser: | unexpected) self.ini_section_parser.ignore(comment) + def parse(self, data: str): + for tokens, start, end in self.ini_section_parser.scanString(data): + if tokens.getName() == "error": + continue + + section, defkeys = tokens + + print('section type: {}'.format(section.getName())) + if section.getName() == 'namespace': + section_list = section.asList() + if section_list[-1] == []: + yield {'clear_section': (section_list[:-1], )} + else: + yield {'start_section': (section_list, )} + + for defkey in defkeys: + if defkey.getName() == "error": + continue + yield {'define_key': (defkey[0], defkey[2], + self.operations[defkey[1]])} + else: + table_list = section.asList() + if table_list[-1] == []: + yield {'clear_table': (table_list[:-1], )} + else: + table_values = {} + for defkey in defkeys: + if defkey.getName() == "error": + continue + table_values.update({defkey[0]: defkey[2]}) + yield {'start_table': (table_list, table_values)} + def _unexpected_token(self, string, location, tokens): '''Метод вызываемый парсером, если обнаружена некорректная строка, предназначен для получения некорректной строки и ее дальнейшего @@ -95,36 +137,21 @@ class CalculateIniParser: self._errors = [] return errors - def parse(self, data): - for tokens, start, end in self.ini_section_parser.scanString(data): - if tokens.getName() == "error": - continue - - section, defkeys = tokens - - section_list = section.asList() - if section_list[-1] == []: - yield {'clear_section': (section_list[:-1], )} - else: - yield {'start_section': (section.asList(), )} - - for defkey in defkeys: - if defkey.getName() == "error": - continue - yield {'define_key': (section.asList(), defkey[0], defkey[2], - self.operations[defkey[1]])} - class NamespaceIniFiller: '''Класс, предназначенный для наполнения Namespace объекта переменными из calculate.ini файла.''' - def __init__(self): + available_sections = {'custom'} + + def __init__(self, restrict_creation=True): self.ini_parser = CalculateIniParser() + self.restricted = restrict_creation + self.modify_only = False def error(self, lineno, error_message): self.errors.append(lineno, error_message) - def fill(self, namespace, ini_file_text): + def fill(self, namespace: NamespaceNode, ini_file_text: str) -> None: '''Метод для разбора calculate.ini файла и добавления всех его переменных в указанное пространство имен.''' self.namespace = namespace @@ -136,68 +163,152 @@ class NamespaceIniFiller: def _line_processor(self, start_section=None, clear_section=None, + start_table=None, + clear_table=None, define_key=None, error=None, **kwargs): if start_section is not None: self.start_section(*start_section) elif clear_section is not None: self.clear_section(*clear_section) + elif start_table is not None: + self.start_table(*start_table) + elif clear_table is not None: + self.clear_table(*clear_table) elif define_key is not None: self.define_key(*define_key) for error in self.ini_parser.errors: self.set_error(*error) - def start_section(self, sections): + def start_section(self, sections: str) -> None: '''Метод для получения доступа и создания пространств имен.''' + if self.restricted: + self.modify_only = sections[0] not in self.available_sections self.current_namespace = self.namespace for section in sections: if section not in self.current_namespace.namespaces: - self.current_namespace.add_namespace(NamespaceNode(section)) + if not self.modify_only: + self.current_namespace.add_namespace( + NamespaceNode(section)) + else: + self.current_namespace = None + return self.current_namespace = self.current_namespace.namespaces[section] - def clear_section(self, sections): + def clear_section(self, sections: list) -> None: '''Метод для очистки пространства имен.''' current_namespace = self.namespace + for section in sections: - if section not in current_namespace.namespaces: + if section in current_namespace.namespaces: + current_namespace = current_namespace.namespaces[section] + elif (section in current_namespace.variables and + current_namespace.variables[section].variable_type + is TableType): + table_variable = current_namespace.variables[section] + table_to_clear = table_variable.get_value() + table_to_clear.clear() + table_variable.source = table_to_clear + else: return - current_namespace = current_namespace.namespaces[section] current_namespace.clear() - def change_value(self, key, value): + def start_table(self, sections: str, row) -> None: + '''Метод для создания и модификации таблиц.''' + if self.restricted: + self.modify_only = sections[0] not in self.available_sections + self.current_namespace = self.namespace + row_number = int(sections.pop()) + table_name = sections.pop() + for section in sections: + if section not in self.current_namespace.namespaces: + if not self.modify_only: + self.current_namespace.add_namespace( + NamespaceNode(section)) + else: + self.current_namespace = None + return + self.current_namespace = self.current_namespace.namespaces[section] + if table_name not in self.current_namespace.variables: + if not self.modify_only: + table_variable = VariableNode(table_name, + self.current_namespace, + variable_type=TableType, + source=[row]) + else: + table_variable = self.current_namespace.variables[table_name] + table = table_variable.get_value() + if len(table) < row_number: + table.change_row(row, row_number) + else: + table.add_row(row) + table_variable.source = table + + def define_key(self, key: str, value: str, optype) -> None: + if self.current_namespace is None: + return + + if optype == Define.assign: + if key not in self.current_namespace: + self.define_variable(key, value) + else: + self.change_value(key, value) + elif optype == Define.append: + if key not in self.current_namespace: + self.define_variable(key, value) + else: + self.append_value(key, value) + elif optype == Define.remove: + if key not in self.current_namespace: + self.define_variable(key, value) + else: + self.remove_value(key, value) + + def change_value(self, key: str, value: str) -> None: + '''Метод для изменения значения переменной.''' variable = self.current_namespace[key] variable.source = value - def define_variable(self, key, value): + def define_variable(self, key: str, value: str) -> None: '''Метод для создания переменных в calculate.ini файле.''' - print("define variable: '{}' with value: '{}'".format(key, value)) - VariableNode(key, self.current_namespace, variable_type=IniType, - source=value) + if not self.modify_only: + VariableNode(key, self.current_namespace, variable_type=IniType, + source=value) + else: + # TODO Какая-то обработка ошибки. + pass - def append_value(self, key, value): + def append_value(self, key: str, value: str) -> None: '''Метод выполняющий действия возложенные на оператор +=.''' variable = self.current_namespace[key] variable_value = variable.get_value() + if variable.variable_type is IniType: value_list = value.split(',') variable_list = variable_value.split(',') + for item in value_list: if item not in variable_list: variable_list.append(item.strip()) variable_value = ','.join(variable_list) + elif variable.variable_type is ListType: value_list = value.split(',') + for item in value_list: if item not in variable_value: variable_value.append(item.strip()) + elif variable.variable_type is IntegerType: variable_value += int(value) + elif variable.variable_type is FloatType: variable_value += float(value) + variable.source = variable_value - def remove_value(self, key, value): + def remove_value(self, key: str, value: str) -> None: '''Метод выполняющий действия возложенные на оператор -=.''' variable = self.current_namespace[key] variable_value = variable.get_value() @@ -225,24 +336,8 @@ class NamespaceIniFiller: variable.source = variable_value - def define_key(self, section, key, value, optype): - if optype == Define.assign: - if key not in self.current_namespace: - self.define_variable(key, value) - else: - self.change_value(key, value) - elif optype == Define.append: - if key not in self.current_namespace: - self.define_variable(key, value) - else: - self.append_value(key, value) - elif optype == Define.remove: - if key not in self.current_namespace: - self.define_variable(key, value) - else: - self.remove_value(key, value) - def set_error(self, line, lineno, col): + '''Метод для добавления ошибки в лог.''' self.error(lineno, "Syntax error: {}".format(line)) diff --git a/tests/vars/test_alt_vars.py b/tests/vars/test_alt_vars.py index 18036bd..a823858 100644 --- a/tests/vars/test_alt_vars.py +++ b/tests/vars/test_alt_vars.py @@ -478,6 +478,8 @@ class TestNamespace: def test_if_calculate_ini_file_is_used_just_to_create_some_variables__the_namespace_filler_will_create_them(self): Namespace.reset() datavars = Namespace.datavars + namespace_filler = NamespaceIniFiller(restrict_creation=False) + calculate_ini_text = """ [os] test = 123 @@ -486,7 +488,6 @@ test = 123 shortname = CLD fullname = Calculate Linux Desktop """ - namespace_filler = NamespaceIniFiller() namespace_filler.fill(datavars, calculate_ini_text) assert datavars.os.test == '123' assert datavars.os.linux.shortname == 'CLD' @@ -495,7 +496,7 @@ fullname = Calculate Linux Desktop def test_if_a_calculate_ini_file_is_used_to_create_some_variables_and_an_other_one_is_used_to_update_value_of_the_variable__the_namespace_filler_creates_them_using_a_first_calculate_ini_and_then_changes_value_using_a_second_calculate_ini(self): Namespace.reset() datavars = Namespace.datavars - namespace_filler = NamespaceIniFiller() + namespace_filler = NamespaceIniFiller(restrict_creation=False) first_ini_text = """ [os] @@ -520,7 +521,7 @@ shortname = CLDX def test_if_calculate_ini_file_creates_a_variable_and_then_uses_append_operation_to_append_any_value_to_the_created_variable__the_namespace_filler_appends_the_value_to_the_created_variable(self): Namespace.reset() datavars = Namespace.datavars - namespace_filler = NamespaceIniFiller() + namespace_filler = NamespaceIniFiller(restrict_creation=False) first_ini_text = """ [os] test = 123 @@ -542,7 +543,7 @@ test += asdf,qwer,zxcv def test_if_calculate_ini_file_creates_a_variable_and_then_uses_remove_operation_to_remove_any_values_in_the_created_variable__the_namespace_filler_removes_the_values_from_the_created_variable(self): Namespace.reset() datavars = Namespace.datavars - namespace_filler = NamespaceIniFiller() + namespace_filler = NamespaceIniFiller(restrict_creation=False) first_ini_text = """ [os] test = 123,345,asdf,qwer,zxcv @@ -565,3 +566,71 @@ test -= asdf,zxcv namespace_filler.fill(datavars, third_ini_text) assert datavars.os.test == "123,qwer" + + def test_custom_and_not_custom_creation(self): + Namespace.reset() + datavars = Namespace.datavars + namespace_filler = NamespaceIniFiller() + + with Namespace('os'): + Variable('var_1', type=StringType, source='value_1') + Variable('var_2', type=StringType, source='value_2') + + first_ini_text = """ +[os] +var_1 = other_value +var_3 = value_3 + +[custom][test_1] +var_1 = value_1 + +[custom][test_2] +var_1 = value_1 +""" + + namespace_filler.fill(datavars, first_ini_text) + assert datavars.os.var_1 == "other_value" + assert 'var_3' not in datavars.os + assert datavars.custom.test_1.var_1 == "value_1" + assert datavars.custom.test_2.var_1 == "value_1" + + def test_table_from_ini(self): + Namespace.reset() + datavars = Namespace.datavars + namespace_filler = NamespaceIniFiller() + + with Namespace('os'): + Variable('var_1', type=StringType, source='value_1') + Variable('var_2', type=StringType, source='value_2') + + first_ini_text = """ +[custom][test][0] +name = name_0 +value = value_0 + +[custom][test][1] +name = name_1 +value = value_1 + +[custom][test][2] +name = name_2 +value = value_2 +""" + + namespace_filler.fill(datavars, first_ini_text) + assert datavars.custom.test.get_table() ==\ + [{'name': 'name_0', 'value': 'value_0'}, + {'name': 'name_1', 'value': 'value_1'}, + {'name': 'name_2', 'value': 'value_2'}] + + second_ini_text = """ +[custom][test][] + +[custom][test][0] +name = name_0 +value = value_0 +""" + + namespace_filler.fill(datavars, second_ini_text) + assert datavars.custom.test.get_table() ==\ + [{'name': 'name_0', 'value': 'value_0'}]