From 471f957daaf725adb4bf29ed5b5320ff3f9c9048 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: Tue, 21 Jul 2020 18:19:08 +0300 Subject: [PATCH] Tested variables invalidation and creation of the depended variables. Added check of the cycles while calculating variables. --- calculate/vars/alt_datavars.py | 234 +++++++++++++++++++++++-------- tests/utils/test_calculateini.py | 11 +- tests/vars/test_alt_vars.py | 45 ++++-- 3 files changed, 219 insertions(+), 71 deletions(-) diff --git a/calculate/vars/alt_datavars.py b/calculate/vars/alt_datavars.py index 6f31e5c..e0bf3e5 100644 --- a/calculate/vars/alt_datavars.py +++ b/calculate/vars/alt_datavars.py @@ -1,5 +1,6 @@ import re import ast +from contextlib import contextmanager from inspect import signature, getsource from types import FunctionType, LambdaType @@ -19,6 +20,19 @@ class VariableTypeError(Exception): pass +class DependenceError(Exception): + pass + + +class CyclicVariableError(VariableError): + def __init__(self, *queue): + self.queue = queue + + def __str__(self): + return "Cyclic dependence in variables: {}".format(", ".join( + self.queue[:-1])) + + class StringType(VariableType): pass @@ -51,89 +65,139 @@ class ReadonlyType(VariableType): pass +class Dependence: + def __init__(self, *variables, depend=None): + self._subscriptions = variables + self.depend_function = depend + + def check(self): + if self.depend_function is None: + if len(self._subscriptions) > 1: + raise DependenceError('the depend function is needed if the' + ' number of dependencies is more than' + ' one') + elif len(self._subscriptions) == 0: + raise DependenceError('dependence is set without variables') + else: + self._check_function(self.depend_function) + + def calculate_value(self): + '''Метод для расчета значения переменной с использованием зависимостей + и заданной для них функции.''' + try: + if self.depend_function is None: + return self._subscriptions[-1].get_value() + return self.depend_function(*(subscription.get_value() + for subscription + in self._subscriptions)) + except Exception as error: + raise DependenceError('can not calculate using dependencies: {}' + ' reason: {}'.format(', '.join( + self._subscriptions), + str(error))) + + @property + def subscriptions(self): + return self._subscriptions + + def _check_function(self, function_to_check: FunctionType): + '''Метод для проверки того, возращает ли функция какое-либо значение, а + также того, совпадает ли число подписок с числом аргументов этой + функции.''' + if not isinstance(function_to_check, LambdaType): + # Если функция не лямбда, проверяем есть ли у нее возвращаемое + # значение. + for node in ast.walk(ast.parse(getsource(function_to_check))): + if isinstance(node, ast.Return): + break + else: + raise VariableError("the depend function does not return" + " anything in variable") + + # Проверяем совпадение количества аргументов функции и заданных для + # функции переменных. + function_signature = signature(function_to_check) + if not len(self._subscriptions) == len(function_signature.parameters): + raise VariableError("the depend function takes {} arguments," + " while {} is given".format( + len(function_signature.parameters), + len(self._subscriptions))) + + class VariableNode: - def __init__(self, name, namespace, variable_type=StringType, value=None): + def __init__(self, name, namespace, variable_type=StringType, source=None): self.name = name if issubclass(variable_type, VariableType): self.variable_type = variable_type else: raise VariableTypeError('variable_type must be VariableType' ', but not {}'.format(type(variable_type))) - self.set_function = None - # Если значение переменной None -- значит она обнулена. - self.value = None + self.calculating = False + + self.namespace = namespace + self.namespace.add_variable(self) self.subscribers = set() - self.subscriptions = tuple() - if value is not None: - self.update_value(value) + # Если значение переменной None -- значит она обнулена. + self.value = None + # Источник значения переменной, может быть значением, а может быть + # зависимостью. + self._source = source + if source is not None: + self.update_value() - self.namespace = namespace - namespace.add_variable(self) + def update_value(self): + if self.calculating: + raise CyclicVariableError(self.name) - def update_value(self, value=None): self._invalidate() - if value is not None: - self.value = value - elif self.set_function is not None and self.subscriptions: - self.value = self.set_function(*(subscription.value for - subscription in - self.subscriptions)) - elif self.subscriptions: - # Пока что будем полагать, что в такой ситуации берем значение из - # последней подписки. - self.value = self.subscriptions[-1].get_value() - else: + if self._source is None: raise VariableError("No sources to update variable '{}'". format(self.fullname)) + if isinstance(self._source, Dependence): + with self._start_calculate(): + try: + self.value = self._source.calculate_value() + except CyclicVariableError as error: + raise CyclicVariableError(self.name, *error.queue) + else: + self.value = self._source + + @property + def source(self): + return self._source + + @source.setter + def source(self, source): + self._invalidate() + self._source = source + if isinstance(source, Dependence): + for subscription in source._subscriptions: + subscription.subscribers.add(self) + def _invalidate(self): self.value = None for subscriber in self.subscribers: subscriber._invalidate() + @contextmanager + def _start_calculate(self): + '''Менеджер контекста устанавливающий флаг, указывающий, что данная + переменная в состоянии расчета.''' + try: + self.calculating = True + yield self + finally: + self.calculating = False + def get_value(self): if self.value is None: self.update_value() return self.value - def subscribe(self, *variables, set_function=None): - self.subscriptions = variables - for subscription in self.subscriptions: - subscription.subscribers.add(self) - - self._check_function(set_function) - self.set_function = set_function - - def subscribe(self, *variables, set_function=None): - - def _check_function(self, function_to_check: FunctionType): - '''Метод для проверки того, возращает ли функция какое-либо значение, а - также того, совпадает ли число подписок с числом аргументов этой - функции.''' - if not isinstance(function_to_check, LambdaType): - # Если функция не лямбда, проверяем есть ли у нее возвращаемое - # значение. - for node in ast.walk(ast.parse(getsource(function_to_check))): - if isinstance(node, ast.Return): - break - else: - raise VariableError("Variable set function does not return" - " anything in variable '{}'.".format( - self.fullname)) - - # Проверяем совпадение количества аргументов функции и заданных для - # функции переменных. - function_signature = signature(function_to_check) - if not len(self.subscriptions) == len(function_signature.parameters): - raise VariableError("Variable set function takes {} arguments," - " while {} is given for in variable '{}'." - .format(len(function_signature.parameters), - len(self.subscriptions), - self.fullname)) - @property def fullname(self) -> str: if self.namespace: @@ -189,5 +253,63 @@ class NamespaceNode: variable_name=name, namespace_name=self.name)) + def __contains__(self, name): + return name in self.childs or name in self.variables + def __repr__(self): return ''.format(self.fullname) + + +class VariableAPI: + def __init__(self): + self.current_namespace = None + self.errors = [] + + def __call__(self, name: str, source=None): + variable = VariableNode(name, self.current_namespace) + if isinstance(source, Dependence): + try: + source.check() + variable.source = source + except DependenceError as error: + self.errors.append('Dependence error: {} in variable: {}'. + format(str(error), variable.fullname)) + else: + variable.source = source + return variable + + +class NamespaceAPI: + def __init__(self, var_fabric: VariableAPI, + datavars_root=NamespaceNode('')): + self._datavars = datavars_root + self._variables_fabric = var_fabric + self.namespace_stack = [datavars_root] + self._variables_fabric.current_namespace = self._datavars + + @property + def datavars(self): + return self._datavars + + def set_root(self, datavars_root: NamespaceNode): + self._datavars = datavars_root + self.namespace_stack = [datavars_root] + self._variables_fabric.current_namespace = self._datavars + + @contextmanager + def __call__(self, namespace_name): + new_namespace = NamespaceNode( + namespace_name, + parent=self.namespace_stack[-1]) + self.namespace_stack[-1].add_namespace(new_namespace) + self.namespace_stack.append(new_namespace) + self._variables_fabric.current_namespace = new_namespace + try: + yield self + finally: + self._variables_fabric.current_namespace =\ + self.namespace_stack.pop() + + +Variable = VariableAPI() +Namespace = NamespaceAPI(Variable) diff --git a/tests/utils/test_calculateini.py b/tests/utils/test_calculateini.py index 71972ad..804af21 100644 --- a/tests/utils/test_calculateini.py +++ b/tests/utils/test_calculateini.py @@ -1,5 +1,4 @@ import pytest -from mock import call from calculate.vars.vars_loader import CalculateIniParser, Define @@ -11,7 +10,7 @@ class TestCalculateIni: # ns = Namespace(varPath=None) # assert ns.varPath is None - def test_section_values(self, mocker): + def test_section_values(self): ini_parser = CalculateIniParser() input_text = ("[section]\n" @@ -26,7 +25,7 @@ class TestCalculateIni: for parsed_line, result_line in zip(parsed_lines, parse_result): assert parsed_line == result_line - def test_simple_calculate_ini_with_comments(self, mocker): + def test_simple_calculate_ini_with_comments(self): ini_parser = CalculateIniParser() input_text = ("[section]\n" @@ -48,7 +47,7 @@ class TestCalculateIni: for parsed_line, result_line in zip(parsed_lines, parse_result): assert parsed_line == result_line - def test_some_complex_section_calculate_ini(self, mocker): + def test_some_complex_section_calculate_ini(self): ini_parser = CalculateIniParser() input_text = ("[section][sub]\n" @@ -76,7 +75,7 @@ class TestCalculateIni: for parsed_line, result_line in zip(parsed_lines, parse_result): assert parsed_line == result_line - def test_error(self, mocker): + def test_error(self): ini_parser = CalculateIniParser() input_text = ("[section\n" @@ -116,7 +115,7 @@ class TestCalculateIni: for parsed_line, result_line in zip(parsed_lines, parse_result): assert parsed_line == result_line - def test_clear_section(self, mocker): + def test_clear_section(self): ini_parser = CalculateIniParser() parse_result = next(ini_parser.parse("[section][test][]\n")) diff --git a/tests/vars/test_alt_vars.py b/tests/vars/test_alt_vars.py index 6e4d1e7..0495d22 100644 --- a/tests/vars/test_alt_vars.py +++ b/tests/vars/test_alt_vars.py @@ -1,5 +1,6 @@ import pytest -from calculate.vars.alt_datavars import NamespaceNode, VariableNode +from calculate.vars.alt_datavars import NamespaceNode, VariableNode,\ + Namespace, Variable, Dependence @pytest.mark.alt_vars @@ -29,8 +30,8 @@ class TestNamespace: def test_if_two_VariableNode_objects_are_initialized_and_added_to_a_namespace__the_NamespaceNode_object_contains_this_variables_and_can_be_used_to_get_variables_values_and_variables_have_namespace_name_in_their_fullnames(self): namespace_1 = NamespaceNode('namespace_1') - variable_1 = VariableNode('var_1', namespace_1, value='value_1') - variable_2 = VariableNode('var_2', namespace_1, value='value_2') + variable_1 = VariableNode('var_1', namespace_1, source='value_1') + variable_2 = VariableNode('var_2', namespace_1, source='value_2') assert namespace_1.namespaces == dict() assert namespace_1.variables == {'var_1': variable_1, @@ -46,16 +47,42 @@ class TestNamespace: def test_if_a_variable_subscribed_to_two_other_variables_using_set_function__the_(self): namespace_1 = NamespaceNode('namespace_1') - variable_1 = VariableNode('var_1', namespace_1, value=2) - variable_2 = VariableNode('var_2', namespace_1, value=4) + variable_1 = VariableNode('var_1', namespace_1, source=2) + variable_2 = VariableNode('var_2', namespace_1, source=4) namespace_2 = NamespaceNode('namespace_2') variable = VariableNode('var_1', namespace_2) + variable.source = Dependence(variable_1, variable_2, + depend=lambda var_1, var_2: + 'greater' if var_1 > var_2 else 'less') - variable.subscribe(variable_1, variable_2, - set_function=lambda var_1, var_2: - 'greater' if var_1 > var_2 else 'less') assert namespace_2.var_1 == 'less' - namespace_1.get_variable_node('var_1').update_value(5) + namespace_1.get_variable_node('var_1').source = 5 assert namespace_2.var_1 == 'greater' + + def test_api(self): + with Namespace('namespace_1'): + Variable('var_1', source='val_1') + + var_1 = Variable('var_2', source=2) + + var_2 = Variable('var_3', source=4) + + with Namespace('namespace_2'): + Variable('var_1', source=Dependence( + var_1, var_2, + depend=lambda arg_1, arg_2: + 'greater' if arg_1 > arg_2 else 'less')) + with Namespace('namespace_2_1'): + Variable('var_1', source='val_1') + + datavars = Namespace.datavars + assert datavars.namespace_1.var_1 == 'val_1' + assert datavars.namespace_1.var_2 == 2 + assert datavars.namespace_1.var_3 == 4 + assert datavars.namespace_2.var_1 == 'less' + assert datavars.namespace_2.namespace_2_1.var_1 == 'val_1' + + datavars.namespace_1.get_variable_node('var_2').source = 5 + assert datavars.namespace_2.var_1 == 'greater'