From 4e2168cad63bfd310d77d45b26cd8dec4892219d Mon Sep 17 00:00:00 2001 From: Dave Cowden Date: Tue, 8 Dec 2015 21:35:01 -0500 Subject: [PATCH] initial working version of cqgi --- cadquery/cqgi.py | 254 +++++++++++++++++++++++++--------------------- tests/TestCQGI.py | 114 ++++++++++++++++++--- 2 files changed, 235 insertions(+), 133 deletions(-) diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index 25deebb..fd0b6b6 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -8,177 +8,216 @@ import traceback import re import time -CQSCRIPT= "" -BUILD_OBJECT_COLLECTOR = "__boc__" +CQSCRIPT = "" + class CQModel(object): """ Object that provides a nice interface to a cq script that is following the cce model """ - def __init__(self,scriptSource): + + def __init__(self, script_source): self.metadata = ScriptMetadata() - self.astTree = ast.parse(scriptSource,CQSCRIPT) + self.astTree = ast.parse(script_source, CQSCRIPT) ConstantAssignmentFinder(self.metadata).visit(self.astTree) - #TODO: pick up other scirpt metadata: + # TODO: pick up other scirpt metadata: # describe # pick up validation methods - def validate(self,params): + def validate(self, params): raise NotImplementedError("not yet implemented") - def build(self,params): + def build(self, params=None): """ :param params: dictionary of parameter values to build with :return: """ - self.setParamValues(params) + if not params: + params = {} + + self.set_param_values(params) collector = BuildObjectCollector() - env = EnvironmentBuilder().withRealBuiltins() \ - .addEntry("build_object",collector.build_object).build() + env = EnvironmentBuilder().with_real_builtins() \ + .add_entry("build_object", collector.build_object).build() start = time.clock() result = BuildResult() - - c = compile(self.astTree,CQSCRIPT,'exec') - exec (c,env) - if collector.hasResults(): - result.setSuccessResult(collector.outputObjects) - else: - raise ValueError("Script did not call build_object-- no output available.") - #except Exception,ex: - - # result.setFailureResult(ex) + try: + c = compile(self.astTree, CQSCRIPT, 'exec') + exec (c, env) + if collector.hasResults(): + result.set_success_result(collector.outputObjects) + else: + raise ValueError("Script did not call build_object-- no output available.") + except Exception, ex: + result.set_failure_result(ex) end = time.clock() result.buildTime = end - start return result - def setParamValues(self,params): - modelParameters = self.metadata.parameters + def set_param_values(self, params): + model_parameters = self.metadata.parameters - for k,v in params.iteritems(): - if not modelParameters.has_key(k): - raise ValueError("Cannot set value '%s', it is not a parameter of the model." % k) + for k, v in params.iteritems(): + if k not in model_parameters: + raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k) - p = modelParameters[k] - p.setValue(v) + p = model_parameters[k] + p.set_value(v) class BuildResult(object): - def __init__(self): self.buildTime = None self.results = [] self.success = False self.exception = None - def setFailureResult(self,ex): + def set_failure_result(self, ex): self.exception = ex self.success = False - def setSuccessResult(self,results): + def set_success_result(self, results): self.results = results self.success = True + class ScriptMetadata(object): def __init__(self): self.parameters = {} - def add_script_parameter(self,p): + def add_script_parameter(self, p): self.parameters[p.name] = p -class ParameterType(object): pass -class NumberParameterType(ParameterType) : pass -class StringParameterType(ParameterType) : pass -class BooleanParameterType(ParameterType): pass + +class ParameterType(object): + pass + + +class NumberParameterType(ParameterType): + pass + + +class StringParameterType(ParameterType): + pass + + +class BooleanParameterType(ParameterType): + pass class InputParameter: - def __init__(self): self.name = None self.shortDesc = None self.varType = None self.validValues = [] - self.defaultValue= None - self.astNode = None + self.default_value = None + self.ast_node = None @staticmethod - def create(astNode,varname,varType, defaultValue,validValues =[],shortDesc = None): + def create(ast_node, var_name, var_type, default_value, valid_values=None, short_desc=None): + + if valid_values is None: + valid_values = [] + p = InputParameter() - p.astNode = astNode - p.defaultValue = defaultValue - p.name = varname - if shortDesc == None: - p.shortDesc = varname + p.ast_node = ast_node + p.default_value = default_value + p.name = var_name + if short_desc is None: + p.shortDesc = var_name else: - p.shortDesc = shortDesc - p.varType = varType - p.validValues = validValues + p.shortDesc = short_desc + p.varType = var_type + p.validValues = valid_values return p - def setValue(self,newValue): - #todo: check to make sure newValue can be cast correctly? + def set_value(self, new_value): + + if len(self.validValues) > 0 and not new_value in self.validValues: + raise InvalidParameterError( + "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " + .format( str(new_value), self.name, str(self.validValues))) + if self.varType == NumberParameterType: - self.astNode.n = newValue + try: + f = float(new_value) + self.ast_node.n = f + except ValueError: + raise InvalidParameterError( + "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." + .format(str(new_value), self.name)) + elif self.varType == StringParameterType: - self.astNode.s = str(newValue) + self.ast_node.s = str(new_value) elif self.varType == BooleanParameterType: - if ( newValue ): - self.astNode.value.id = 'True' + if new_value: + self.ast_node.value.id = 'True' else: - self.astNode.value.id = 'False' + self.ast_node.value.id = 'False' else: raise ValueError("Unknown Type of var: ", str(self.varType)) def __str__(self): - return "InputParmaeter: {name=%s, type=%s, defaultValue=%s" % ( self.name, str(self.varType), str(self.defaultValue) ) + return "InputParameter: {name=%s, type=%s, defaultValue=%s" % ( + self.name, str(self.varType), str(self.default_value)) + class BuildObjectCollector(object): """ Allows a script to provide output objects """ + def __init__(self): self.outputObjects = [] - def build_object(self,shape): + def build_object(self, shape): self.outputObjects.append(shape) def hasResults(self): return len(self.outputObjects) > 0 + class ScriptExecutor(object): """ executes a script in a given environment. """ - def __init__(self,environment,astTree): + + def __init__(self, environment, astTree): try: - exec ( astTree ) in environment - except Exception,ex: + exec (astTree) in environment + except Exception, ex: - #an error here means there was a problem compiling the script - #try to figure out what line the error was on + # an error here means there was a problem compiling the script + # try to figure out what line the error was on traceback.print_exc() formatted_lines = traceback.format_exc().splitlines() - lineText = "" + line_text = "" for f in formatted_lines: if f.find(CQSCRIPT) > -1: - m = re.search("line\\s+(\\d+)",f,re.IGNORECASE ) + m = re.search("line\\s+(\\d+)", f, re.IGNORECASE) if m and m.group(1): - lineText = m.group(1) + line_text = m.group(1) else: - lineText = 0 + line_text = 0 + + sse = ScriptExecutionError() + sse.line = int(line_text) + sse.message = str(ex) + raise sse + + +class InvalidParameterError(Exception): + pass - sse = ScriptExecutionError() - sse.line = int(lineText) - sse.message = str(ex) - raise sse class ScriptExecutionError(Exception): """ @@ -186,7 +225,8 @@ class ScriptExecutionError(Exception): Useful for helping clients pinpoint issues with the script interactively """ - def __init__(self,line=None,message=None): + + def __init__(self, line=None, message=None): if line is None: self.line = 0 else: @@ -197,86 +237,64 @@ class ScriptExecutionError(Exception): else: self.message = message - def fullMessage(self): + def full_message(self): return self.__repr__() def __str__(self): return self.__repr__() def __repr__(self): - return "ScriptError [Line %s]: %s" % ( self.line, self.message) + return "ScriptError [Line %s]: %s" % (self.line, self.message) + class EnvironmentBuilder(object): - def __init__(self): self.env = {} - def withRealBuiltins(self): - return self.withBuiltins(__builtins__) + def with_real_builtins(self): + return self.with_builtins(__builtins__) - def withBuiltins(self,dict): - self.env['__builtins__'] = dict + def with_builtins(self, env_dict): + self.env['__builtins__'] = env_dict return self - def addEntry(self,name, value): + def add_entry(self, name, value): self.env[name] = value return self def build(self): return self.env -class VariableAssignmentChanger(ast.NodeTransformer): - """ - - """ - def __init__(self, overrides): - self.overrides = overrides - - def modify_node(self, varname, node): - if self.overrides.has_key(varname): - new_value = self.overrides[varname] - if type(node) == ast.Num: - node.n = new_value - elif type(node) == ast.Str: - node.s = new_value - else: - raise ValueError("Unknown Assignment Type:" + str(type(node))) - - def visit_Assign(self, node): - left_side = node.targets[0] - if type(node.value) in [ast.Num, ast.Str]: - self.modify_node(left_side.id, node.value) - elif type(node.value) == ast.Tuple: - # we have a multi-value assignment - for n, v in zip(left_side.elts, node.value.elts): - self.modify_node(n.id, v) - return node class ConstantAssignmentFinder(ast.NodeTransformer): """ Visits a parse tree, and adds script parameters to the cqModel """ - def __init__(self,cqModel): - self.cqModel = cqModel + def __init__(self, cq_model): + self.cqModel = cq_model - def handle_assignment(self,varname, valuenode): - if type(valuenode) == ast.Num: - self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,NumberParameterType,valuenode.n)) - elif type(valuenode) == ast.Str: - self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,StringParameterType,valuenode.s)) - elif type(valuenode == ast.Name): - if valuenode.value.Id == 'True': - self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,BooleanParameterType,True)) - elif valuenode.value.Id == 'False': - self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,BooleanParameterType,True)) + def handle_assignment(self, var_name, value_node): + if type(value_node) == ast.Num: + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, NumberParameterType, value_node.n)) + elif type(value_node) == ast.Str: + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, StringParameterType, value_node.s)) + elif type(value_node == ast.Name): + if value_node.value.Id == 'True': + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, BooleanParameterType, True)) + elif value_node.value.Id == 'False': + self.cqModel.add_script_parameter( + InputParameter.create(value_node, var_name, BooleanParameterType, True)) def visit_Assign(self, node): left_side = node.targets[0] if type(node.value) in [ast.Num, ast.Str, ast.Name]: - self.handle_assignment(left_side.id,node.value) + self.handle_assignment(left_side.id, node.value) elif type(node.value) == ast.Tuple: # we have a multi-value assignment for n, v in zip(left_side.elts, node.value.elts): self.handle_assignment(n.id, v) - return node \ No newline at end of file + return node diff --git a/tests/TestCQGI.py b/tests/TestCQGI.py index 8ced76d..925e7c4 100644 --- a/tests/TestCQGI.py +++ b/tests/TestCQGI.py @@ -1,37 +1,121 @@ """ Tests CQGI functionality -""" + Currently, this includes: + Parsing a script, and detecting its available variables + Altering the values at runtime + defining a build_object function to return results +""" from cadquery import cqgi from tests import BaseTest +import textwrap -TESTSCRIPT = """ -height=2.0 -width=3.0 -(a,b) = (1.0,1.0) -foo="bar" +TESTSCRIPT = textwrap.dedent( + """ + height=2.0 + width=3.0 + (a,b) = (1.0,1.0) + foo="bar" + + result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) ) + build_object(result) + """ +) -result = "%s|%s|%s|%s" % ( str(height) , str(width) , foo , str(a) ) -build_object(result) -""" class TestCQGI(BaseTest): - - def test_parser(self): model = cqgi.CQModel(TESTSCRIPT) metadata = model.metadata - self.assertEquals( len(metadata.parameters) , 5 ) + + self.assertEquals(set(metadata.parameters.keys()), {'height', 'width', 'a', 'b', 'foo'}) def test_build_with_empty_params(self): model = cqgi.CQModel(TESTSCRIPT) - result = model.build({}) #building with no params should have no affect on the output + result = model.build() + self.assertTrue(result.success) self.assertTrue(len(result.results) == 1) self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0") def test_build_with_different_params(self): model = cqgi.CQModel(TESTSCRIPT) - result = model.build({ 'height':3.0}) - self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0") \ No newline at end of file + result = model.build({'height': 3.0}) + self.assertTrue(result.results[0] == "3.0|3.0|bar|1.0") + + def test_build_with_exception(self): + badscript = textwrap.dedent( + """ + raise ValueError("ERROR") + """ + ) + + model = cqgi.CQModel(badscript) + result = model.build({}) + self.assertFalse(result.success) + self.assertIsNotNone(result.exception) + self.assertTrue(result.exception.message == "ERROR") + + def test_that_invalid_syntax_in_script_fails_immediately(self): + badscript = textwrap.dedent( + """ + this doesnt even compile + """ + ) + + with self.assertRaises(Exception) as context: + model = cqgi.CQModel(badscript) + + self.assertTrue('invalid syntax' in context.exception) + + def test_that_two_results_are_returned(self): + script = textwrap.dedent( + """ + h = 1 + build_object(h) + h = 2 + build_object(h) + """ + ) + + model = cqgi.CQModel(script) + result = model.build({}) + self.assertEquals(2, len(result.results)) + self.assertEquals(1, result.results[0]) + self.assertEquals(2, result.results[1]) + + def test_that_assinging_number_to_string_works(self): + script = textwrap.dedent( + """ + h = "this is a string" + build_object(h) + """ + ) + result = self._executeScriptWithParams(script, {'h': 33.33}) + self.assertEquals(result.results[0], "33.33") + + def test_that_assigning_string_to_number_fails(self): + script = textwrap.dedent( + """ + h = 20.0 + build_object(h) + """ + ) + with self.assertRaises(Exception): + result = self._executeScriptWithParams(script, {'h': "a string"}) + + def test_that_assigning_unknown_var_fails(self): + + script = textwrap.dedent( + """ + h = 20.0 + build_object(h) + """ + ) + with self.assertRaises(cqgi.InvalidParameterError): + result = self._executeScriptWithParams(script, {'w': "var is not there"}) + + def _executeScriptWithParams(self, script, params): + model = cqgi.CQModel(script) + return model.build(params)