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,177 +8,216 @@ import traceback
import re
import time
CQSCRIPT= "<cqscript>"
BUILD_OBJECT_COLLECTOR = "__boc__"
CQSCRIPT = "<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
return node

View File

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