diff --git a/calculate/vars/alt_datavars.py b/calculate/vars/alt_datavars.py index e0bf3e5..e3db5f4 100644 --- a/calculate/vars/alt_datavars.py +++ b/calculate/vars/alt_datavars.py @@ -1,5 +1,6 @@ import re import ast +import dis from contextlib import contextmanager from inspect import signature, getsource from types import FunctionType, LambdaType @@ -12,15 +13,19 @@ class VariableType: return True +class DependenceError(Exception): + pass + + class VariableError(Exception): pass -class VariableTypeError(Exception): +class VariableTypeError(VariableError): pass -class DependenceError(Exception): +class VariableNotFoundError(VariableError): pass @@ -41,7 +46,9 @@ class IntegerType(VariableType): integer_pattern = re.compile(r"^-?\d+$") @classmethod - def check(self, value: str) -> bool: + def check(self, value) -> bool: + if isinstance(value, int): + return True return self.integer_pattern.match(value) @@ -49,12 +56,18 @@ class FloatType(VariableType): float_pattern = re.compile(r"^-?\d+(\.\d+)?$") @classmethod - def check(self, value: str) -> bool: + def check(self, value) -> bool: + if isinstance(value, float): + return True return self.integer_pattern.match(value) class BooleanType(VariableType): - pass + bool_available = {'True', 'False'} + + @classmethod + def check(self, value) -> bool: + return isinstance(value, bool) or value in self.bool_available class ListType(VariableType): @@ -66,11 +79,16 @@ class ReadonlyType(VariableType): class Dependence: + current_namespace = None + datavars_root = None + 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' @@ -84,16 +102,19 @@ class Dependence: def calculate_value(self): '''Метод для расчета значения переменной с использованием зависимостей и заданной для них функции.''' + args_values = tuple(subscription.get_value() for subscription + in self._subscriptions) + print('args_values: {}'.format(args_values)) 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)) + return args_values[-1] + return self.depend_function(*args_values) except Exception as error: raise DependenceError('can not calculate using dependencies: {}' ' reason: {}'.format(', '.join( - self._subscriptions), + [subscription.fullname + for subscription + in self._subscriptions]), str(error))) @property @@ -123,6 +144,65 @@ class Dependence: len(function_signature.parameters), len(self._subscriptions))) + def _get_variables(self, variables_names): + print('current_namespace: {}'.format(self.current_namespace.fullname)) + print('root:') + for namespace_name, namespace in self.datavars_root: + print(namespace_name) + + variables = list() + for variable_name in variables_names: + result = None + name_parts = variable_name.split('.') + if not name_parts[0]: + namespace = self.current_namespace + index = 1 + while not name_parts[index]: + namespace = namespace.parent + index += 1 + name_parts = name_parts[index:] + else: + namespace = self.datavars_root + result = namespace + for part in name_parts: + result = result[part] + variables.append(result) + + return variables + + def __ne__(self, other): + return not self == other + + def __eq__(self, other): + if not isinstance(other, Dependence): + return False + + if not self._compare_depend_functions(self.depend_function, + other.depend_function): + return False + for l_var, r_var in zip(self._subscriptions, other._subscriptions): + if l_var != r_var: + return False + return True + + def _compare_depend_functions(self, l_func: FunctionType, + r_func: FunctionType) -> bool: + '''Метод для сравнения функций путем сравнения инструкций, полученных + в результате их дизассемблирования.''' + l_instructions = list(dis.get_instructions(l_func)) + r_instructions = list(dis.get_instructions(r_func)) + + for l_instr, r_instr in zip(l_instructions, r_instructions): + if l_instr.opname != r_instr.opname: + return False + if l_instr.arg != r_instr.arg: + return False + if ((l_instr.argval != l_instr.argrepr) and + (r_instr.argval != r_instr.argrepr)): + if r_instr.argval != l_instr.argval: + return False + return True + class VariableNode: def __init__(self, name, namespace, variable_type=StringType, source=None): @@ -151,8 +231,6 @@ class VariableNode: if self.calculating: raise CyclicVariableError(self.name) - self._invalidate() - if self._source is None: raise VariableError("No sources to update variable '{}'". format(self.fullname)) @@ -172,16 +250,18 @@ class VariableNode: @source.setter def source(self, source): - self._invalidate() - self._source = source - if isinstance(source, Dependence): - for subscription in source._subscriptions: - subscription.subscribers.add(self) + if self._source != 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() + if self.value is not None: + self.value = None + for subscriber in self.subscribers: + subscriber._invalidate() @contextmanager def _start_calculate(self): @@ -194,6 +274,7 @@ class VariableNode: self.calculating = False def get_value(self): + '''Метод для получения значения переменной.''' if self.value is None: self.update_value() return self.value @@ -233,20 +314,24 @@ class NamespaceNode: else: return self.name - def get_variable_node(self, name): - if name in self.variables: - return self.variables[name] + def __getattr__(self, name: str): + '''Метод возвращает ноду пространства имен или значение переменной.''' + if name in self.namespaces: + return self.namespaces[name] + elif name in self.variables: + return self.variables[name].get_value() else: - raise VariableError("Variable '{variable_name}' is not found in" - " the namespace '{namespace_name}'".format( + raise VariableError("'{variable_name}' is not found in the" + " namespace '{namespace_name}'".format( variable_name=name, namespace_name=self.name)) - def __getattr__(self, name: str): + def __getitem__(self, name: str): + '''Метод возвращает ноду пространства имен или ноду переменной.''' if name in self.namespaces: return self.namespaces[name] elif name in self.variables: - return self.variables[name].get_value() + return self.variables[name] else: raise VariableError("'{variable_name}' is not found in the" " namespace '{namespace_name}'".format( @@ -261,12 +346,20 @@ class NamespaceNode: class VariableAPI: + '''Класс для создания переменных при задании их через + python-скрипты.''' def __init__(self): self.current_namespace = None + # TODO Продумать другой способ обработки ошибок. self.errors = [] def __call__(self, name: str, source=None): - variable = VariableNode(name, self.current_namespace) + '''Метод для создания переменных внутри with Namespace('name').''' + if name not in self.current_namespace.variables: + variable = VariableNode(name, self.current_namespace) + else: + variable = self.current_namespace[name] + if isinstance(source, Dependence): try: source.check() @@ -280,6 +373,8 @@ class VariableAPI: class NamespaceAPI: + '''Класс для создания пространств имен при задании переменных через + python-скрипты.''' def __init__(self, var_fabric: VariableAPI, datavars_root=NamespaceNode('')): self._datavars = datavars_root @@ -287,28 +382,51 @@ class NamespaceAPI: self.namespace_stack = [datavars_root] self._variables_fabric.current_namespace = self._datavars + Dependence.current_namespace = self._datavars + Dependence.datavars_root = 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 + def reset(self): + '''Метод для сброса корневого пространства имен.''' + self._datavars = NamespaceNode('') + self.namespace_stack = [self._datavars] + self._variables_fabric.current_namespace = self._datavars + + Dependence.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 + '''Метод для создания пространств имен с помощью with.''' + if namespace_name not in self.namespace_stack[-1].namespaces: + namespace = NamespaceNode(namespace_name, + parent=self.namespace_stack[-1]) + else: + namespace = self.namespace_stack[-1].namespaces[namespace_name] + + self.namespace_stack[-1].add_namespace(namespace) + self.namespace_stack.append(namespace) + self._variables_fabric.current_namespace = namespace + + Dependence.datavars_root = self._datavars + Dependence.current_namespace = namespace try: yield self finally: - self._variables_fabric.current_namespace =\ - self.namespace_stack.pop() + current_namespace = self.namespace_stack.pop() + self._variables_fabric.current_namespace = current_namespace + Dependence.current_namespace = current_namespace Variable = VariableAPI() diff --git a/tests/vars/test_alt_vars.py b/tests/vars/test_alt_vars.py index 0495d22..a5a1f54 100644 --- a/tests/vars/test_alt_vars.py +++ b/tests/vars/test_alt_vars.py @@ -1,6 +1,7 @@ import pytest from calculate.vars.alt_datavars import NamespaceNode, VariableNode,\ - Namespace, Variable, Dependence + Namespace, Variable, Dependence,\ + CyclicVariableError @pytest.mark.alt_vars @@ -23,6 +24,9 @@ class TestNamespace: assert namespace_1.variables == dict() assert namespace_1.fullname == 'namespace_1' + assert namespace_1.namespace_2 == namespace_2 + assert namespace_1['namespace_2'] == namespace_2 + assert namespace_2.namespaces == dict() assert namespace_2.variables == dict() assert namespace_2.parent == namespace_1 @@ -39,11 +43,75 @@ class TestNamespace: assert namespace_1.fullname == 'namespace_1' assert namespace_1.var_1 == 'value_1' + assert namespace_1['var_1'] == variable_1 + assert namespace_1.var_2 == 'value_2' - assert namespace_1.get_variable_node('var_1').fullname ==\ - 'namespace_1.var_1' - assert namespace_1.get_variable_node('var_2').fullname ==\ - 'namespace_1.var_2' + assert namespace_1['var_2'] == variable_2 + + assert namespace_1['var_1'].fullname == 'namespace_1.var_1' + assert namespace_1['var_2'].fullname == 'namespace_1.var_2' + + def test_compare_two_dependencies_equal(self): + namespace_1 = NamespaceNode('namespace_1') + variable_1 = VariableNode('var_1', namespace_1, source=2) + variable_2 = VariableNode('var_2', namespace_1, source=4) + + dependence_1 = Dependence(variable_1, variable_2, + depend=lambda arg_1, arg_2: + 'greater' if arg_1 > arg_2 else 'less') + + dependence_2 = Dependence(variable_1, variable_2, + depend=lambda var_1, var_2: + 'greater' if var_1 > var_2 else 'less') + assert dependence_1 == dependence_2 + + def test_compare_two_dependencies_equal_but_one_is_function_and_other_is_lambda(self): + namespace_1 = NamespaceNode('namespace_1') + variable_1 = VariableNode('var_1', namespace_1, source=2) + variable_2 = VariableNode('var_2', namespace_1, source=4) + + dependence_1 = Dependence(variable_1, variable_2, + depend=lambda arg_1, arg_2: + 'greater' if arg_1 > arg_2 else 'less') + + def comparator(var_1, var_2): + if var_1 > var_2: + return 'greater' + else: + return 'less' + dependence_2 = Dependence(variable_1, variable_2, depend=comparator) + assert dependence_1 == dependence_2 + + def test_compare_two_dependencies_not_equal(self): + namespace_1 = NamespaceNode('namespace_1') + variable_1 = VariableNode('var_1', namespace_1, source=2) + variable_2 = VariableNode('var_2', namespace_1, source=4) + + dependence_1 = Dependence(variable_1, variable_2, + depend=lambda arg_1, arg_2: + 'less' if arg_1 > arg_2 else 'greater') + + dependence_2 = Dependence(variable_1, variable_2, + depend=lambda var_1, var_2: + 'greater' if var_1 > var_2 else 'less') + assert dependence_1 != dependence_2 + + def test_compare_two_dependencies_not_equal_but_one_is_function_and_other_is_lambda(self): + namespace_1 = NamespaceNode('namespace_1') + variable_1 = VariableNode('var_1', namespace_1, source=2) + variable_2 = VariableNode('var_2', namespace_1, source=4) + + dependence_1 = Dependence(variable_1, variable_2, + depend=lambda arg_1, arg_2: + 'greater' if arg_1 > arg_2 else 'less') + + def comparator(var_1, var_2): + if var_1 > var_2: + return 'less' + else: + return 'greater' + dependence_2 = Dependence(variable_1, variable_2, depend=comparator) + assert dependence_1 != dependence_2 def test_if_a_variable_subscribed_to_two_other_variables_using_set_function__the_(self): namespace_1 = NamespaceNode('namespace_1') @@ -58,10 +126,12 @@ class TestNamespace: assert namespace_2.var_1 == 'less' - namespace_1.get_variable_node('var_1').source = 5 + namespace_1['var_1'].source = 5 assert namespace_2.var_1 == 'greater' + # Теперь тестируем интерфейс создания переменных. def test_api(self): + Namespace.reset() with Namespace('namespace_1'): Variable('var_1', source='val_1') @@ -84,5 +154,95 @@ class TestNamespace: 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 + datavars.namespace_1['var_2'].source = 5 assert datavars.namespace_2.var_1 == 'greater' + + def test_rewriting_if_source_changed_to_a_value(self): + Namespace.reset() + datavars = Namespace.datavars + + with Namespace('namespace_1'): + var_1 = Variable('var_1', source='value_1') + Variable('var_2', source=Dependence( + var_1, + depend=lambda arg_1: + 'from var_1: {}'.format(arg_1))) + assert datavars.namespace_1.var_1 == 'value_1' + assert datavars.namespace_1.var_2 == 'from var_1: value_1' + + with Namespace('namespace_1'): + var_1 = Variable('var_1', source='value_2') + + assert datavars.namespace_1.var_1 == 'value_2' + assert datavars.namespace_1.var_2 == 'from var_1: value_2' + + def test_rewriting_if_source_changed_to_a_dependence(self): + Namespace.reset() + datavars = Namespace.datavars + + with Namespace('namespace_1'): + var_1 = Variable('var_1', source='value_1') + Variable('var_2', source=Dependence(var_1, depend=lambda arg_1: + 'from var_1: {}'. + format(arg_1))) + var_3 = Variable('var_3', source='value_3') + + assert datavars.namespace_1.var_1 == 'value_1' + assert datavars.namespace_1.var_2 == 'from var_1: value_1' + + with Namespace('namespace_1'): + Variable('var_2', source=Dependence(var_3, depend=lambda arg_1: + 'from var_3: {}'. + format(arg_1))) + + assert datavars.namespace_1.var_2 == 'from var_3: value_3' + + def test_cyclic_dependence(self): + Namespace.reset() + datavars = Namespace.datavars + + with Namespace('namespace_1'): + var_1 = Variable('var_1', source='value_1') + var_2 = Variable('var_2', + source=Dependence(var_1, + depend=lambda arg_1: + 'from var_1: {}'.format(arg_1))) + + def comparator(arg_1): + return 'from var_2: {}'.format(arg_1) + var_3 = Variable('var_3', + source=Dependence(var_2, depend=comparator)) + Variable('var_1', + source=Dependence(var_3, + depend=lambda arg_1: + 'from var_3: {}'.format(arg_1))) + + with pytest.raises(CyclicVariableError): + datavars.namespace_1.var_3 + + def test_find_vars(self): + Namespace.reset() + datavars = Namespace.datavars + var_1 = Variable('var', source='null') + + with Namespace('namespace_1'): + Variable('var_1', source='val_1') + Variable('var_2', source=2) + Variable('var_3', source=4) + + with Namespace('namespace_1_1'): + Variable('var_1', source='val_1') + + for var in Dependence(var_1)._get_variables(['.var_1', + '..var_3']): + print(var.fullname) + + with Namespace('namespace_2'): + print('current_datavars:') + for namespace_name, namespace in datavars.namespaces.items(): + print(namespace_name) + + for var in Dependence(var_1)._get_variables(['namespace_1.var_2']): + print(var.fullname) + + assert False