Package intermine :: Module constraints
[hide private]
[frames] | no frames]

Source Code for Module intermine.constraints

   1  import re 
   2  import string 
   3  from intermine.pathfeatures import PathFeature, PATH_PATTERN 
   4  from intermine.util import ReadableException 
5 6 -class Constraint(PathFeature):
7 """ 8 A class representing constraints on a query 9 =========================================== 10 11 All constraints inherit from this class, which 12 simply defines the type of element for the 13 purposes of serialisation. 14 """ 15 child_type = "constraint"
16
17 -class LogicNode(object):
18 """ 19 A class representing nodes in a logic graph 20 =========================================== 21 22 Objects which can be represented as nodes 23 in the AST of a constraint logic graph should 24 inherit from this class, which defines 25 methods for overloading built-in operations. 26 """ 27
28 - def __add__(self, other):
29 """ 30 Overloads + 31 =========== 32 33 Logic may be defined by using addition to sum 34 logic nodes:: 35 36 > query.set_logic(con_a + con_b + con_c) 37 > str(query.logic) 38 ... A and B and C 39 40 """ 41 if not isinstance(other, LogicNode): 42 return NotImplemented 43 else: 44 return LogicGroup(self, 'AND', other)
45
46 - def __and__(self, other):
47 """ 48 Overloads & 49 =========== 50 51 Logic may be defined by using the & operator:: 52 53 > query.set_logic(con_a & con_b) 54 > sr(query.logic) 55 ... A and B 56 57 """ 58 if not isinstance(other, LogicNode): 59 return NotImplemented 60 else: 61 return LogicGroup(self, 'AND', other)
62
63 - def __or__(self, other):
64 """ 65 Overloads | 66 =========== 67 68 Logic may be defined by using the | operator:: 69 70 > query.set_logic(con_a | con_b) 71 > str(query.logic) 72 ... A or B 73 74 """ 75 if not isinstance(other, LogicNode): 76 return NotImplemented 77 else: 78 return LogicGroup(self, 'OR', other)
79
80 -class LogicGroup(LogicNode):
81 """ 82 A logic node that represents two sub-nodes joined in some way 83 ============================================================= 84 85 A logic group is a logic node with two child nodes, which are 86 either connected by AND or by OR logic. 87 """ 88 89 LEGAL_OPS = frozenset(['AND', 'OR']) 90
91 - def __init__(self, left, op, right, parent=None):
92 """ 93 Constructor 94 =========== 95 96 Makes a new node composes of two nodes (left and right), 97 and some operator. 98 99 Groups may have a reference to their parent. 100 """ 101 if not op in self.LEGAL_OPS: 102 raise TypeError(op + " is not a legal logical operation") 103 self.parent = parent 104 self.left = left 105 self.right = right 106 self.op = op 107 for node in [self.left, self.right]: 108 if isinstance(node, LogicGroup): 109 node.parent = self
110
111 - def __repr__(self):
112 """ 113 Provide a sensible representation of a node 114 """ 115 return '<' + self.__class__.__name__ + ': ' + str(self) + '>'
116
117 - def __str__(self):
118 """ 119 Provide a human readable version of the group. The 120 string version should be able to be parsed back into the 121 original logic group. 122 """ 123 core = ' '.join(map(str, [self.left, self.op.lower(), self.right])) 124 if self.parent and self.op != self.parent.op: 125 return '(' + core + ')' 126 else: 127 return core
128
129 - def get_codes(self):
130 """ 131 Get a list of all constraint codes used in this group. 132 """ 133 codes = [] 134 for node in [self.left, self.right]: 135 if isinstance(node, LogicGroup): 136 codes.extend(node.get_codes()) 137 else: 138 codes.append(node.code) 139 return codes
140
141 -class LogicParseError(ReadableException):
142 """ 143 An error representing problems in parsing constraint logic. 144 """ 145 pass
146
147 -class EmptyLogicError(ValueError):
148 """ 149 An error representing the fact that an the logic string to be parsed was empty 150 """ 151 pass
152
153 -class LogicParser(object):
154 """ 155 Parses logic strings into logic groups 156 ====================================== 157 158 Instances of this class are used to parse logic strings into 159 abstract syntax trees, and then logic groups. This aims to provide 160 robust parsing of logic strings, with the ability to identify syntax 161 errors in such strings. 162 """ 163
164 - def __init__(self, query):
165 """ 166 Constructor 167 =========== 168 169 Parsers need access to the query they are parsing for, in 170 order to reference the constraints on the query. 171 172 @param query: The parent query object 173 @type query: intermine.query.Query 174 """ 175 self._query = query
176
177 - def get_constraint(self, code):
178 """ 179 Get the constraint with the given code 180 ====================================== 181 182 This method fetches the constraint from the 183 parent query with the matching code. 184 185 @see: intermine.query.Query.get_constraint 186 @rtype: intermine.constraints.CodedConstraint 187 """ 188 return self._query.get_constraint(code)
189
190 - def get_priority(self, op):
191 """ 192 Get the priority for a given operator 193 ===================================== 194 195 Operators have a specific precedence, from highest 196 to lowest: 197 - () 198 - AND 199 - OR 200 201 This method returns an integer which can be 202 used to compare operator priorities. 203 204 @rtype: int 205 """ 206 return { 207 "AND": 2, 208 "OR" : 1, 209 "(" : 3, 210 ")" : 3 211 }.get(op)
212 213 ops = { 214 "AND" : "AND", 215 "&" : "AND", 216 "&&" : "AND", 217 "OR" : "OR", 218 "|" : "OR", 219 "||" : "OR", 220 "(" : "(", 221 ")" : ")" 222 } 223
224 - def parse(self, logic_str):
225 """ 226 Parse a logic string into an abstract syntax tree 227 ================================================= 228 229 Takes a string such as "A and B or C and D", and parses it 230 into a structure which represents this logic as a binary 231 abstract syntax tree. The above string would parse to 232 "(A and B) or (C and D)", as AND binds more tightly than OR. 233 234 Note that only singly rooted trees are parsed. 235 236 @param logic_str: The logic defininition as a string 237 @type logic_str: string 238 239 @rtype: LogicGroup 240 241 @raise LogicParseError: if there is a syntax error in the logic 242 """ 243 def flatten(l): 244 """Flatten out a list which contains both values and sublists""" 245 ret = [] 246 for item in l: 247 if isinstance(item, list): 248 ret.extend(item) 249 else: 250 ret.append(item) 251 return ret
252 def canonical(x, d): 253 if x in d: 254 return d[x] 255 else: 256 return re.split("\b", x)
257 def dedouble(x): 258 if re.search("[()]", x): 259 return list(x) 260 else: 261 return x 262 263 logic_str = logic_str.upper() 264 tokens = [t for t in re.split("\s+", logic_str) if t] 265 if not tokens: 266 raise EmptyLogicError() 267 tokens = flatten([canonical(x, self.ops) for x in tokens]) 268 tokens = flatten([dedouble(x) for x in tokens]) 269 self.check_syntax(tokens) 270 postfix_tokens = self.infix_to_postfix(tokens) 271 abstract_syntax_tree = self.postfix_to_tree(postfix_tokens) 272 return abstract_syntax_tree 273
274 - def check_syntax(self, infix_tokens):
275 """ 276 Check the syntax for errors before parsing 277 ========================================== 278 279 Syntax is checked before parsing to provide better errors, 280 which should hopefully lead to more informative error messages. 281 282 This checks for: 283 - correct operator positions (cannot put two codes next to each other without intervening operators) 284 - correct grouping (all brackets are matched, and contain valid expressions) 285 286 @param infix_tokens: The input parsed into a list of tokens. 287 @type infix_tokens: iterable 288 289 @raise LogicParseError: if there is a problem. 290 """ 291 need_an_op = False 292 need_binary_op_or_closing_bracket = False 293 processed = [] 294 open_brackets = 0 295 for token in infix_tokens: 296 if token not in self.ops: 297 if need_an_op: 298 raise LogicParseError("Expected an operator after: '" + ' '.join(processed) + "'" 299 + " - but got: '" + token + "'") 300 if need_binary_op_or_closing_bracket: 301 raise LogicParseError("Logic grouping error after: '" + ' '.join(processed) + "'" 302 + " - expected an operator or a closing bracket") 303 304 need_an_op = True 305 else: 306 need_an_op = False 307 if token == "(": 308 if processed and processed[-1] not in self.ops: 309 raise LogicParseError("Logic grouping error after: '" + ' '.join(processed) + "'" 310 + " - got an unexpeced opening bracket") 311 if need_binary_op_or_closing_bracket: 312 raise LogicParseError("Logic grouping error after: '" + ' '.join(processed) + "'" 313 + " - expected an operator or a closing bracket") 314 315 open_brackets += 1 316 elif token == ")": 317 need_binary_op_or_closing_bracket = True 318 open_brackets -= 1 319 else: 320 need_binary_op_or_closing_bracket = False 321 processed.append(token) 322 if open_brackets != 0: 323 if open_brackets < 0: 324 message = "Unmatched closing bracket in: " 325 else: 326 message = "Unmatched opening bracket in: " 327 raise LogicParseError(message + '"' + ' '.join(infix_tokens) + '"')
328
329 - def infix_to_postfix(self, infix_tokens):
330 """ 331 Convert a list of infix tokens to postfix notation 332 ================================================== 333 334 Take in a set of infix tokens and return the set parsed 335 to a postfix sequence. 336 337 @param infix_tokens: The list of tokens 338 @type infix_tokens: iterable 339 340 @rtype: list 341 """ 342 stack = [] 343 postfix_tokens = [] 344 for token in infix_tokens: 345 if token not in self.ops: 346 postfix_tokens.append(token) 347 else: 348 op = token 349 if op == "(": 350 stack.append(token) 351 elif op == ")": 352 while stack: 353 last_op = stack.pop() 354 if last_op == "(": 355 if stack: 356 previous_op = stack.pop() 357 if previous_op != "(": postfix_tokens.append(previous_op) 358 break 359 else: 360 postfix_tokens.append(last_op) 361 else: 362 while stack and self.get_priority(stack[-1]) <= self.get_priority(op): 363 prev_op = stack.pop() 364 if prev_op != "(": postfix_tokens.append(prev_op) 365 stack.append(op) 366 while stack: postfix_tokens.append(stack.pop()) 367 return postfix_tokens
368
369 - def postfix_to_tree(self, postfix_tokens):
370 """ 371 Convert a set of structured tokens to a single LogicGroup 372 ========================================================= 373 374 Convert a set of tokens in postfix notation to a single 375 LogicGroup object. 376 377 @param postfix_tokens: A list of tokens in postfix notation. 378 @type postfix_tokens: list 379 380 @rtype: LogicGroup 381 382 @raise AssertionError: is the tree doesn't have a unique root. 383 """ 384 stack = [] 385 try: 386 for token in postfix_tokens: 387 if token not in self.ops: 388 stack.append(self.get_constraint(token)) 389 else: 390 op = token 391 right = stack.pop() 392 left = stack.pop() 393 stack.append(LogicGroup(left, op, right)) 394 assert len(stack) == 1, "Tree doesn't have a unique root" 395 return stack.pop() 396 except IndexError: 397 raise EmptyLogicError()
398
399 -class CodedConstraint(Constraint, LogicNode):
400 """ 401 A parent class for all constraints that have codes 402 ================================================== 403 404 Constraints that have codes are the principal logical 405 filters on queries, and need to be refered to individually 406 (hence the codes). They will all have a logical operation they 407 embody, and so have a reference to an operator. 408 409 This class is not meant to be instantiated directly, but instead 410 inherited from to supply default behaviour. 411 """ 412 413 OPS = set([]) 414
415 - def __init__(self, path, op, code="A"):
416 """ 417 Constructor 418 =========== 419 420 @param path: The path to constrain 421 @type path: string 422 423 @param op: The operation to apply - must be in the OPS set 424 @type op: string 425 """ 426 if op not in self.OPS: 427 raise TypeError(op + " not in " + str(self.OPS)) 428 self.op = op 429 self.code = code 430 super(CodedConstraint, self).__init__(path)
431
432 - def get_codes(self):
433 return [self.code]
434
435 - def __str__(self):
436 """ 437 Stringify to the code they are refered to by. 438 """ 439 return self.code
440 - def to_string(self):
441 """ 442 Provide a human readable representation of the logic. 443 This method is called by repr. 444 """ 445 s = super(CodedConstraint, self).to_string() 446 return " ".join([s, self.op])
447
448 - def to_dict(self):
449 """ 450 Return a dict object which can be used to construct a 451 DOM element with the appropriate attributes. 452 """ 453 d = super(CodedConstraint, self).to_dict() 454 d.update(op=self.op, code=self.code) 455 return d
456
457 -class UnaryConstraint(CodedConstraint):
458 """ 459 Constraints which have just a path and an operator 460 ================================================== 461 462 These constraints are simple assertions about the 463 object/value refered to by the path. The set of valid 464 operators is: 465 - IS NULL 466 - IS NOT NULL 467 468 """ 469 OPS = set(['IS NULL', 'IS NOT NULL'])
470
471 -class BinaryConstraint(CodedConstraint):
472 """ 473 Constraints which have an operator and a value 474 ============================================== 475 476 These constraints assert a relationship between the 477 value represented by the path (it must be a representation 478 of a value, ie an Attribute) and another value - ie. the 479 operator takes two parameters. 480 481 In all case the 'left' side of the relationship is the path, 482 and the 'right' side is the supplied value. 483 484 Valid operators are: 485 - = (equal to) 486 - != (not equal to) 487 - < (less than) 488 - > (greater than) 489 - <= (less than or equal to) 490 - >= (greater than or equal to) 491 - LIKE (same as equal to, but with implied wildcards) 492 - CONTAINS (same as equal to, but with implied wildcards) 493 - NOT LIKE (same as not equal to, but with implied wildcards) 494 495 """ 496 OPS = set(['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'CONTAINS'])
497 - def __init__(self, path, op, value, code="A"):
498 """ 499 Constructor 500 =========== 501 502 @param path: The path to constrain 503 @type path: string 504 505 @param op: The relationship between the value represented by the path and the value provided (must be a valid operator) 506 @type op: string 507 508 @param value: The value to compare the stored value to 509 @type value: string or number 510 511 @param code: The code for this constraint (default = "A") 512 @type code: string 513 """ 514 self.value = value 515 super(BinaryConstraint, self).__init__(path, op, code)
516
517 - def to_string(self):
518 """ 519 Provide a human readable representation of the logic. 520 This method is called by repr. 521 """ 522 s = super(BinaryConstraint, self).to_string() 523 return " ".join([s, str(self.value)])
524 - def to_dict(self):
525 """ 526 Return a dict object which can be used to construct a 527 DOM element with the appropriate attributes. 528 """ 529 d = super(BinaryConstraint, self).to_dict() 530 d.update(value=str(self.value)) 531 return d
532
533 -class ListConstraint(CodedConstraint):
534 """ 535 Constraints which refer to an objects membership of lists 536 ========================================================= 537 538 These constraints assert a membership relationship between the 539 object represented by the path (it must always be an object, ie. 540 a Reference or a Class) and a List. Lists are collections of 541 objects in the database which are stored in InterMine 542 datawarehouses. These lists must be set up before the query is run, either 543 manually in the webapp or by using the webservice API list 544 upload feature. 545 546 Valid operators are: 547 - IN 548 - NOT IN 549 550 """ 551 OPS = set(['IN', 'NOT IN'])
552 - def __init__(self, path, op, list_name, code="A"):
553 if hasattr(list_name, 'to_query'): 554 q = list_name.to_query() 555 l = q.service.create_list(q) 556 self.list_name = l.name 557 elif hasattr(list_name, "name"): 558 self.list_name = list_name.name 559 else: 560 self.list_name = list_name 561 super(ListConstraint, self).__init__(path, op, code)
562
563 - def to_string(self):
564 """ 565 Provide a human readable representation of the logic. 566 This method is called by repr. 567 """ 568 s = super(ListConstraint, self).to_string() 569 return " ".join([s, str(self.list_name)])
570 - def to_dict(self):
571 """ 572 Return a dict object which can be used to construct a 573 DOM element with the appropriate attributes. 574 """ 575 d = super(ListConstraint, self).to_dict() 576 d.update(value=str(self.list_name)) 577 return d
578
579 -class LoopConstraint(CodedConstraint):
580 """ 581 Constraints with refer to object identity 582 ========================================= 583 584 These constraints assert that two paths refer to the same 585 object. 586 587 Valid operators: 588 - IS 589 - IS NOT 590 591 The operators IS and IS NOT map to the ops "=" and "!=" when they 592 are used in XML serialisation. 593 594 """ 595 OPS = set(['IS', 'IS NOT']) 596 SERIALISED_OPS = {'IS':'=', 'IS NOT':'!='}
597 - def __init__(self, path, op, loopPath, code="A"):
598 """ 599 Constructor 600 =========== 601 602 @param path: The path to constrain 603 @type path: string 604 605 @param op: The relationship between the path and the path provided (must be a valid operator) 606 @type op: string 607 608 @param loopPath: The path to check for identity against 609 @type loopPath: string 610 611 @param code: The code for this constraint (default = "A") 612 @type code: string 613 """ 614 self.loopPath = loopPath 615 super(LoopConstraint, self).__init__(path, op, code)
616
617 - def to_string(self):
618 """ 619 Provide a human readable representation of the logic. 620 This method is called by repr. 621 """ 622 s = super(LoopConstraint, self).to_string() 623 return " ".join([s, self.loopPath])
624 - def to_dict(self):
625 """ 626 Return a dict object which can be used to construct a 627 DOM element with the appropriate attributes. 628 """ 629 d = super(LoopConstraint, self).to_dict() 630 d.update(loopPath=self.loopPath, op=self.SERIALISED_OPS[self.op]) 631 return d
632
633 -class TernaryConstraint(BinaryConstraint):
634 """ 635 Constraints for broad, general searching over all fields 636 ======================================================== 637 638 These constraints request a wide-ranging search for matching 639 fields over all aspects of an object, including up to coercion 640 from related classes. 641 642 Valid operators: 643 - LOOKUP 644 645 To aid disambiguation, Ternary constaints accept an extra_value as 646 well as the main value. 647 """ 648 OPS = set(['LOOKUP'])
649 - def __init__(self, path, op, value, extra_value=None, code="A"):
650 """ 651 Constructor 652 =========== 653 654 @param path: The path to constrain. Here is must be a class, or a reference to a class. 655 @type path: string 656 657 @param op: The relationship between the path and the path provided (must be a valid operator) 658 @type op: string 659 660 @param value: The value to check other fields against. 661 @type value: string 662 663 @param extra_value: A further value for disambiguation. The meaning of this value varies by class 664 and configuration. For example, if the class of the object is Gene, then 665 extra_value will refer to the Organism. 666 @type extra_value: string 667 668 @param code: The code for this constraint (default = "A") 669 @type code: string 670 """ 671 self.extra_value = extra_value 672 super(TernaryConstraint, self).__init__(path, op, value, code)
673
674 - def to_string(self):
675 """ 676 Provide a human readable representation of the logic. 677 This method is called by repr. 678 """ 679 s = super(TernaryConstraint, self).to_string() 680 if self.extra_value is None: 681 return s 682 else: 683 return " ".join([s, 'IN', self.extra_value])
684 - def to_dict(self):
685 """ 686 Return a dict object which can be used to construct a 687 DOM element with the appropriate attributes. 688 """ 689 d = super(TernaryConstraint, self).to_dict() 690 if self.extra_value is not None: 691 d.update(extraValue=self.extra_value) 692 return d
693
694 -class MultiConstraint(CodedConstraint):
695 """ 696 Constraints for checking membership of a set of values 697 ====================================================== 698 699 These constraints require the value they constrain to be 700 either a member of a set of values, or not a member. 701 702 Valid operators: 703 - ONE OF 704 - NONE OF 705 706 These constraints are similar in use to List constraints, with 707 the following differences: 708 - The list in this case is a defined set of values that is passed 709 along with the query itself, rather than anything stored 710 independently on a server. 711 - The object of the constaint is the value of an attribute, rather 712 than an object's identity. 713 """ 714 OPS = set(['ONE OF', 'NONE OF'])
715 - def __init__(self, path, op, values, code="A"):
716 """ 717 Constructor 718 =========== 719 720 @param path: The path to constrain. Here it must be an attribute of some object. 721 @type path: string 722 723 @param op: The relationship between the path and the path provided (must be a valid operator) 724 @type op: string 725 726 @param values: The set of values which the object of the constraint either must or must not belong to. 727 @type values: set or list 728 729 @param code: The code for this constraint (default = "A") 730 @type code: string 731 """ 732 if not isinstance(values, (set, list)): 733 raise TypeError("values must be a set or a list, not " + str(type(values))) 734 self.values = values 735 super(MultiConstraint, self).__init__(path, op, code)
736
737 - def to_string(self):
738 """ 739 Provide a human readable representation of the logic. 740 This method is called by repr. 741 """ 742 s = super(MultiConstraint, self).to_string() 743 return ' '.join([s, str(self.values)])
744 - def to_dict(self):
745 """ 746 Return a dict object which can be used to construct a 747 DOM element with the appropriate attributes. 748 """ 749 d = super(MultiConstraint, self).to_dict() 750 d.update(value=self.values) 751 return d
752
753 -class RangeConstraint(MultiConstraint):
754 """ 755 Constraints for testing where a value lies relative to a set of ranges 756 ====================================================================== 757 758 These constraints require that the value of the path they constrain 759 should lie in relationship to the set of values passed according to 760 the specific operator. 761 762 Valid operators: 763 - OVERLAPS : The value overlaps at least one of the given ranges 764 - WITHIN : The value is wholly outside the given set of ranges 765 - CONTAINS : The value contains all the given ranges 766 - DOES NOT CONTAIN : The value does not contain all the given ranges 767 - OUTSIDE : Some part is outside the given set of ranges 768 - DOES NOT OVERLAP : The value does not overlap with any of the ranges 769 770 For example: 771 772 4 WITHIN [1..5, 20..25] => True 773 774 The format of the ranges depends on the value being constrained and what range 775 parsers have been configured on the target server. A common range parser for 776 biological mines is the one for Locations: 777 778 Gene.chromosomeLocation OVERLAPS [2X:54321..67890, 3R:12345..456789] 779 780 """ 781 OPS = set(['OVERLAPS', 'DOES NOT OVERLAP', 'WITHIN', 'OUTSIDE', 'CONTAINS', 'DOES NOT CONTAIN'])
782
783 -class SubClassConstraint(Constraint):
784 """ 785 Constraints on the class of a reference 786 ======================================= 787 788 If an object has a reference X to another object of type A, 789 and type B extends type A, then any object of type B may be 790 the value of the reference X. If you only want to see X's 791 which are B's, this may be achieved with subclass constraints, 792 which allow the type of an object to be limited to one of the 793 subclasses (at any depth) of the class type required 794 by the attribute. 795 796 These constraints do not use operators. Since they cannot be 797 conditional (eg. "A is a B or A is a C" would not be possible 798 in an InterMine query), they do not have codes 799 and cannot be referenced in logic expressions. 800 """
801 - def __init__(self, path, subclass):
802 """ 803 Constructor 804 =========== 805 806 @param path: The path to constrain. This must refer to a class or a reference to a class. 807 @type path: str 808 809 @param subclass: The class to subclass the path to. This must be a simple class name (not a dotted name) 810 @type subclass: str 811 """ 812 if not PATH_PATTERN.match(subclass): 813 raise TypeError 814 self.subclass = subclass 815 super(SubClassConstraint, self).__init__(path)
816 - def to_string(self):
817 """ 818 Provide a human readable representation of the logic. 819 This method is called by repr. 820 """ 821 s = super(SubClassConstraint, self).to_string() 822 return s + ' ISA ' + self.subclass
823 - def to_dict(self):
824 """ 825 Return a dict object which can be used to construct a 826 DOM element with the appropriate attributes. 827 """ 828 d = super(SubClassConstraint, self).to_dict() 829 d.update(type=self.subclass) 830 return d
831
832 833 -class TemplateConstraint(object):
834 """ 835 A mixin to supply the behaviour and state of constraints on templates 836 ===================================================================== 837 838 Constraints on templates can also be designated as "on", "off" or "locked", which refers 839 to whether they are active or not. Inactive constraints are still configured, but behave 840 as if absent for the purpose of results. In addition, template constraints can be 841 editable or not. Only values for editable constraints can be provided when requesting results, 842 and only constraints that can participate in logic expressions can be editable. 843 """ 844 REQUIRED = "locked" 845 OPTIONAL_ON = "on" 846 OPTIONAL_OFF = "off"
847 - def __init__(self, editable=True, optional="locked"):
848 """ 849 Constructor 850 =========== 851 852 @param editable: Whether or not this constraint should accept new values. 853 @type editable: bool 854 855 @param optional: Whether a value for this constraint must be provided when running. 856 @type optional: "locked", "on" or "off" 857 """ 858 self.editable = editable 859 if optional == TemplateConstraint.REQUIRED: 860 self.optional = False 861 self.switched_on = True 862 else: 863 self.optional = True 864 if optional == TemplateConstraint.OPTIONAL_ON: 865 self.switched_on = True 866 elif optional == TemplateConstraint.OPTIONAL_OFF: 867 self.switched_on = False 868 else: 869 raise TypeError("Bad value for optional")
870 871 @property
872 - def required(self):
873 """ 874 True if a value must be provided for this constraint. 875 876 @rtype: bool 877 """ 878 return not self.optional
879 880 @property
881 - def switched_off(self):
882 """ 883 True if this constraint is currently inactive. 884 885 @rtype: bool 886 """ 887 return not self.switched_on
888
889 - def get_switchable_status(self):
890 """ 891 Returns either "locked", "on" or "off". 892 """ 893 if not self.optional: 894 return "locked" 895 else: 896 if self.switched_on: 897 return "on" 898 else: 899 return "off"
900
901 - def switch_on(self):
902 """ 903 Make sure this constraint is active 904 =================================== 905 906 @raise ValueError: if the constraint is not editable and optional 907 """ 908 if self.editable and self.optional: 909 self.switched_on = True 910 else: 911 raise ValueError, "This constraint is not switchable"
912
913 - def switch_off(self):
914 """ 915 Make sure this constraint is inactive 916 ===================================== 917 918 @raise ValueError: if the constraint is not editable and optional 919 """ 920 if self.editable and self.optional: 921 self.switched_on = False 922 else: 923 raise ValueError, "This constraint is not switchable"
924
925 - def to_string(self):
926 """ 927 Provide a template specific human readable representation of the 928 constraint. This method is called by repr. 929 """ 930 if self.editable: 931 editable = "editable" 932 else: 933 editable = "non-editable" 934 return '(' + editable + ", " + self.get_switchable_status() + ')'
935 - def separate_arg_sets(self, args):
936 """ 937 A static function to use when building template constraints. 938 ============================================================ 939 940 dict -> (dict, dict) 941 942 Splits a dictionary of arguments into two separate dictionaries, one with 943 arguments for the main constraint, and one with arguments for the template 944 portion of the behaviour 945 """ 946 c_args = {} 947 t_args = {} 948 for k, v in args.items(): 949 if k == "editable": 950 t_args[k] = v == "true" 951 elif k == "optional": 952 t_args[k] = v 953 else: 954 c_args[k] = v 955 return (c_args, t_args)
956
957 -class TemplateUnaryConstraint(UnaryConstraint, TemplateConstraint):
958 - def __init__(self, *a, **d):
959 (c_args, t_args) = self.separate_arg_sets(d) 960 UnaryConstraint.__init__(self, *a, **c_args) 961 TemplateConstraint.__init__(self, **t_args)
962 - def to_string(self):
963 """ 964 Provide a template specific human readable representation of the 965 constraint. This method is called by repr. 966 """ 967 return(UnaryConstraint.to_string(self) 968 + " " + TemplateConstraint.to_string(self))
969
970 -class TemplateBinaryConstraint(BinaryConstraint, TemplateConstraint):
971 - def __init__(self, *a, **d):
972 (c_args, t_args) = self.separate_arg_sets(d) 973 BinaryConstraint.__init__(self, *a, **c_args) 974 TemplateConstraint.__init__(self, **t_args)
975 - def to_string(self):
976 """ 977 Provide a template specific human readable representation of the 978 constraint. This method is called by repr. 979 """ 980 return(BinaryConstraint.to_string(self) 981 + " " + TemplateConstraint.to_string(self))
982
983 -class TemplateListConstraint(ListConstraint, TemplateConstraint):
984 - def __init__(self, *a, **d):
985 (c_args, t_args) = self.separate_arg_sets(d) 986 ListConstraint.__init__(self, *a, **c_args) 987 TemplateConstraint.__init__(self, **t_args)
988 - def to_string(self):
989 """ 990 Provide a template specific human readable representation of the 991 constraint. This method is called by repr. 992 """ 993 return(ListConstraint.to_string(self) 994 + " " + TemplateConstraint.to_string(self))
995
996 -class TemplateLoopConstraint(LoopConstraint, TemplateConstraint):
997 - def __init__(self, *a, **d):
998 (c_args, t_args) = self.separate_arg_sets(d) 999 LoopConstraint.__init__(self, *a, **c_args) 1000 TemplateConstraint.__init__(self, **t_args)
1001 - def to_string(self):
1002 """ 1003 Provide a template specific human readable representation of the 1004 constraint. This method is called by repr. 1005 """ 1006 return(LoopConstraint.to_string(self) 1007 + " " + TemplateConstraint.to_string(self))
1008
1009 -class TemplateTernaryConstraint(TernaryConstraint, TemplateConstraint):
1010 - def __init__(self, *a, **d):
1011 (c_args, t_args) = self.separate_arg_sets(d) 1012 TernaryConstraint.__init__(self, *a, **c_args) 1013 TemplateConstraint.__init__(self, **t_args)
1014 - def to_string(self):
1015 """ 1016 Provide a template specific human readable representation of the 1017 constraint. This method is called by repr. 1018 """ 1019 return(TernaryConstraint.to_string(self) 1020 + " " + TemplateConstraint.to_string(self))
1021
1022 -class TemplateMultiConstraint(MultiConstraint, TemplateConstraint):
1023 - def __init__(self, *a, **d):
1024 (c_args, t_args) = self.separate_arg_sets(d) 1025 MultiConstraint.__init__(self, *a, **c_args) 1026 TemplateConstraint.__init__(self, **t_args)
1027 - def to_string(self):
1028 """ 1029 Provide a template specific human readable representation of the 1030 constraint. This method is called by repr. 1031 """ 1032 return(MultiConstraint.to_string(self) 1033 + " " + TemplateConstraint.to_string(self))
1034
1035 -class TemplateRangeConstraint(RangeConstraint, TemplateConstraint):
1036 - def __init__(self, *a, **d):
1037 (c_args, t_args) = self.separate_arg_sets(d) 1038 RangeConstraint.__init__(self, *a, **c_args) 1039 TemplateConstraint.__init__(self, **t_args)
1040 - def to_string(self):
1041 """ 1042 Provide a template specific human readable representation of the 1043 constraint. This method is called by repr. 1044 """ 1045 return(RangeConstraint.to_string(self) 1046 + " " + TemplateConstraint.to_string(self))
1047
1048 -class TemplateSubClassConstraint(SubClassConstraint, TemplateConstraint):
1049 - def __init__(self, *a, **d):
1050 (c_args, t_args) = self.separate_arg_sets(d) 1051 SubClassConstraint.__init__(self, *a, **c_args) 1052 TemplateConstraint.__init__(self, **t_args)
1053 - def to_string(self):
1054 """ 1055 Provide a template specific human readable representation of the 1056 constraint. This method is called by repr. 1057 """ 1058 return(SubClassConstraint.to_string(self) 1059 + " " + TemplateConstraint.to_string(self))
1060
1061 -class ConstraintFactory(object):
1062 """ 1063 A factory for creating constraints from a set of arguments. 1064 =========================================================== 1065 1066 A constraint factory is responsible for finding an appropriate 1067 constraint class for the given arguments and instantiating the 1068 constraint. 1069 """ 1070 CONSTRAINT_CLASSES = set([ 1071 UnaryConstraint, BinaryConstraint, TernaryConstraint, 1072 MultiConstraint, SubClassConstraint, LoopConstraint, 1073 ListConstraint, RangeConstraint]) 1074
1075 - def __init__(self):
1076 """ 1077 Constructor 1078 =========== 1079 1080 Creates a new ConstraintFactory 1081 """ 1082 self._codes = iter(string.ascii_uppercase)
1083
1084 - def get_next_code(self):
1085 """ 1086 Return the available constraint code. 1087 1088 @return: A single uppercase character 1089 @rtype: str 1090 """ 1091 return self._codes.next()
1092
1093 - def make_constraint(self, *args, **kwargs):
1094 """ 1095 Create a constraint from a set of arguments. 1096 ============================================ 1097 1098 Finds a suitable constraint class, and instantiates it. 1099 1100 @rtype: Constraint 1101 """ 1102 for CC in self.CONSTRAINT_CLASSES: 1103 try: 1104 c = CC(*args, **kwargs) 1105 if hasattr(c, "code") and c.code == "A": 1106 c.code = self.get_next_code() 1107 return c 1108 except TypeError, e: 1109 pass 1110 raise TypeError("No matching constraint class found for " 1111 + str(args) + ", " + str(kwargs))
1112
1113 -class TemplateConstraintFactory(ConstraintFactory):
1114 """ 1115 A factory for creating constraints with template specific characteristics. 1116 ========================================================================== 1117 1118 A constraint factory is responsible for finding an appropriate 1119 constraint class for the given arguments and instantiating the 1120 constraint. TemplateConstraintFactories make constraints with the 1121 extra set of TemplateConstraint qualities. 1122 """ 1123 CONSTRAINT_CLASSES = set([ 1124 TemplateUnaryConstraint, TemplateBinaryConstraint, 1125 TemplateTernaryConstraint, TemplateMultiConstraint, 1126 TemplateSubClassConstraint, TemplateLoopConstraint, 1127 TemplateListConstraint, TemplateRangeConstraint 1128 ])
1129