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 IsaConstraint(MultiConstraint):
784 """ 785 Constraints for testing the class of a value, as a disjoint union 786 ====================================================================== 787 788 These constraints require that the value of the path they constrain 789 should be an instance of one of the classes provided. 790 791 Valid operators: 792 - ISA : The value is an instance of one of the provided classes. 793 794 For example: 795 796 SequenceFeature ISA [Exon, Intron] 797 798 """ 799 OPS = set(['ISA'])
800
801 -class SubClassConstraint(Constraint):
802 """ 803 Constraints on the class of a reference 804 ======================================= 805 806 If an object has a reference X to another object of type A, 807 and type B extends type A, then any object of type B may be 808 the value of the reference X. If you only want to see X's 809 which are B's, this may be achieved with subclass constraints, 810 which allow the type of an object to be limited to one of the 811 subclasses (at any depth) of the class type required 812 by the attribute. 813 814 These constraints do not use operators. Since they cannot be 815 conditional (eg. "A is a B or A is a C" would not be possible 816 in an InterMine query), they do not have codes 817 and cannot be referenced in logic expressions. 818 """
819 - def __init__(self, path, subclass):
820 """ 821 Constructor 822 =========== 823 824 @param path: The path to constrain. This must refer to a class or a reference to a class. 825 @type path: str 826 827 @param subclass: The class to subclass the path to. This must be a simple class name (not a dotted name) 828 @type subclass: str 829 """ 830 if not PATH_PATTERN.match(subclass): 831 raise TypeError 832 self.subclass = subclass 833 super(SubClassConstraint, self).__init__(path)
834 - def to_string(self):
835 """ 836 Provide a human readable representation of the logic. 837 This method is called by repr. 838 """ 839 s = super(SubClassConstraint, self).to_string() 840 return s + ' ISA ' + self.subclass
841 - def to_dict(self):
842 """ 843 Return a dict object which can be used to construct a 844 DOM element with the appropriate attributes. 845 """ 846 d = super(SubClassConstraint, self).to_dict() 847 d.update(type=self.subclass) 848 return d
849
850 851 -class TemplateConstraint(object):
852 """ 853 A mixin to supply the behaviour and state of constraints on templates 854 ===================================================================== 855 856 Constraints on templates can also be designated as "on", "off" or "locked", which refers 857 to whether they are active or not. Inactive constraints are still configured, but behave 858 as if absent for the purpose of results. In addition, template constraints can be 859 editable or not. Only values for editable constraints can be provided when requesting results, 860 and only constraints that can participate in logic expressions can be editable. 861 """ 862 REQUIRED = "locked" 863 OPTIONAL_ON = "on" 864 OPTIONAL_OFF = "off"
865 - def __init__(self, editable=True, optional="locked"):
866 """ 867 Constructor 868 =========== 869 870 @param editable: Whether or not this constraint should accept new values. 871 @type editable: bool 872 873 @param optional: Whether a value for this constraint must be provided when running. 874 @type optional: "locked", "on" or "off" 875 """ 876 self.editable = editable 877 if optional == TemplateConstraint.REQUIRED: 878 self.optional = False 879 self.switched_on = True 880 else: 881 self.optional = True 882 if optional == TemplateConstraint.OPTIONAL_ON: 883 self.switched_on = True 884 elif optional == TemplateConstraint.OPTIONAL_OFF: 885 self.switched_on = False 886 else: 887 raise TypeError("Bad value for optional")
888 889 @property
890 - def required(self):
891 """ 892 True if a value must be provided for this constraint. 893 894 @rtype: bool 895 """ 896 return not self.optional
897 898 @property
899 - def switched_off(self):
900 """ 901 True if this constraint is currently inactive. 902 903 @rtype: bool 904 """ 905 return not self.switched_on
906
907 - def get_switchable_status(self):
908 """ 909 Returns either "locked", "on" or "off". 910 """ 911 if not self.optional: 912 return "locked" 913 else: 914 if self.switched_on: 915 return "on" 916 else: 917 return "off"
918
919 - def switch_on(self):
920 """ 921 Make sure this constraint is active 922 =================================== 923 924 @raise ValueError: if the constraint is not editable and optional 925 """ 926 if self.editable and self.optional: 927 self.switched_on = True 928 else: 929 raise ValueError, "This constraint is not switchable"
930
931 - def switch_off(self):
932 """ 933 Make sure this constraint is inactive 934 ===================================== 935 936 @raise ValueError: if the constraint is not editable and optional 937 """ 938 if self.editable and self.optional: 939 self.switched_on = False 940 else: 941 raise ValueError, "This constraint is not switchable"
942
943 - def to_string(self):
944 """ 945 Provide a template specific human readable representation of the 946 constraint. This method is called by repr. 947 """ 948 if self.editable: 949 editable = "editable" 950 else: 951 editable = "non-editable" 952 return '(' + editable + ", " + self.get_switchable_status() + ')'
953 - def separate_arg_sets(self, args):
954 """ 955 A static function to use when building template constraints. 956 ============================================================ 957 958 dict -> (dict, dict) 959 960 Splits a dictionary of arguments into two separate dictionaries, one with 961 arguments for the main constraint, and one with arguments for the template 962 portion of the behaviour 963 """ 964 c_args = {} 965 t_args = {} 966 for k, v in args.items(): 967 if k == "editable": 968 t_args[k] = v == "true" 969 elif k == "optional": 970 t_args[k] = v 971 else: 972 c_args[k] = v 973 return (c_args, t_args)
974
975 -class TemplateUnaryConstraint(UnaryConstraint, TemplateConstraint):
976 - def __init__(self, *a, **d):
977 (c_args, t_args) = self.separate_arg_sets(d) 978 UnaryConstraint.__init__(self, *a, **c_args) 979 TemplateConstraint.__init__(self, **t_args)
980 - def to_string(self):
981 """ 982 Provide a template specific human readable representation of the 983 constraint. This method is called by repr. 984 """ 985 return(UnaryConstraint.to_string(self) 986 + " " + TemplateConstraint.to_string(self))
987
988 -class TemplateBinaryConstraint(BinaryConstraint, TemplateConstraint):
989 - def __init__(self, *a, **d):
990 (c_args, t_args) = self.separate_arg_sets(d) 991 BinaryConstraint.__init__(self, *a, **c_args) 992 TemplateConstraint.__init__(self, **t_args)
993 - def to_string(self):
994 """ 995 Provide a template specific human readable representation of the 996 constraint. This method is called by repr. 997 """ 998 return(BinaryConstraint.to_string(self) 999 + " " + TemplateConstraint.to_string(self))
1000
1001 -class TemplateListConstraint(ListConstraint, TemplateConstraint):
1002 - def __init__(self, *a, **d):
1003 (c_args, t_args) = self.separate_arg_sets(d) 1004 ListConstraint.__init__(self, *a, **c_args) 1005 TemplateConstraint.__init__(self, **t_args)
1006 - def to_string(self):
1007 """ 1008 Provide a template specific human readable representation of the 1009 constraint. This method is called by repr. 1010 """ 1011 return(ListConstraint.to_string(self) 1012 + " " + TemplateConstraint.to_string(self))
1013
1014 -class TemplateLoopConstraint(LoopConstraint, TemplateConstraint):
1015 - def __init__(self, *a, **d):
1016 (c_args, t_args) = self.separate_arg_sets(d) 1017 LoopConstraint.__init__(self, *a, **c_args) 1018 TemplateConstraint.__init__(self, **t_args)
1019 - def to_string(self):
1020 """ 1021 Provide a template specific human readable representation of the 1022 constraint. This method is called by repr. 1023 """ 1024 return(LoopConstraint.to_string(self) 1025 + " " + TemplateConstraint.to_string(self))
1026
1027 -class TemplateTernaryConstraint(TernaryConstraint, TemplateConstraint):
1028 - def __init__(self, *a, **d):
1029 (c_args, t_args) = self.separate_arg_sets(d) 1030 TernaryConstraint.__init__(self, *a, **c_args) 1031 TemplateConstraint.__init__(self, **t_args)
1032 - def to_string(self):
1033 """ 1034 Provide a template specific human readable representation of the 1035 constraint. This method is called by repr. 1036 """ 1037 return(TernaryConstraint.to_string(self) 1038 + " " + TemplateConstraint.to_string(self))
1039
1040 -class TemplateMultiConstraint(MultiConstraint, TemplateConstraint):
1041 - def __init__(self, *a, **d):
1042 (c_args, t_args) = self.separate_arg_sets(d) 1043 MultiConstraint.__init__(self, *a, **c_args) 1044 TemplateConstraint.__init__(self, **t_args)
1045 - def to_string(self):
1046 """ 1047 Provide a template specific human readable representation of the 1048 constraint. This method is called by repr. 1049 """ 1050 return(MultiConstraint.to_string(self) 1051 + " " + TemplateConstraint.to_string(self))
1052
1053 -class TemplateRangeConstraint(RangeConstraint, TemplateConstraint):
1054 - def __init__(self, *a, **d):
1055 (c_args, t_args) = self.separate_arg_sets(d) 1056 RangeConstraint.__init__(self, *a, **c_args) 1057 TemplateConstraint.__init__(self, **t_args)
1058 - def to_string(self):
1059 """ 1060 Provide a template specific human readable representation of the 1061 constraint. This method is called by repr. 1062 """ 1063 return(RangeConstraint.to_string(self) 1064 + " " + TemplateConstraint.to_string(self))
1065
1066 -class TemplateIsaConstraint(IsaConstraint, TemplateConstraint):
1067 - def __init__(self, *a, **d):
1068 (c_args, t_args) = self.separate_arg_sets(d) 1069 IsaConstraint.__init__(self, *a, **c_args) 1070 TemplateConstraint.__init__(self, **t_args)
1071 - def to_string(self):
1072 """ 1073 Provide a template specific human readable representation of the 1074 constraint. This method is called by repr. 1075 """ 1076 return(IsaConstraint.to_string(self) 1077 + " " + TemplateConstraint.to_string(self))
1078
1079 -class TemplateSubClassConstraint(SubClassConstraint, TemplateConstraint):
1080 - def __init__(self, *a, **d):
1081 (c_args, t_args) = self.separate_arg_sets(d) 1082 SubClassConstraint.__init__(self, *a, **c_args) 1083 TemplateConstraint.__init__(self, **t_args)
1084 - def to_string(self):
1085 """ 1086 Provide a template specific human readable representation of the 1087 constraint. This method is called by repr. 1088 """ 1089 return(SubClassConstraint.to_string(self) 1090 + " " + TemplateConstraint.to_string(self))
1091
1092 -class ConstraintFactory(object):
1093 """ 1094 A factory for creating constraints from a set of arguments. 1095 =========================================================== 1096 1097 A constraint factory is responsible for finding an appropriate 1098 constraint class for the given arguments and instantiating the 1099 constraint. 1100 """ 1101 CONSTRAINT_CLASSES = set([ 1102 UnaryConstraint, BinaryConstraint, TernaryConstraint, 1103 MultiConstraint, SubClassConstraint, LoopConstraint, 1104 ListConstraint, RangeConstraint, IsaConstraint]) 1105
1106 - def __init__(self):
1107 """ 1108 Constructor 1109 =========== 1110 1111 Creates a new ConstraintFactory 1112 """ 1113 self._codes = iter(string.ascii_uppercase) 1114 self.reference_ops = TernaryConstraint.OPS | RangeConstraint.OPS | ListConstraint.OPS | IsaConstraint.OPS
1115
1116 - def get_next_code(self):
1117 """ 1118 Return the available constraint code. 1119 1120 @return: A single uppercase character 1121 @rtype: str 1122 """ 1123 return self._codes.next()
1124
1125 - def make_constraint(self, *args, **kwargs):
1126 """ 1127 Create a constraint from a set of arguments. 1128 ============================================ 1129 1130 Finds a suitable constraint class, and instantiates it. 1131 1132 @rtype: Constraint 1133 """ 1134 for CC in self.CONSTRAINT_CLASSES: 1135 try: 1136 c = CC(*args, **kwargs) 1137 if hasattr(c, "code") and c.code == "A": 1138 c.code = self.get_next_code() 1139 return c 1140 except TypeError, e: 1141 pass 1142 raise TypeError("No matching constraint class found for " 1143 + str(args) + ", " + str(kwargs))
1144
1145 -class TemplateConstraintFactory(ConstraintFactory):
1146 """ 1147 A factory for creating constraints with template specific characteristics. 1148 ========================================================================== 1149 1150 A constraint factory is responsible for finding an appropriate 1151 constraint class for the given arguments and instantiating the 1152 constraint. TemplateConstraintFactories make constraints with the 1153 extra set of TemplateConstraint qualities. 1154 """ 1155 CONSTRAINT_CLASSES = set([ 1156 TemplateUnaryConstraint, TemplateBinaryConstraint, 1157 TemplateTernaryConstraint, TemplateMultiConstraint, 1158 TemplateSubClassConstraint, TemplateLoopConstraint, 1159 TemplateListConstraint, TemplateRangeConstraint, TemplateIsaConstraint 1160 ])
1161