################################################################################ # # Copyright (C) 2021-2022 Advanced Micro Devices, Inc. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ################################################################################ from copy import deepcopy import ast class ReadWriteTransformDict(dict): # dicts take a mapping or iterable as their optional first argument """ The focus of this class is to allow explicit customization of reading and writing to attributes. Users of this class might restrict read or write access to particular variables, or perform some customization on those functionalities. """ __slots__ = () # no __dict__ - that would be redundant readTransformFuncKey = '_readTransformFunc' writeTransformFuncKey = '_writeTransformFunc' def __init__(self, readTransformFunc = None, writeTransformFunc = None): super().__init__() if readTransformFunc is not None and callable(readTransformFunc): self.setReadTransform(readTransformFunc) if writeTransformFunc is not None and callable(writeTransformFunc): self.setWriteTransform(writeTransformFunc) def __getitem__(self, key): return self.readWithTransform(key) if self.hasReadTransform() else self.readNoTransform(key) def __setitem__(self, key, value): self.writeWithTransform(key, value) if self.hasWriteTransform() else self.writeNoTransform(key, value) def __getattr__(self, key): return self[key] def __setattr__(self, key, value): self[key] = value def __copy__(self): # don't delegate w/ super - dict.copy() -> dict :( cls = self.__class__ result = cls.__new__(cls) result.update(self) return result def __deepcopy__(self, memo): # don't delegate w/ super - dict.copy() -> dict :( cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result for key in self.keys(): result.writeNoTransform(key, deepcopy(self.readNoTransform(key))) return result def __repr__(self): return "\n".join(self.__toPrettyLines()) def __toPrettyLines(self): """ Embedded values are sometimes hard to read and understand with so many layers, so add some tabs and line breaks to help distinguish hierarchy trees of objects in the dict. """ prettyLines = [] prettyLines += ["<{0}(".format(type(self).__name__)] for key, value in self.items(): if isinstance(value, ReadWriteTransformDict): valuePrettyLines = value.__toPrettyLines() prettyLines += ["\t{0}: {1}".format(key, valuePrettyLines.pop(0))] for line in valuePrettyLines: prettyLines += ["\t{0}".format(line)] else: prettyLines += ["\t{0}: {1}".format(key, value)] prettyLines += [")>"] return prettyLines def get(self, key, default=None): try: return self[key] except: return default def set(self, key, value): try: self[key] = value except: pass # Transform tests def hasReadTransform(self): return self.readTransformFuncKey in self def hasWriteTransform(self): return self.writeTransformFuncKey in self # Omit transform def readNoTransform(self, name): return super().__getitem__(name) def writeNoTransform(self, name, value): super().__setitem__(name, value) # Use transform def readWithTransform(self, name): return self.getReadTransform()(self, name) def writeWithTransform(self, name, value): self.getWriteTransform()(self, name, value) # Transform get/set def getReadTransform(self): return self.readNoTransform(self.readTransformFuncKey) if self.hasReadTransform() else None def setReadTransform(self, readTransformFunc): self.writeNoTransform(self.readTransformFuncKey, readTransformFunc) if readTransformFunc is None: self.pop(self.readTransformFuncKey, None) def getWriteTransform(self): return self.readNoTransform(self.writeTransformFuncKey) if self.hasWriteTransform() else None def setWriteTransform(self, writeTransformFunc): self.writeNoTransform(self.writeTransformFuncKey, writeTransformFunc) if writeTransformFunc is None: self.pop(self.writeTransformFuncKey, None) def toDict(self): return dict(zip(self.keys(), self.values())) @staticmethod def flattenDict(obj, prefix = "", separator="."): """ Sometimes it is favorable to reduce to a single hierarchy dict for readability or debugging. """ result = ReadWriteTransformDict() for key in obj.keys(): attribute = obj.readNoTransform(key) attribute = attribute.readNoTransform("value") if isinstance(attribute, ReadWriteTransformDict) else attribute if isinstance(attribute, dict): subDict = ReadWriteTransformDict.toFlattenedDict(attribute, key, separator) for subKey in subDict.keys(): newKey = separator.join([prefix, subKey]) if prefix != "" else subKey result[newKey] = deepcopy(subDict[subKey]) else: newKey = separator.join([prefix, key]) if prefix != "" else key result[newKey] = deepcopy(obj[key]) return result def toFlattenedDict(self, prefix = "", separator="."): return ReadWriteTransformDict.flattenDict(self, prefix, separator) class Parameter(ReadWriteTransformDict): """ The Parameter class will be used as the main container for configuration properties. For this class we want to group configurations by name, value, default value and descriptions. No other attributes may be set. All operators and reverse operators are overloaded to use value attribute as the operand as this is usually the one we are interested in. """ __slots__ = () # Simplification of creating attributes def createAttr(self, name, value): super().writeNoTransform(name, value) def __init__(self, name, initialValue, defaultValue=None, description=""): self.createAttr("type", type(initialValue)) self.createAttr("name", name) self.createAttr("value", initialValue) self.createAttr("defaultValue", defaultValue) self.createAttr("description", description) # if defaultValue is None: # self.createAttr("defaultValue", initialValue) # else: # self.createAttr("defaultValue", defaultValue) # Restrict writing to only known fields def writeXForm(self, key, value): if key not in self or key == "type": raise AttributeError("Cannot write attribute: {0}".format(key)) else: # Ensure incoming values are same type to allow assignment objType = self.readNoTransform("type") incomingType = type(value) if key == "value" and objType is not incomingType: raise AttributeError("Type preservation: stored {0} != incoming {1}".format(objType, incomingType)) else: self.writeNoTransform(key, value) super().__init__(None, writeXForm) # Boolean ops def __lt__(self, rhs): if isinstance(rhs, Parameter): return self.value < rhs.value else: return self.value < rhs def __rlt__(self, lhs): if isinstance(lhs, Parameter): return lhs.value < self.value else: return lhs < self.value def __le__(self, rhs): if isinstance(rhs, Parameter): return self.value <= rhs.value else: return self.value <= rhs def __rle__(self, lhs): if isinstance(lhs, Parameter): return lhs.value <= self.value else: return lhs <= self.value def __eq__(self, rhs): if isinstance(rhs, Parameter): return self.value == rhs.value else: return self.value == rhs def __req__(self, lhs): if isinstance(lhs, Parameter): return lhs.value == self.value else: return lhs == self.value def __ne__(self, rhs): if isinstance(rhs, Parameter): return self.value != rhs.value else: return self.value != rhs def __rne__(self, lhs): if isinstance(lhs, Parameter): return lhs.value != self.value else: return lhs != self.value def __gt__(self, rhs): if isinstance(rhs, Parameter): return self.value > rhs.value else: return self.value > rhs def __rgt__(self, lhs): if isinstance(lhs, Parameter): return lhs.value > self.value else: return lhs > self.value def __ge__(self, rhs): if isinstance(rhs, Parameter): return self.value >= rhs.value else: return self.value >= rhs def __rge__(self, lhs): if isinstance(lhs, Parameter): return lhs.value >= self.value else: return lhs >= self.value # Binary Ops with their reverses def __add__(self, rhs): if isinstance(rhs, Parameter): return self.value + rhs.value else: return self.value + rhs def __radd__(self, lhs): if isinstance(lhs, Parameter): return lhs.value + self.value else: return lhs + self.value def __sub__(self, rhs): if isinstance(rhs, Parameter): return self.value - rhs.value else: return self.value - rhs def __rsub__(self, lhs): if isinstance(lhs, Parameter): return lhs.value - self.value else: return lhs - self.value def __mul__(self, rhs): if isinstance(rhs, Parameter): return self.value * rhs.value else: return self.value * rhs def __rmul__(self, lhs): if isinstance(lhs, Parameter): return lhs.value * self.value else: return lhs * self.value def __truediv__(self, rhs): if isinstance(rhs, Parameter): return self.value / rhs.value else: return self.value / rhs def __rtruediv__(self, lhs): if isinstance(lhs, Parameter): return lhs.value / self.value else: return lhs / self.value def __floordiv__(self, rhs): if isinstance(rhs, Parameter): return self.value // rhs.value else: return self.value // rhs def __rfloordiv__(self, lhs): if isinstance(lhs, Parameter): return lhs.value // self.value else: return lhs // self.value def __mod__(self, rhs): if isinstance(rhs, Parameter): return self.value % rhs.value else: return self.value % rhs def __rmod__(self, lhs): if isinstance(lhs, Parameter): return lhs.value % self.value else: return lhs % self.value def __pow__(self, rhs): if isinstance(rhs, Parameter): return self.value ** rhs.value else: return self.value ** rhs def __rpow__(self, lhs): if isinstance(lhs, Parameter): return lhs.value ** self.value else: return lhs ** self.value def __rshift__(self, rhs): if isinstance(rhs, Parameter): return self.value >> rhs.value else: return self.value >> rhs def __rrshift__(self, lhs): if isinstance(lhs, Parameter): return lhs.value >> self.value else: return lhs >> self.value def __lshift__(self, rhs): if isinstance(rhs, Parameter): return self.value << rhs.value else: return self.value << rhs def __rlshift__(self, lhs): if isinstance(lhs, Parameter): return lhs.value << self.value else: return lhs << self.value def __and__(self, rhs): if isinstance(rhs, Parameter): return self.value & rhs.value else: return self.value & rhs def __rand__(self, lhs): if isinstance(lhs, Parameter): return lhs.value & self.value else: return lhs & self.value def __or__(self, rhs): if isinstance(rhs, Parameter): return self.value | rhs.value else: return self.value | rhs def __ror__(self, lhs): if isinstance(lhs, Parameter): return lhs.value | self.value else: return lhs | self.value def __xor__(self, rhs): if isinstance(rhs, Parameter): return self.value ^ rhs.value else: return self.value ^ rhs def __rxor__(self, lhs): if isinstance(lhs, Parameter): return lhs.value ^ self.value else: return lhs ^ self.value def __bool__(self): return bool(self.value) # Unary ops def __neg__(self): return -self.value def __pos__(self): return +self.value def __invert__(self): return ~self.value # Interface def resetToDefault(self): self.value = self.defaultValue return self.value def getValue(self): return self.value def getDefault(self): return self.defaultValue def getDescription(self): return self.description class CallableParameter(Parameter): """ This class is a Parameter who gets its value dynamically at read time. It stores a function that gets called with self as context, which the result will replace the value attribute. This is useful for retrieving values that are not constants and must be retrieved from another function calls """ __slots__ = () class BadFunc(Exception): pass def __init__(self, name, callFunc, description=""): super().__init__(name, 0, 0, description) self.createAttr("callFunc", callFunc) # Before each read, call on self to update value rXForm = self.getReadTransform() def readXForm(obj, key): if key == "value": obj.writeNoTransform(key, obj.__call__()) return rXForm(obj, key) if rXForm is not None else obj.readNoTransform(key) self.setReadTransform(readXForm) # Prevent external writing to value attribute # Will be updated by reads as callable is updated. wXForm = self.getWriteTransform() def writeXForm(obj, key, value): if key == "value": pass else: wXForm(obj, key, value) if wXForm is not None else obj.writeNoTransform(key, value) self.setWriteTransform(writeXForm) def __call__(self): return self.callFunc(self) @classmethod def createBinaryOp(cls, lhs, rhs, op): """ This function will build a binary operation function to call with two operands that are bound to the parameter """ FuncMap = { "And" : lambda lhs, rhs : lhs and rhs, "Or" : lambda lhs, rhs : lhs or rhs, "Lt" : lambda lhs, rhs : lhs < rhs, "LtE" : lambda lhs, rhs : lhs <= rhs, "Eq" : lambda lhs, rhs : lhs == rhs, "NotEq" : lambda lhs, rhs : lhs != rhs, "Gt" : lambda lhs, rhs : lhs > rhs, "GtE" : lambda lhs, rhs : lhs >= rhs, "Mult" : lambda lhs, rhs : lhs * rhs, "Pow" : lambda lhs, rhs : lhs ** rhs, "Div" : lambda lhs, rhs : lhs / rhs, "FloorDiv" : lambda lhs, rhs : lhs // rhs, "Mod" : lambda lhs, rhs : lhs % rhs, "Add" : lambda lhs, rhs : lhs + rhs, "Sub" : lambda lhs, rhs : lhs - rhs, "BitAnd" : lambda lhs, rhs : lhs & rhs, "BitOr" : lambda lhs, rhs : lhs | rhs, "BitXor" : lambda lhs, rhs : lhs ^ rhs, "LShift" : lambda lhs, rhs : lhs << rhs, "RShift" : lambda lhs, rhs : lhs >> rhs, "min" : lambda lhs, rhs : min(lhs, rhs), "max" : lambda lhs, rhs : max(lhs, rhs), } # Incoming op is a name if isinstance(op, str): assert op in FuncMap, "Missing operation in funcMap: {0}".format(op) name = op func = FuncMap[op] # Incoming op assumed as a function else: name = "CustomBinaryOp" func = op # Verify that the function is callable with 2 params try: func(lhs, rhs) except Exception as ex: raise CallableParameter.BadFunc from ex # Capture function and attached values opKey = next((key for key in FuncMap if FuncMap[key] == op), None) if opKey is not None and opKey in ["And", "Or", "Lt", "Le", "Eq", "NotEq", "Gt", "Ge"]: callFunc = lambda obj : bool(func(obj.lhs, obj.rhs)) else: callFunc = lambda obj : func(obj.lhs, obj.rhs) binOp = cls(name, callFunc, "Binary operaton with two operands") binOp.createAttr("lhs", lhs) binOp.createAttr("rhs", rhs) return binOp @classmethod def createUnaryOp(cls, rhs, op): """ This function will build a unary operation function to call with one operand that is bound to the parameter """ FuncMap = { "Not" : lambda val : not val, "Invert" : lambda val : ~val, "USub" : lambda val : -val, "UAdd" : lambda val : +val, "None" : lambda val : val } # Incoming op is a name if isinstance(op, str): assert op in FuncMap, "Missing operation in funcMap: {0}".format(op) name = op func = FuncMap[op] # Incoming op assumed as a function else: name = "CustomUnaryOp" func = op # Verify that the function is callable with 1 param try: func(rhs) except Exception as ex: raise CallableParameter.BadFunc from ex # Capture function and attached values opKey = next((key for key in FuncMap if FuncMap[key] == op), None) if opKey is not None and opKey in ["Not"]: callFunc = lambda obj : bool(func(obj.rhs)) else: callFunc = lambda obj : func(obj.rhs) unOp = cls(name, callFunc, "Unary operaton with one operand") unOp.createAttr("rhs", rhs) return unOp class ExpressionEvaluator(object): """ This class is a recursive visitor of an ast module tree. The ast tree is generated from a text expression (e.g. a > 5). Given a context object to resolve names to values, the expression evaluator will return a chain of callable parameters that can be invoked to get the result of the expression. Example: context = {a : 10} tree = ast.parse("a > 5", mode='exec') # ast library exprEval = ExpressionEvaluator.evaluate(tree, context) result = exprEval.__call__() # = True """ __slots__ = () def evaluate(self, node, namesContext): #print("Type: {0}".format(type(node).__name__)) #print(node) nodeType = type(node).__name__ # Root Level 0 node (required) if nodeType == "Module": # fields: ('body') assert len(node.body) == 1, "Expecting only one expression" return self.evaluate(node.body[0], namesContext) # Level 1 node (required) elif nodeType == "Expr": # fields: ('value') result = self.evaluate(node.value, namesContext) return result.value if hasattr(result, "value") else result # Binary operations nodes which take 2 operands elif nodeType == "BinOp": # fields: ('op', 'left', 'right') lhs = self.evaluate(node.left, namesContext) rhs = self.evaluate(node.right, namesContext) op = type(node.op).__name__ return CallableParameter.createBinaryOp(lhs, rhs, op) elif nodeType == "BoolOp": # fields: ('op', 'values') lhs = self.evaluate(node.values[0], namesContext) rhs = self.evaluate(node.values[1], namesContext) op = type(node.op).__name__ return CallableParameter.createBinaryOp(lhs, rhs, op) elif nodeType == "Compare": # fields: ('left', 'ops', 'comparators') lhs = self.evaluate(node.left, namesContext) for i in range(len(node.ops)): rhs = self.evaluate(node.comparators[i], namesContext) op = type(node.ops[i]).__name__ result = CallableParameter.createBinaryOp(lhs, rhs, op) lhs = result return lhs # Unary operations which take 1 operand elif nodeType == "UnaryOp": # fields: ('op', 'operand') value = self.evaluate(node.operand, namesContext) op = type(node.op).__name__ return CallableParameter.createUnaryOp(value, op) elif nodeType == "Call": # fields: ('func', 'args', 'keywords') if len(node.args) == 2: lhs = self.evaluate(node.args[0], namesContext) rhs = self.evaluate(node.args[1], namesContext) # For the op, give empty context so we get a string # from the name node. Then pass it into the factory op = self.evaluate(node.func, {}) return CallableParameter.createBinaryOp(lhs, rhs, op) elif len(node.args) == 1: rhs = self.evaluate(node.args[0], namesContext) # For the op, give empty context so we get a string # from the name node. Then pass it into the factory op = self.evaluate(node.func, {}) return CallableParameter.createUnaryOp(rhs, op) assert 0, "Unknown function call with {0} parameters".format(len(node.args)) elif nodeType == "Assign": # fields: ('targets', 'value') value = self.evaluate(node.value, namesContext) valueToAssign = value.value if hasattr(value, "value") else value for target in node.targets: targetType = type(target).__name__ # If we are assigning to a name, use # current context as write object. if targetType == "Name": assignObj = namesContext assignAttr = target.id # If we are assigning to an attribute, # get the embedded context to write to elif targetType == "Attribute": assignObj = self.evaluate(target, namesContext) assignAttr = target.attr else: assert 0, "Don't know how to handle target node type: {0}".format(targetType) # The assignment value might be an expression, # so if it is callable, then evaluate it assignObj[assignAttr] = valueToAssign return valueToAssign # Leaf node types elif nodeType == "Name": # fields: ('id') if node.id in namesContext: return namesContext[node.id] else: print("No context for named variable: {0}".format(node.id)) return str(node.id) elif nodeType == "Attribute": # fields: ('value', 'attr', 'ctx') # node.attr has the leaf attribute. We need to recurse and get # the leaf attribute container so we can access the attribute. # node.value has the next chained node, which is either another Attribute # or Name node newContext = self.evaluate(node.value, namesContext) assert node.attr in newContext, "No attribute for named variable: {0}".format(node.attr) # For storing context, return the target value's container so we can # setattr on it. See: Assign case. # Otherwise return the actual stored value return newContext if type(node.ctx).__name__ == "Store" else newContext[node.attr] elif nodeType == "IfExp": # fields ('test', 'body', 'orelse') theTest = self.evaluate(node.test, namesContext) trueCond = self.evaluate(node.body, namesContext) falseCond = self.evaluate(node.orelse, namesContext) return trueCond if theTest() else falseCond elif nodeType == "Num": # fields ('n') return node.n elif nodeType == "Constant": # fields ('kind', 'value') return node.value elif nodeType == "Str": # fields ('s') return node.s assert 0, "Unknown node type: {0}".format(nodeType) # Everything stored as a Parameter object, which has # value, default value and type attributes class ProjectConfig(ReadWriteTransformDict): """ Adds support for hierarchical configuration designs. Allows grouping of configurations together. Also allows dynamic constraints. Each Config entry is a Parameter type object that encapsulates attributes of Name, Type, Value, DefaultValue and Description as seen above. Example: Program: Network: IP: X.X.X.X PORT: XXXX MaxPort: YYYY UI: WindowSize: 2000 Antialiasing: True # Creation patterns: Program = ProjectConfig() networkConfig = Program.createSection("Network") networkConfig.createValue("IP", X.X.X.X, "This is the IP we connect to") networkConfig.createValue("PORT", XXXX, "Port we want to access") networkConfig.createValue("PORT", YYYY, "Maximum port value") uiConfig = Program.createSection("UI") uiConfig.createValue("WindowSize", 2000) uiConfig.createValue("Antialiasing", True) # Access patterns: IP = Program.Network.IP PORT = Program["Network"]["IP"] networkConfig = Program["Network"] # Constraints assertions: e.g. limit port numbers or antialiasing. Program.addConstraint("Program.Network.PORT >= MaxPort") Program.addConstraint("False if Program.UI.WindowSize > 2000 and Program.UI.Antialiasing else True") # Overrides: e.g. enforce port numbers Program.addConstraint("Program.Network.PORT = Program.Network.MaxPort if Program.Network.PORT > Program.Network.MaxPort") """ __slots__ = () ContainerType = Parameter SubConfigType : 'ProjectConfig' ConstraintsKey = "_Constraints" def __init__(self): # Sub-configs will be stored inside ContainerType, # so when reading them as an attribute, forward the config # stored in the container's value def readXForm(obj, key): attribute = obj.readNoTransform(key) # Attribute is a Container, which has a "value" # and should be forwarded to get around awkward # value forwarding. We are mainly interested # in accessing the value attribute, so we can # now access it with config["Attrib"] instead # of config["Attrib"]["value"] if isinstance(attribute, self.ContainerType): return attribute.readNoTransform("value") else: return attribute def writeXForm(obj, key, value): if key in super(obj.__class__, obj).keys() and isinstance(obj.readNoTransform(key), self.ContainerType): obj.readNoTransform(key).writeNoTransform("value", value) else: obj.writeNoTransform(key, value) super().__init__(readXForm, writeXForm) def __contains__(self, key): return True if super().__contains__(key) else self.toFlattenedDict().__contains__(key) def __getContainer(self, key): # In case we are accessing with keys such as [a.b.c] # Then we need to recurse to get the embedded container. # Assumes that a and b for example are embedded configs if "." in key: try: levels = key.split(".") currentValue = self for level in levels[:-1]: currentValue = currentValue[level] return currentValue.readNoTransform(levels[-1]) except: pass # Fallback, in case there is a "." in the key return self.readNoTransform(key) def __getitem__(self, key): if "." in key: try: levels = key.split(".") currentValue = self for level in levels: currentValue = currentValue[level] return currentValue except: pass # Fallback is to check if the full key lives # in the top level, in case it just happens # to have a '.' in it. return super().__getitem__(key) def __setitem__(self, key, value): # In case we are accessing with keys such as [a.b.c] # Then we need to recurse to get the embedded value. # Assumes that a and b for example are embedded configs if "." in key: try: levels = key.split(".") currentValue = self for level in levels[:-1]: currentValue = currentValue[level] setattr(currentValue, levels[-1], value) return except: pass # Fallback is to check if the full key lives # in the top level, in case it just happens # to have a '.' in it. super().__setitem__(key, value) # Simplification of creating attributes def createValue(self, name, value, defaultValue = None, description=""): self[name] = self.ContainerType(name, value, defaultValue, description) return self[name] def createSection(self, name): return self.createValue(name, ProjectConfig()) def resetToDefaults(self): for key in self.keys(): attribute = self.readNoTransform(key) if isinstance(attribute, ProjectConfig): attribute.resetToDefaults() elif isinstance(attribute, self.ContainerType): attribute.writeNoTransform("value", attribute.readNoTransform("defaultValue")) def addConstraint(self, expressionStr): if self.ConstraintsKey not in self: self.createSection(self.ConstraintsKey) self[self.ConstraintsKey].createValue(expressionStr, ast.parse(expressionStr, mode='exec')) def checkConstraints(self): result = True if self.ConstraintsKey in self: constraints = self.readNoTransform(self.ConstraintsKey).value for expression, tree in [(x, y) for (x, y) in constraints.items() if "_" not in x]: result = ExpressionEvaluator().evaluate(tree.value, self) value = result.value if isinstance(result, Parameter) else result assert value, "Constraint evaluation failed: {0}".format(expression) return value def getDefaultValue(self, name): return self.__getContainer(name).getDefault() def getDescription(self, name): return self.__getContainer(name).getDescription()