1 import re
2 import string
3 from intermine.pathfeatures import PathFeature, PATH_PATTERN
4 from intermine.util import ReadableException
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
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
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
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
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
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
112 """
113 Provide a sensible representation of a node
114 """
115 return '<' + self.__class__.__name__ + ': ' + str(self) + '>'
116
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
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
142 """
143 An error representing problems in parsing constraint logic.
144 """
145 pass
146
148 """
149 An error representing the fact that an the logic string to be parsed was empty
150 """
151 pass
152
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
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
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
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
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
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
434
436 """
437 Stringify to the code they are refered to by.
438 """
439 return self.code
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
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
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
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
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)])
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
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
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)])
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
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
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])
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
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
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])
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
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
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)])
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
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
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 """
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)
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
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
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
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
882 """
883 True if this constraint is currently inactive.
884
885 @rtype: bool
886 """
887 return not self.switched_on
888
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
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
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
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() + ')'
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
969
982
995
1008
1021
1034
1047
1060
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
1076 """
1077 Constructor
1078 ===========
1079
1080 Creates a new ConstraintFactory
1081 """
1082 self._codes = iter(string.ascii_uppercase)
1083
1085 """
1086 Return the available constraint code.
1087
1088 @return: A single uppercase character
1089 @rtype: str
1090 """
1091 return self._codes.next()
1092
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
1129