initial working version of cqgi

This commit is contained in:
Dave Cowden 2015-12-08 21:35:01 -05:00
parent c12e663638
commit 4e2168cad6
2 changed files with 235 additions and 133 deletions

View File

@ -8,185 +8,225 @@ import traceback
import re import re
import time import time
CQSCRIPT= "<cqscript>" CQSCRIPT = "<cqscript>"
BUILD_OBJECT_COLLECTOR = "__boc__"
class CQModel(object): class CQModel(object):
""" """
Object that provides a nice interface to a cq script that Object that provides a nice interface to a cq script that
is following the cce model is following the cce model
""" """
def __init__(self,scriptSource):
def __init__(self, script_source):
self.metadata = ScriptMetadata() self.metadata = ScriptMetadata()
self.astTree = ast.parse(scriptSource,CQSCRIPT) self.astTree = ast.parse(script_source, CQSCRIPT)
ConstantAssignmentFinder(self.metadata).visit(self.astTree) ConstantAssignmentFinder(self.metadata).visit(self.astTree)
#TODO: pick up other scirpt metadata: # TODO: pick up other scirpt metadata:
# describe # describe
# pick up validation methods # pick up validation methods
def validate(self,params): def validate(self, params):
raise NotImplementedError("not yet implemented") raise NotImplementedError("not yet implemented")
def build(self,params): def build(self, params=None):
""" """
:param params: dictionary of parameter values to build with :param params: dictionary of parameter values to build with
:return: :return:
""" """
self.setParamValues(params) if not params:
params = {}
self.set_param_values(params)
collector = BuildObjectCollector() collector = BuildObjectCollector()
env = EnvironmentBuilder().withRealBuiltins() \ env = EnvironmentBuilder().with_real_builtins() \
.addEntry("build_object",collector.build_object).build() .add_entry("build_object", collector.build_object).build()
start = time.clock() start = time.clock()
result = BuildResult() result = BuildResult()
try:
c = compile(self.astTree,CQSCRIPT,'exec') c = compile(self.astTree, CQSCRIPT, 'exec')
exec (c,env) exec (c, env)
if collector.hasResults(): if collector.hasResults():
result.setSuccessResult(collector.outputObjects) result.set_success_result(collector.outputObjects)
else: else:
raise ValueError("Script did not call build_object-- no output available.") raise ValueError("Script did not call build_object-- no output available.")
#except Exception,ex: except Exception, ex:
result.set_failure_result(ex)
# result.setFailureResult(ex)
end = time.clock() end = time.clock()
result.buildTime = end - start result.buildTime = end - start
return result return result
def setParamValues(self,params): def set_param_values(self, params):
modelParameters = self.metadata.parameters model_parameters = self.metadata.parameters
for k,v in params.iteritems(): for k, v in params.iteritems():
if not modelParameters.has_key(k): if k not in model_parameters:
raise ValueError("Cannot set value '%s', it is not a parameter of the model." % k) raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k)
p = modelParameters[k] p = model_parameters[k]
p.setValue(v) p.set_value(v)
class BuildResult(object): class BuildResult(object):
def __init__(self): def __init__(self):
self.buildTime = None self.buildTime = None
self.results = [] self.results = []
self.success = False self.success = False
self.exception = None self.exception = None
def setFailureResult(self,ex): def set_failure_result(self, ex):
self.exception = ex self.exception = ex
self.success = False self.success = False
def setSuccessResult(self,results): def set_success_result(self, results):
self.results = results self.results = results
self.success = True self.success = True
class ScriptMetadata(object): class ScriptMetadata(object):
def __init__(self): def __init__(self):
self.parameters = {} self.parameters = {}
def add_script_parameter(self,p): def add_script_parameter(self, p):
self.parameters[p.name] = p self.parameters[p.name] = p
class ParameterType(object): pass
class NumberParameterType(ParameterType) : pass class ParameterType(object):
class StringParameterType(ParameterType) : pass pass
class BooleanParameterType(ParameterType): pass
class NumberParameterType(ParameterType):
pass
class StringParameterType(ParameterType):
pass
class BooleanParameterType(ParameterType):
pass
class InputParameter: class InputParameter:
def __init__(self): def __init__(self):
self.name = None self.name = None
self.shortDesc = None self.shortDesc = None
self.varType = None self.varType = None
self.validValues = [] self.validValues = []
self.defaultValue= None self.default_value = None
self.astNode = None self.ast_node = None
@staticmethod @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 = InputParameter()
p.astNode = astNode p.ast_node = ast_node
p.defaultValue = defaultValue p.default_value = default_value
p.name = varname p.name = var_name
if shortDesc == None: if short_desc is None:
p.shortDesc = varname p.shortDesc = var_name
else: else:
p.shortDesc = shortDesc p.shortDesc = short_desc
p.varType = varType p.varType = var_type
p.validValues = validValues p.validValues = valid_values
return p return p
def setValue(self,newValue): def set_value(self, new_value):
#todo: check to make sure newValue can be cast correctly?
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: 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: elif self.varType == StringParameterType:
self.astNode.s = str(newValue) self.ast_node.s = str(new_value)
elif self.varType == BooleanParameterType: elif self.varType == BooleanParameterType:
if ( newValue ): if new_value:
self.astNode.value.id = 'True' self.ast_node.value.id = 'True'
else: else:
self.astNode.value.id = 'False' self.ast_node.value.id = 'False'
else: else:
raise ValueError("Unknown Type of var: ", str(self.varType)) raise ValueError("Unknown Type of var: ", str(self.varType))
def __str__(self): 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): class BuildObjectCollector(object):
""" """
Allows a script to provide output objects Allows a script to provide output objects
""" """
def __init__(self): def __init__(self):
self.outputObjects = [] self.outputObjects = []
def build_object(self,shape): def build_object(self, shape):
self.outputObjects.append(shape) self.outputObjects.append(shape)
def hasResults(self): def hasResults(self):
return len(self.outputObjects) > 0 return len(self.outputObjects) > 0
class ScriptExecutor(object): class ScriptExecutor(object):
""" """
executes a script in a given environment. executes a script in a given environment.
""" """
def __init__(self,environment,astTree):
def __init__(self, environment, astTree):
try: try:
exec ( astTree ) in environment exec (astTree) in environment
except Exception,ex: except Exception, ex:
#an error here means there was a problem compiling the script # an error here means there was a problem compiling the script
#try to figure out what line the error was on # try to figure out what line the error was on
traceback.print_exc() traceback.print_exc()
formatted_lines = traceback.format_exc().splitlines() formatted_lines = traceback.format_exc().splitlines()
lineText = "" line_text = ""
for f in formatted_lines: for f in formatted_lines:
if f.find(CQSCRIPT) > -1: 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): if m and m.group(1):
lineText = m.group(1) line_text = m.group(1)
else: else:
lineText = 0 line_text = 0
sse = ScriptExecutionError() sse = ScriptExecutionError()
sse.line = int(lineText) sse.line = int(line_text)
sse.message = str(ex) sse.message = str(ex)
raise sse raise sse
class InvalidParameterError(Exception):
pass
class ScriptExecutionError(Exception): class ScriptExecutionError(Exception):
""" """
Represents a script syntax error. Represents a script syntax error.
Useful for helping clients pinpoint issues with the script Useful for helping clients pinpoint issues with the script
interactively interactively
""" """
def __init__(self,line=None,message=None):
def __init__(self, line=None, message=None):
if line is None: if line is None:
self.line = 0 self.line = 0
else: else:
@ -197,84 +237,62 @@ class ScriptExecutionError(Exception):
else: else:
self.message = message self.message = message
def fullMessage(self): def full_message(self):
return self.__repr__() return self.__repr__()
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
def __repr__(self): def __repr__(self):
return "ScriptError [Line %s]: %s" % ( self.line, self.message) return "ScriptError [Line %s]: %s" % (self.line, self.message)
class EnvironmentBuilder(object): class EnvironmentBuilder(object):
def __init__(self): def __init__(self):
self.env = {} self.env = {}
def withRealBuiltins(self): def with_real_builtins(self):
return self.withBuiltins(__builtins__) return self.with_builtins(__builtins__)
def withBuiltins(self,dict): def with_builtins(self, env_dict):
self.env['__builtins__'] = dict self.env['__builtins__'] = env_dict
return self return self
def addEntry(self,name, value): def add_entry(self, name, value):
self.env[name] = value self.env[name] = value
return self return self
def build(self): def build(self):
return self.env 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): class ConstantAssignmentFinder(ast.NodeTransformer):
""" """
Visits a parse tree, and adds script parameters to the cqModel Visits a parse tree, and adds script parameters to the cqModel
""" """
def __init__(self,cqModel): def __init__(self, cq_model):
self.cqModel = cqModel self.cqModel = cq_model
def handle_assignment(self,varname, valuenode): def handle_assignment(self, var_name, value_node):
if type(valuenode) == ast.Num: if type(value_node) == ast.Num:
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,NumberParameterType,valuenode.n)) self.cqModel.add_script_parameter(
elif type(valuenode) == ast.Str: InputParameter.create(value_node, var_name, NumberParameterType, value_node.n))
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,StringParameterType,valuenode.s)) elif type(value_node) == ast.Str:
elif type(valuenode == ast.Name): self.cqModel.add_script_parameter(
if valuenode.value.Id == 'True': InputParameter.create(value_node, var_name, StringParameterType, value_node.s))
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,BooleanParameterType,True)) elif type(value_node == ast.Name):
elif valuenode.value.Id == 'False': if value_node.value.Id == 'True':
self.cqModel.add_script_parameter(InputParameter.create(valuenode,varname,BooleanParameterType,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): def visit_Assign(self, node):
left_side = node.targets[0] left_side = node.targets[0]
if type(node.value) in [ast.Num, ast.Str, ast.Name]: 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: elif type(node.value) == ast.Tuple:
# we have a multi-value assignment # we have a multi-value assignment
for n, v in zip(left_side.elts, node.value.elts): for n, v in zip(left_side.elts, node.value.elts):

View File

@ -1,37 +1,121 @@
""" """
Tests CQGI functionality 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 cadquery import cqgi
from tests import BaseTest from tests import BaseTest
import textwrap
TESTSCRIPT = """ TESTSCRIPT = textwrap.dedent(
height=2.0 """
width=3.0 height=2.0
(a,b) = (1.0,1.0) width=3.0
foo="bar" (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): class TestCQGI(BaseTest):
def test_parser(self): def test_parser(self):
model = cqgi.CQModel(TESTSCRIPT) model = cqgi.CQModel(TESTSCRIPT)
metadata = model.metadata 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): def test_build_with_empty_params(self):
model = cqgi.CQModel(TESTSCRIPT) 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(result.success)
self.assertTrue(len(result.results) == 1) self.assertTrue(len(result.results) == 1)
self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0") self.assertTrue(result.results[0] == "2.0|3.0|bar|1.0")
def test_build_with_different_params(self): def test_build_with_different_params(self):
model = cqgi.CQModel(TESTSCRIPT) model = cqgi.CQModel(TESTSCRIPT)
result = model.build({ 'height':3.0}) result = model.build({'height': 3.0})
self.assertTrue(result.results[0] == "3.0|3.0|bar|1.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)