Source code for Valentina.Pattern.Calculator

####################################################################################################
#
# PyValentina - A Python implementation of Valentina Pattern Drafting Software
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################

####################################################################################################

import ast
import logging

import astunparse
# import astor

from Valentina.Graph.DirectedAcyclicGraph import DirectedAcyclicGraph

####################################################################################################

_module_logger = logging.getLogger(__name__)

####################################################################################################

[docs]class NodeVisitor(ast.NodeVisitor): ############################################## def __init__(self, calculator): super(NodeVisitor, self).__init__() self._calculator = calculator self._dependencies = [] ############################################## @property def dependencies(self): return self._dependencies ############################################## # def visit(self, node): # print(node) # super(NodeVisitor, self).visit(node) ##############################################
[docs] def visit_Call(self, node): function = node.func if isinstance(function, ast.Attribute) and function.value.id == '__calculator__': dag = self._calculator.dag if function.attr == '_function_Line': for arg in node.args: self._dependencies.append(self._calculator._name_to_point(arg.s)) self.generic_visit(node)
####################################################################################################
[docs]class Calculator: _logger = _module_logger.getChild('Calculator') ############################################## def __init__(self, measurements): self._measurements = measurements self._dag = DirectedAcyclicGraph() self._cache = {'__calculator__': self} self._points = {} self._current_operation = None ############################################## @property def measurements(self): return self._measurements @property def dag(self): return self._dag @property def cache(self): return self._cache ############################################## def _update_cache(self, named_expression): self._cache[named_expression.name] = named_expression.value ##############################################
[docs] def add_point(self, point): self._points[point.name] = point
##############################################
[docs] def set_current_segment(self, vector): self._current_segment = vector
##############################################
[docs] def unset_current_segment(self): self._current_segment = None
# self._logger.info('Unset current segment') ############################################## def _name_to_point(self, name): return self._points[name] ############################################## def _name_to_vector_point(self, name): return self._points[name].vector ############################################## def _names_to_vector_points(self, *names): return [self._name_to_vector_point(name) for name in names] ############################################## # Fixme: special functions # increments ? # Length of curve: Spl_A_B # Angle of line: AngleLine_A_B # radius of arcs # Angles of curves: Angle1Spl_A_B Angle2Spl_A_B # Length of control points: C1LengthSpl_A_B C2LengthSpl_A_B # functions def _function_Angle1Spl(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return 0 def _function_Angle2Spl(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return 0 def _function_AngleLine(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return (point2 - point1).orientation() def _function_CurrentLength(self): return self._current_segment.magnitude() def _function_C1LengthSpl(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return 0 def _function_C2LengthSpl(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return 0 def _function_Line(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return (point2 - point1).magnitude() def _function_Spl(self, point_name1, point_name2): point1, point2 = self._names_to_vector_points(point_name1, point_name2) return 0
####################################################################################################
[docs]class Expression: _logger = _module_logger.getChild('Expression') ############################################## def __init__(self, expression, calculator=None): self._expression = str(expression) self._calculator = calculator self._ast = None self._dependencies = None self._code = None self._value = None self._value_error = False ############################################## @property def expression(self): return self._expression @property def dependencies(self): return self._dependencies ############################################## def __str__(self): return self._expression ##############################################
[docs] def is_float(self): try: float(self._expression) return True except ValueError: return False
############################################## def _find_name(self, prefix, start=0): expression = self._expression start = expression.find(prefix, start) if start is -1: return None, None index = start + 1 while index < len(expression): c = expression[index] if 'a' <= c <= 'z' or 'A' <= c <= 'Z' or '0' <= c <= '9' or c in '_': index += 1 else: break return expression[start:index], index + 1 ############################################## def _compile(self): expression = self._expression self._logger.info("expression '{}'".format(expression)) # Python don't accept identifier starting with @ # https://docs.python.org/3.5/reference/lexical_analysis.html#identifiers if '@' in expression: custom_measurements = [] start = 0 while True: name, start = self._find_name('@', start) if name is None: break else: custom_measurements.append(name) for measurement in custom_measurements: expression = self.expression.replace(measurement, self._calculator.measurements[measurement].name) functions = [] for function in ( 'Angle1Spl_', 'Angle2Spl_', 'AngleLine_', 'CurrentLength', 'C1LengthSpl_', 'C2LengthSpl_', 'Line_', 'Spl_', ): start = 0 while True: name, start = self._find_name(function, start) if name is None: break else: functions.append(name) # self._logger.info('Functions ' + str(functions)) for function_call in functions: parts = function_call.split('_') function = parts[0] args = parts[1:] pythonised_function = '__calculator__._function_' + function + '(' + ', '.join(["'{}'".format(x) for x in args]) + ')' # self._logger.info('Function {} {} -> {}'.format(function, args, pythonised_function)) expression = expression.replace(function_call, pythonised_function) self._logger.info("Pythonised expression '{}'".format(expression)) # Fixme: What is the (supported) grammar ? # http://beltoforion.de/article.php?a=muparser # http://beltoforion.de/article.php?a=muparserx self._ast = ast.parse(expression, mode='eval') node_visitor = NodeVisitor(self._calculator) node_visitor.generic_visit(self._ast) self._dependencies = node_visitor.dependencies self._code = compile(self._ast, '<string>', mode='eval') # print('AST', astunparse.dump(self._ast)) # print('AST -> Python', astunparse.unparse(self._ast)) ## print('AST -> Python', astor.to_source(self._ast.body)) ##############################################
[docs] def eval(self): if self._code is None: self._compile() try: self._value = eval(self._code, self._calculator.cache) self._value_error = False except NameError: self._value = None self._value_error = True # except AttributeError as e: # self._logger.warning(e) # self._value = None self._logger.info('Eval {} = {}'.format(self._expression, self._value))
##############################################
[docs] def set_dirty(self): self._value = None self._value_error = False
############################################## @property def value(self): if self._value is None and self._value_error is False: self.eval() return self._value
####################################################################################################
[docs]class NamedExpression(Expression): ############################################## def __init__(self, name, expression, calculator=None): Expression.__init__(self, expression, calculator) self._name = name ############################################## @property def name(self): return self._name