Tested variables invalidation and creation of the depended variables. Added check of the cycles while calculating variables.

packages
Иванов Денис 4 years ago
parent 653f5064ff
commit 471f957daa

@ -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 '<Namespace: {}>'.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('<root>')):
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)

@ -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"))

@ -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'

Loading…
Cancel
Save