Source code for intermine.constraints

import re
import string
from intermine.pathfeatures import PathFeature, PATH_PATTERN
from intermine.util import ReadableException

[docs]class Constraint(PathFeature): """ A class representing constraints on a query =========================================== All constraints inherit from this class, which simply defines the type of element for the purposes of serialisation. """ child_type = "constraint"
[docs]class LogicNode(object): """ A class representing nodes in a logic graph =========================================== Objects which can be represented as nodes in the AST of a constraint logic graph should inherit from this class, which defines methods for overloading built-in operations. """ def __add__(self, other): """ Overloads + =========== Logic may be defined by using addition to sum logic nodes:: > query.set_logic(con_a + con_b + con_c) > str(query.logic) ... A and B and C """ if not isinstance(other, LogicNode): return NotImplemented else: return LogicGroup(self, 'AND', other) def __and__(self, other): """ Overloads & =========== Logic may be defined by using the & operator:: > query.set_logic(con_a & con_b) > sr(query.logic) ... A and B """ if not isinstance(other, LogicNode): return NotImplemented else: return LogicGroup(self, 'AND', other) def __or__(self, other): """ Overloads | =========== Logic may be defined by using the | operator:: > query.set_logic(con_a | con_b) > str(query.logic) ... A or B """ if not isinstance(other, LogicNode): return NotImplemented else: return LogicGroup(self, 'OR', other)
[docs]class LogicGroup(LogicNode): """ A logic node that represents two sub-nodes joined in some way ============================================================= A logic group is a logic node with two child nodes, which are either connected by AND or by OR logic. """ LEGAL_OPS = frozenset(['AND', 'OR']) def __init__(self, left, op, right, parent=None): """ Constructor =========== Makes a new node composes of two nodes (left and right), and some operator. Groups may have a reference to their parent. """ if not op in self.LEGAL_OPS: raise TypeError(op + " is not a legal logical operation") self.parent = parent self.left = left self.right = right self.op = op for node in [self.left, self.right]: if isinstance(node, LogicGroup): node.parent = self def __repr__(self): """ Provide a sensible representation of a node """ return '<' + self.__class__.__name__ + ': ' + str(self) + '>' def __str__(self): """ Provide a human readable version of the group. The string version should be able to be parsed back into the original logic group. """ core = ' '.join(map(str, [self.left, self.op.lower(), self.right])) if self.parent and self.op != self.parent.op: return '(' + core + ')' else: return core
[docs] def get_codes(self): """ Get a list of all constraint codes used in this group. """ codes = [] for node in [self.left, self.right]: if isinstance(node, LogicGroup): codes.extend(node.get_codes()) else: codes.append(node.code) return codes
[docs]class LogicParseError(ReadableException): """ An error representing problems in parsing constraint logic. """ pass
[docs]class EmptyLogicError(ValueError): """ An error representing the fact that an the logic string to be parsed was empty """ pass
[docs]class LogicParser(object): """ Parses logic strings into logic groups ====================================== Instances of this class are used to parse logic strings into abstract syntax trees, and then logic groups. This aims to provide robust parsing of logic strings, with the ability to identify syntax errors in such strings. """ def __init__(self, query): """ Constructor =========== Parsers need access to the query they are parsing for, in order to reference the constraints on the query. @param query: The parent query object @type query: intermine.query.Query """ self._query = query
[docs] def get_constraint(self, code): """ Get the constraint with the given code ====================================== This method fetches the constraint from the parent query with the matching code. @see: intermine.query.Query.get_constraint @rtype: intermine.constraints.CodedConstraint """ return self._query.get_constraint(code)
[docs] def get_priority(self, op): """ Get the priority for a given operator ===================================== Operators have a specific precedence, from highest to lowest: - () - AND - OR This method returns an integer which can be used to compare operator priorities. @rtype: int """ return { "AND": 2, "OR" : 1, "(" : 3, ")" : 3 }.get(op)
ops = { "AND" : "AND", "&" : "AND", "&&" : "AND", "OR" : "OR", "|" : "OR", "||" : "OR", "(" : "(", ")" : ")" }
[docs] def parse(self, logic_str): """ Parse a logic string into an abstract syntax tree ================================================= Takes a string such as "A and B or C and D", and parses it into a structure which represents this logic as a binary abstract syntax tree. The above string would parse to "(A and B) or (C and D)", as AND binds more tightly than OR. Note that only singly rooted trees are parsed. @param logic_str: The logic defininition as a string @type logic_str: string @rtype: LogicGroup @raise LogicParseError: if there is a syntax error in the logic """ def flatten(l): """Flatten out a list which contains both values and sublists""" ret = [] for item in l: if isinstance(item, list): ret.extend(item) else: ret.append(item) return ret def canonical(x, d): if x in d: return d[x] else: return re.split("\b", x) def dedouble(x): if re.search("[()]", x): return list(x) else: return x logic_str = logic_str.upper() tokens = [t for t in re.split("\s+", logic_str) if t] if not tokens: raise EmptyLogicError() tokens = flatten([canonical(x, self.ops) for x in tokens]) tokens = flatten([dedouble(x) for x in tokens]) self.check_syntax(tokens) postfix_tokens = self.infix_to_postfix(tokens) abstract_syntax_tree = self.postfix_to_tree(postfix_tokens) return abstract_syntax_tree
[docs] def check_syntax(self, infix_tokens): """ Check the syntax for errors before parsing ========================================== Syntax is checked before parsing to provide better errors, which should hopefully lead to more informative error messages. This checks for: - correct operator positions (cannot put two codes next to each other without intervening operators) - correct grouping (all brackets are matched, and contain valid expressions) @param infix_tokens: The input parsed into a list of tokens. @type infix_tokens: iterable @raise LogicParseError: if there is a problem. """ need_an_op = False need_binary_op_or_closing_bracket = False processed = [] open_brackets = 0 for token in infix_tokens: if token not in self.ops: if need_an_op: raise LogicParseError("Expected an operator after: '" + ' '.join(processed) + "'" + " - but got: '" + token + "'") if need_binary_op_or_closing_bracket: raise LogicParseError("Logic grouping error after: '" + ' '.join(processed) + "'" + " - expected an operator or a closing bracket") need_an_op = True else: need_an_op = False if token == "(": if processed and processed[-1] not in self.ops: raise LogicParseError("Logic grouping error after: '" + ' '.join(processed) + "'" + " - got an unexpeced opening bracket") if need_binary_op_or_closing_bracket: raise LogicParseError("Logic grouping error after: '" + ' '.join(processed) + "'" + " - expected an operator or a closing bracket") open_brackets += 1 elif token == ")": need_binary_op_or_closing_bracket = True open_brackets -= 1 else: need_binary_op_or_closing_bracket = False processed.append(token) if open_brackets != 0: if open_brackets < 0: message = "Unmatched closing bracket in: " else: message = "Unmatched opening bracket in: " raise LogicParseError(message + '"' + ' '.join(infix_tokens) + '"')
[docs] def infix_to_postfix(self, infix_tokens): """ Convert a list of infix tokens to postfix notation ================================================== Take in a set of infix tokens and return the set parsed to a postfix sequence. @param infix_tokens: The list of tokens @type infix_tokens: iterable @rtype: list """ stack = [] postfix_tokens = [] for token in infix_tokens: if token not in self.ops: postfix_tokens.append(token) else: op = token if op == "(": stack.append(token) elif op == ")": while stack: last_op = stack.pop() if last_op == "(": if stack: previous_op = stack.pop() if previous_op != "(": postfix_tokens.append(previous_op) break else: postfix_tokens.append(last_op) else: while stack and self.get_priority(stack[-1]) <= self.get_priority(op): prev_op = stack.pop() if prev_op != "(": postfix_tokens.append(prev_op) stack.append(op) while stack: postfix_tokens.append(stack.pop()) return postfix_tokens
[docs] def postfix_to_tree(self, postfix_tokens): """ Convert a set of structured tokens to a single LogicGroup ========================================================= Convert a set of tokens in postfix notation to a single LogicGroup object. @param postfix_tokens: A list of tokens in postfix notation. @type postfix_tokens: list @rtype: LogicGroup @raise AssertionError: is the tree doesn't have a unique root. """ stack = [] try: for token in postfix_tokens: if token not in self.ops: stack.append(self.get_constraint(token)) else: op = token right = stack.pop() left = stack.pop() stack.append(LogicGroup(left, op, right)) assert len(stack) == 1, "Tree doesn't have a unique root" return stack.pop() except IndexError: raise EmptyLogicError()
[docs]class CodedConstraint(Constraint, LogicNode): """ A parent class for all constraints that have codes ================================================== Constraints that have codes are the principal logical filters on queries, and need to be refered to individually (hence the codes). They will all have a logical operation they embody, and so have a reference to an operator. This class is not meant to be instantiated directly, but instead inherited from to supply default behaviour. """ OPS = set([]) def __init__(self, path, op, code="A"): """ Constructor =========== @param path: The path to constrain @type path: string @param op: The operation to apply - must be in the OPS set @type op: string """ if op not in self.OPS: raise TypeError(op + " not in " + str(self.OPS)) self.op = op self.code = code super(CodedConstraint, self).__init__(path)
[docs] def get_codes(self): return [self.code]
def __str__(self): """ Stringify to the code they are refered to by. """ return self.code
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(CodedConstraint, self).to_string() return " ".join([s, self.op])
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(CodedConstraint, self).to_dict() d.update(op=self.op, code=self.code) return d
[docs]class UnaryConstraint(CodedConstraint): """ Constraints which have just a path and an operator ================================================== These constraints are simple assertions about the object/value refered to by the path. The set of valid operators is: - IS NULL - IS NOT NULL """ OPS = set(['IS NULL', 'IS NOT NULL'])
[docs]class BinaryConstraint(CodedConstraint): """ Constraints which have an operator and a value ============================================== These constraints assert a relationship between the value represented by the path (it must be a representation of a value, ie an Attribute) and another value - ie. the operator takes two parameters. In all case the 'left' side of the relationship is the path, and the 'right' side is the supplied value. Valid operators are: - = (equal to) - != (not equal to) - < (less than) - > (greater than) - <= (less than or equal to) - >= (greater than or equal to) - LIKE (same as equal to, but with implied wildcards) - CONTAINS (same as equal to, but with implied wildcards) - NOT LIKE (same as not equal to, but with implied wildcards) """ OPS = set(['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'CONTAINS']) def __init__(self, path, op, value, code="A"): """ Constructor =========== @param path: The path to constrain @type path: string @param op: The relationship between the value represented by the path and the value provided (must be a valid operator) @type op: string @param value: The value to compare the stored value to @type value: string or number @param code: The code for this constraint (default = "A") @type code: string """ self.value = value super(BinaryConstraint, self).__init__(path, op, code)
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(BinaryConstraint, self).to_string() return " ".join([s, str(self.value)])
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(BinaryConstraint, self).to_dict() d.update(value=str(self.value)) return d
[docs]class ListConstraint(CodedConstraint): """ Constraints which refer to an objects membership of lists ========================================================= These constraints assert a membership relationship between the object represented by the path (it must always be an object, ie. a Reference or a Class) and a List. Lists are collections of objects in the database which are stored in InterMine datawarehouses. These lists must be set up before the query is run, either manually in the webapp or by using the webservice API list upload feature. Valid operators are: - IN - NOT IN """ OPS = set(['IN', 'NOT IN']) def __init__(self, path, op, list_name, code="A"): if hasattr(list_name, 'to_query'): q = list_name.to_query() l = q.service.create_list(q) self.list_name = l.name elif hasattr(list_name, "name"): self.list_name = list_name.name else: self.list_name = list_name super(ListConstraint, self).__init__(path, op, code)
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(ListConstraint, self).to_string() return " ".join([s, str(self.list_name)])
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(ListConstraint, self).to_dict() d.update(value=str(self.list_name)) return d
[docs]class LoopConstraint(CodedConstraint): """ Constraints with refer to object identity ========================================= These constraints assert that two paths refer to the same object. Valid operators: - IS - IS NOT The operators IS and IS NOT map to the ops "=" and "!=" when they are used in XML serialisation. """ OPS = set(['IS', 'IS NOT']) SERIALISED_OPS = {'IS':'=', 'IS NOT':'!='} def __init__(self, path, op, loopPath, code="A"): """ Constructor =========== @param path: The path to constrain @type path: string @param op: The relationship between the path and the path provided (must be a valid operator) @type op: string @param loopPath: The path to check for identity against @type loopPath: string @param code: The code for this constraint (default = "A") @type code: string """ self.loopPath = loopPath super(LoopConstraint, self).__init__(path, op, code)
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(LoopConstraint, self).to_string() return " ".join([s, self.loopPath])
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(LoopConstraint, self).to_dict() d.update(loopPath=self.loopPath, op=self.SERIALISED_OPS[self.op]) return d
[docs]class TernaryConstraint(BinaryConstraint): """ Constraints for broad, general searching over all fields ======================================================== These constraints request a wide-ranging search for matching fields over all aspects of an object, including up to coercion from related classes. Valid operators: - LOOKUP To aid disambiguation, Ternary constaints accept an extra_value as well as the main value. """ OPS = set(['LOOKUP']) def __init__(self, path, op, value, extra_value=None, code="A"): """ Constructor =========== @param path: The path to constrain. Here is must be a class, or a reference to a class. @type path: string @param op: The relationship between the path and the path provided (must be a valid operator) @type op: string @param value: The value to check other fields against. @type value: string @param extra_value: A further value for disambiguation. The meaning of this value varies by class and configuration. For example, if the class of the object is Gene, then extra_value will refer to the Organism. @type extra_value: string @param code: The code for this constraint (default = "A") @type code: string """ self.extra_value = extra_value super(TernaryConstraint, self).__init__(path, op, value, code)
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(TernaryConstraint, self).to_string() if self.extra_value is None: return s else: return " ".join([s, 'IN', self.extra_value])
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(TernaryConstraint, self).to_dict() if self.extra_value is not None: d.update(extraValue=self.extra_value) return d
[docs]class MultiConstraint(CodedConstraint): """ Constraints for checking membership of a set of values ====================================================== These constraints require the value they constrain to be either a member of a set of values, or not a member. Valid operators: - ONE OF - NONE OF These constraints are similar in use to List constraints, with the following differences: - The list in this case is a defined set of values that is passed along with the query itself, rather than anything stored independently on a server. - The object of the constaint is the value of an attribute, rather than an object's identity. """ OPS = set(['ONE OF', 'NONE OF']) def __init__(self, path, op, values, code="A"): """ Constructor =========== @param path: The path to constrain. Here it must be an attribute of some object. @type path: string @param op: The relationship between the path and the path provided (must be a valid operator) @type op: string @param values: The set of values which the object of the constraint either must or must not belong to. @type values: set or list @param code: The code for this constraint (default = "A") @type code: string """ if not isinstance(values, (set, list)): raise TypeError("values must be a set or a list, not " + str(type(values))) self.values = values super(MultiConstraint, self).__init__(path, op, code)
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(MultiConstraint, self).to_string() return ' '.join([s, str(self.values)])
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(MultiConstraint, self).to_dict() d.update(value=self.values) return d
[docs]class RangeConstraint(MultiConstraint): """ Constraints for testing where a value lies relative to a set of ranges ====================================================================== These constraints require that the value of the path they constrain should lie in relationship to the set of values passed according to the specific operator. Valid operators: - OVERLAPS : The value overlaps at least one of the given ranges - WITHIN : The value is wholly outside the given set of ranges - CONTAINS : The value contains all the given ranges - DOES NOT CONTAIN : The value does not contain all the given ranges - OUTSIDE : Some part is outside the given set of ranges - DOES NOT OVERLAP : The value does not overlap with any of the ranges For example: 4 WITHIN [1..5, 20..25] => True The format of the ranges depends on the value being constrained and what range parsers have been configured on the target server. A common range parser for biological mines is the one for Locations: Gene.chromosomeLocation OVERLAPS [2X:54321..67890, 3R:12345..456789] """ OPS = set(['OVERLAPS', 'DOES NOT OVERLAP', 'WITHIN', 'OUTSIDE', 'CONTAINS', 'DOES NOT CONTAIN'])
[docs]class IsaConstraint(MultiConstraint): """ Constraints for testing the class of a value, as a disjoint union ====================================================================== These constraints require that the value of the path they constrain should be an instance of one of the classes provided. Valid operators: - ISA : The value is an instance of one of the provided classes. For example: SequenceFeature ISA [Exon, Intron] """ OPS = set(['ISA'])
[docs]class SubClassConstraint(Constraint): """ Constraints on the class of a reference ======================================= If an object has a reference X to another object of type A, and type B extends type A, then any object of type B may be the value of the reference X. If you only want to see X's which are B's, this may be achieved with subclass constraints, which allow the type of an object to be limited to one of the subclasses (at any depth) of the class type required by the attribute. These constraints do not use operators. Since they cannot be conditional (eg. "A is a B or A is a C" would not be possible in an InterMine query), they do not have codes and cannot be referenced in logic expressions. """ def __init__(self, path, subclass): """ Constructor =========== @param path: The path to constrain. This must refer to a class or a reference to a class. @type path: str @param subclass: The class to subclass the path to. This must be a simple class name (not a dotted name) @type subclass: str """ if not PATH_PATTERN.match(subclass): raise TypeError self.subclass = subclass super(SubClassConstraint, self).__init__(path)
[docs] def to_string(self): """ Provide a human readable representation of the logic. This method is called by repr. """ s = super(SubClassConstraint, self).to_string() return s + ' ISA ' + self.subclass
[docs] def to_dict(self): """ Return a dict object which can be used to construct a DOM element with the appropriate attributes. """ d = super(SubClassConstraint, self).to_dict() d.update(type=self.subclass) return d
[docs]class TemplateConstraint(object): """ A mixin to supply the behaviour and state of constraints on templates ===================================================================== Constraints on templates can also be designated as "on", "off" or "locked", which refers to whether they are active or not. Inactive constraints are still configured, but behave as if absent for the purpose of results. In addition, template constraints can be editable or not. Only values for editable constraints can be provided when requesting results, and only constraints that can participate in logic expressions can be editable. """ REQUIRED = "locked" OPTIONAL_ON = "on" OPTIONAL_OFF = "off" def __init__(self, editable=True, optional="locked"): """ Constructor =========== @param editable: Whether or not this constraint should accept new values. @type editable: bool @param optional: Whether a value for this constraint must be provided when running. @type optional: "locked", "on" or "off" """ self.editable = editable if optional == TemplateConstraint.REQUIRED: self.optional = False self.switched_on = True else: self.optional = True if optional == TemplateConstraint.OPTIONAL_ON: self.switched_on = True elif optional == TemplateConstraint.OPTIONAL_OFF: self.switched_on = False else: raise TypeError("Bad value for optional") @property def required(self): """ True if a value must be provided for this constraint. @rtype: bool """ return not self.optional @property def switched_off(self): """ True if this constraint is currently inactive. @rtype: bool """ return not self.switched_on
[docs] def get_switchable_status(self): """ Returns either "locked", "on" or "off". """ if not self.optional: return "locked" else: if self.switched_on: return "on" else: return "off"
[docs] def switch_on(self): """ Make sure this constraint is active =================================== @raise ValueError: if the constraint is not editable and optional """ if self.editable and self.optional: self.switched_on = True else: raise ValueError("This constraint is not switchable")
[docs] def switch_off(self): """ Make sure this constraint is inactive ===================================== @raise ValueError: if the constraint is not editable and optional """ if self.editable and self.optional: self.switched_on = False else: raise ValueError("This constraint is not switchable")
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ if self.editable: editable = "editable" else: editable = "non-editable" return '(' + editable + ", " + self.get_switchable_status() + ')'
[docs] def separate_arg_sets(self, args): """ A static function to use when building template constraints. ============================================================ dict -> (dict, dict) Splits a dictionary of arguments into two separate dictionaries, one with arguments for the main constraint, and one with arguments for the template portion of the behaviour """ c_args = {} t_args = {} for k, v in list(args.items()): if k == "editable": t_args[k] = v == "true" elif k == "optional": t_args[k] = v else: c_args[k] = v return (c_args, t_args)
[docs]class TemplateUnaryConstraint(UnaryConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) UnaryConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(UnaryConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateBinaryConstraint(BinaryConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) BinaryConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(BinaryConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateListConstraint(ListConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) ListConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(ListConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateLoopConstraint(LoopConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) LoopConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(LoopConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateTernaryConstraint(TernaryConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) TernaryConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(TernaryConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateMultiConstraint(MultiConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) MultiConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(MultiConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateRangeConstraint(RangeConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) RangeConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(RangeConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateIsaConstraint(IsaConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) IsaConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(IsaConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class TemplateSubClassConstraint(SubClassConstraint, TemplateConstraint): def __init__(self, *a, **d): (c_args, t_args) = self.separate_arg_sets(d) SubClassConstraint.__init__(self, *a, **c_args) TemplateConstraint.__init__(self, **t_args)
[docs] def to_string(self): """ Provide a template specific human readable representation of the constraint. This method is called by repr. """ return(SubClassConstraint.to_string(self) + " " + TemplateConstraint.to_string(self))
[docs]class ConstraintFactory(object): """ A factory for creating constraints from a set of arguments. =========================================================== A constraint factory is responsible for finding an appropriate constraint class for the given arguments and instantiating the constraint. """ CONSTRAINT_CLASSES = set([ UnaryConstraint, BinaryConstraint, TernaryConstraint, MultiConstraint, SubClassConstraint, LoopConstraint, ListConstraint, RangeConstraint, IsaConstraint]) def __init__(self): """ Constructor =========== Creates a new ConstraintFactory """ self._codes = iter(string.ascii_uppercase) self.reference_ops = TernaryConstraint.OPS | RangeConstraint.OPS | ListConstraint.OPS | IsaConstraint.OPS
[docs] def get_next_code(self): """ Return the available constraint code. @return: A single uppercase character @rtype: str """ return next(self._codes)
[docs] def make_constraint(self, *args, **kwargs): """ Create a constraint from a set of arguments. ============================================ Finds a suitable constraint class, and instantiates it. @rtype: Constraint """ for CC in self.CONSTRAINT_CLASSES: try: c = CC(*args, **kwargs) if hasattr(c, "code") and c.code == "A": c.code = self.get_next_code() return c except TypeError as e: pass raise TypeError("No matching constraint class found for " + str(args) + ", " + str(kwargs))
[docs]class TemplateConstraintFactory(ConstraintFactory): """ A factory for creating constraints with template specific characteristics. ========================================================================== A constraint factory is responsible for finding an appropriate constraint class for the given arguments and instantiating the constraint. TemplateConstraintFactories make constraints with the extra set of TemplateConstraint qualities. """ CONSTRAINT_CLASSES = set([ TemplateUnaryConstraint, TemplateBinaryConstraint, TemplateTernaryConstraint, TemplateMultiConstraint, TemplateSubClassConstraint, TemplateLoopConstraint, TemplateListConstraint, TemplateRangeConstraint, TemplateIsaConstraint ])