-
Notifications
You must be signed in to change notification settings - Fork 1
/
Criteria.txt
executable file
·1238 lines (886 loc) · 38.2 KB
/
Criteria.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
================
Logical Criteria
================
In order to process arbitrary expression-based rules, PEAK-Rules needs to
"understand" the way that conditions logically relate to each other. This
document describes the design (and tests the implementation) of its logical
criteria management. You do not need to read this unless you are extending or
interfacing with this subsystem directly, or just want to understand how this
stuff actually works!
The most important ideas here are implication, intersection, and disjunctive
normal form. But don't panic if you don't know what those terms mean! They're
really quite simple.
Implication means that if one thing is true, then so is another. A implies B
if B is always true whenever A is true. It doesn't matter what B is when A is
not true, however. It could be true or false, we don't care. Implication is
important for prioritizing which rules are "more specific" than others.
Intersection just means that both things have to be true for a condition to
be true - it's like the "and" of two conditions. But rather than performing
an actual "and", we're creating a *new condition* that will only be true when
the two original conditions would be true.
And finally, disjunctive normal form (DNF) means "an OR of ANDs". For example,
this expression is in DNF::
(A and C) or (B and C) or (A and D) or (B and D)
But this equivalent expression is **not** in DNF::
(A or B) and (C or D)
The criteria used to define generic function methods are likely to look more
like this, than they are to be in disjunctive normal form. Therefore, we must
convert them in order to implement the Chambers & Chen dispatch algorithm
correctly (see `Indexing.txt`_).
.. _Indexing.txt: http://peak.telecommunity.com/DevCenter/PEAK-Rules/Indexing
We do this using the ``DisjunctionSet`` and ``OrElse`` classes to represent
overall expressions (sets or sequences of "ors"), and the ``Signature`` and
``Conjunction`` classes to represent sequences or sets of "and"-ed conditions.
Within a ``Signature``, the things that are "and"-ed together are a sequence
of ``Test`` instances. A ``Test`` pairs a "dispatch expression" with a
"criterion". For example, this expression::
isinstance(x, Y)
would be represented internally as a ``Test`` instance like this::
Test(IsInstance(Local('x')), Class(Y))
``Conjunction`` instances, on the other hand, are used to "and" together
criteria that apply to the same dispatch expression. For example, this
expression::
isinstance(x, Y) and isinstance(x, Z)
would be represented internally like this::
Test(IsInstance(Local('x')), Conjunction([Class(Y), Class(Z)]))
The rest of this document describes how predicates, signatures, tests, dispatch
expressions, and criteria work together to create expressions in disjunctive
normal form, and whose implication of other expressions can be determined.
The basic logical functions we will use are ``implies()``, ``intersect()``,
``disjuncts()``, and ``negate()``, all of which are defined in
``peak.rules.core``::
>>> from peak.rules.core import implies, intersect, disjuncts, negate
----------------------------------------
Boolean Conditions and Logical Operators
----------------------------------------
The most fundamental conditions are simply ``True`` and ``False``. ``True``
represents a rule that *always* applies, while ``False`` represents a rule that
*never* applies. Therefore, the result of intersecting ``True`` and any other
object, always returns that object, while intersecting ``False`` with any other
object returns ``False``::
>>> intersect(False, False)
False
>>> intersect(False, True)
False
>>> intersect(True, False)
False
>>> intersect(True, True)
True
>>> intersect(object(), True)
<object object at ...>
>>> intersect(True, object())
<object object at ...>
>>> intersect(object(), False)
False
>>> intersect(False, object())
False
Because ``True`` means "condition that always applies", *everything* implies
``True``, but ``True`` only implies itself::
>>> implies(object(), True)
True
>>> implies(True, object())
False
>>> implies(True, True)
True
On the other hand, because ``False`` means "condition that never applies",
``False`` implies *everything*. (Because if you start from a false premise,
you can arrive at any conclusion!)::
>>> implies(False, True)
True
>>> implies(False, object())
True
However, no condition other than ``False`` can ever imply ``False``, because
all other conditions can *sometimes* apply::
>>> implies(object(), False)
False
>>> implies(True, False)
False
>>> implies(False, False)
True
Notice, by the way, a few important differences between ``implies()`` and
``intersect()``. ``implies()`` *always* returns a boolean value, ``True`` or
``False``, because it's an immediate answer to the question of, "does the
second condition always apply if the first condition applies?"
``intersect()``, on the other hand, returns a *condition* that will always be
true when the original conditions apply. So, if it returns a boolean value,
that's just an indication that the intersection of the two input conditions
would always apply or never apply. Also, ``intersect()`` is logically
symmetrical, in that it doesn't matter what order the arguments are in, whereas
the order is critically important for ``implies()``.
However, ``intersect()`` methods must be order *preserving*, because the order
in which logical "and" operations occur is important. Consider, for example,
the condition ``"y!=0 and z>x/y"``, in which it would be a bad thing to skip
the zero check before the division!
So, as we will see later on, when working with more complex conditions,
``intersect()`` methods must ensure that the subparts of the output condition
are in the same relative order as they were in the input.
(Also, note that in general, when you intersect two conditions, if one condition
implies the other, the result of the intersection is the implying condition.
This general rule greatly simplifies the implementation of most intersect
operations, since as long as there is an implication relationship defined
between conditions, many common cases of intersection can be handled
automatically.)
In contrast to both ``implies()`` and ``intersects()``, the ``disjuncts()``
function takes only a single argument, and returns a list of the "disjuncts"
(or-ed-together conditions) of its argument. More precisely, it returns a list
of conditions that each imply the original condition. That is, if any of the
disjuncts were true, then the original condition would also be true.
Thus, the ``disjuncts()`` of an arbitrary object will normally be a list
containing just that object::
>>> disjuncts(object())
[<object object at ...>]
>>> disjuncts(True)
[True]
But ``False`` is a special case; ``False`` has *no* disjuncts, since no other
condition can ever imply ``False``::
>>> disjuncts(False)
[]
As a result, "or"-ing ``False`` with other conditions will simply remove the
``False`` from the resulting predicate, and conditions that can never be true
are not used for indexing or dispatching.
Another special case is tuples containing nested tuples::
>>> disjuncts( (float, (int, str)) )
[(<type 'float'>, <type 'int'>),
(<type 'float'>, <type 'str'>)]
>>> disjuncts( ((int, str), object) )
[(<type 'int'>, <type 'object'>),
(<type 'str'>, <type 'object'>)]
>>> disjuncts( (object, (int, str), float) )
[(<type 'object'>, <type 'int'>, <type 'float'>),
(<type 'object'>, <type 'str'>, <type 'float'>)]
>>> disjuncts( ((int, str), (int, str)) )
[(<type 'int'>, <type 'int'>),
(<type 'str'>, <type 'int'>),
(<type 'int'>, <type 'str'>),
(<type 'str'>, <type 'str'>)]
This lets you avoid writing lots of decorators for the cases where you want
more than one type (or ``istype()`` instance) to match in a given argument
position. (As you can see, it's equivalent to specifying all the individual
combinations of specified types.)
Finally, the ``negate()`` function inverts the truth of a condition, e.g.::
>>> negate(True)
False
>>> negate(False)
True
Of course, it also applies to criteria other than pure boolean values, as we'll
see in the upcoming sections.
-------------------
"Criterion" Objects
-------------------
A criterion object describes a set of possible values for a dispatch
expression. There are several criterion types supplied with PEAK-Rules, but you
can also add your own, as long as they can be tested for implication with
``implies()``, and intersected with ``intersect()``. (And if they represent an
"or" of sub-criteria, they should be able to provide their list of
``disjuncts()``. They'll also need to be indexable, but more on that later in
other documents!)
"And"-ed Criteria
=================
Sometimes, more than one criterion is applied to the same dispatch expression.
For example in the expression ``x is not y and x is not z``, two criteria are
being applied to the identity of ``x``. To represent this, we need a way to
represent a set of "and-ed" criteria. ``peak.rules.criteria`` provides a base
class for this, called ``Conjunction``::
>>> from peak.rules.criteria import Conjunction
This class is a subclass of ``frozenset``, but has a few additional features.
First, a ``Conjunction`` never contains redundant (implied) items.
For example, the conjunction of the classes ``object`` and ``int`` is ``int``,
because ``int`` already implies ``object``::
>>> Conjunction([int, object])
<type 'int'>
>>> Conjunction([object, int])
<type 'int'>
Notice also that instead of getting back a set with one member, we got back the
item that would have been in the set. This helps to simplify the expression
structure. As a further simplification, creating an empty conjunction returns
``True``, because "no conditions required" is the same as "always true"::
>>> Conjunction([])
True
A conjunction implies a condition, if any condition in the conjunction
implies the other condition::
>>> implies(Conjunction([str, int]), str)
True
>>> implies(Conjunction([str, int]), int)
True
>>> implies(Conjunction([str, int]), object)
True
>>> implies(Conjunction([str, int]), float)
False
A condition implies a conjunction, however, only if the condition implies
every part of the conjunction::
>>> class a: pass
>>> class b: pass
>>> class c(a,b): pass
>>> class d(a, int): pass
>>> implies(c, Conjunction([a, b]))
True
>>> implies(a, Conjunction([a, b]))
False
>>> implies(Conjunction([c,d]), Conjunction([a, int]))
True
>>> implies(Conjunction([c, int]), Conjunction([a, int]))
True
>>> implies(Conjunction([a, int]), Conjunction([c, int]))
False
(By the way, on a more sophisticated level of reasoning, you could say that
``Conjunction([str, int])`` should have equalled ``False`` above, since
there's no way for an object to be both an ``int`` and a ``str`` at the same
time. But that would be an excursion into semantics and outside the bounds of
what PEAK-Rules can "reason" about using only logical implication as defined by
the ``implies()`` generic function.)
``Conjunction`` objects can be intersected with one another, or with
additional conditions, and the result is another ``Conjunction`` of the
same type as the leftmost set. So, if we use subclasses of our own, the result
of intersecting them will be a conjunction of the correct subclass::
>>> class MySet(Conjunction): pass
>>> type(intersect(MySet([int, str]), float))
<class 'MySet'>
>>> intersect(MySet([int, str]), float) == MySet([int, str, float])
True
>>> intersect(float, MySet([int, str])) == MySet([float, int, str])
True
>>> intersect(MySet([d, c]), MySet([int, str])) == MySet([d,c,str])
True
If you want to ensure that all items in a set are of appropriate type or value,
you can override ``__init__`` to do the checking, and raise an appropriate
error. PEAK-Rules does this for its specialized conjunction classes, but uses
``if __debug__:`` and ``assert`` statements to avoid the extra overhead when
run with ``python -O``. You may wish to do the same for your subclasses.
"Or"-ed Criteria
================
The ``DisjunctionSet`` and ``OrElse`` classes are used to represent sets and
sequences of "or"-ed criteria::
>>> from peak.rules.criteria import DisjunctionSet, OrElse
Both types automatically exclude redundant (i.e. more-specific) criteria, and
can never contain less than 2 entries. For example, "or"-ing ``object`` and
``int`` always returns ``object``, because ``object`` is implied by ``int``::
>>> DisjunctionSet([int, object])
<type 'object'>
>>> DisjunctionSet([object, int])
<type 'object'>
>>> OrElse([int, object])
<type 'object'>
>>> OrElse([object, int])
<type 'object'>
Notice that instead of getting back a set or sequence with one member, we got
back the item that would have been in the set. This helps to simplify the
expression structure. As a further simplification, creating an empty
disjunction returns ``False``, because "no conditions are sufficient" is the
same as "always false"::
>>> DisjunctionSet([])
False
>>> OrElse([])
False
In addition to eliminating redundancy, disjunction *sets* also flatten any
nested disjunctions::
>>> DisjunctionSet([DisjunctionSet([1, 2]), DisjunctionSet([3, 4])])
DisjunctionSet([1, 2, 3, 4])
This is because it uses the ``disjuncts()`` generic function to determine
whether any of the items it was given are "or"-ed conditions of some kind. And
the ``disjuncts()`` of a ``DisjunctionSet`` are a list of its contents::
>>> disjuncts(DisjunctionSet([1, 2, 3, 4]))
[1, 2, 3, 4]
But ``OrElse`` sequences do not do this flattening, in order to avoid imposing
an arbitrary sequence on their contents::
>>> OrElse([DisjunctionSet([1, 2]), DisjunctionSet([3, 4])])
OrElse([DisjunctionSet([1, 2]), DisjunctionSet([3, 4])])
(The ``disjuncts()`` of an ``OrElse`` are much more complicated, as the
disjuncts of a Python expression like ``"a or b or c"`` reduce to ``"a"``,
``"(not a) and b"``, and ``"(not a and not b) and c"``! We'll talk more about
this later, in the section on `Predicates`_ below.)
A disjunction only implies a condition if *all* conditions in the disjunction
imply the other condition::
>>> implies(DisjunctionSet([str, int]), str)
False
>>> implies(DisjunctionSet([str, int]), int)
False
>>> implies(DisjunctionSet([str, int]), float)
False
>>> implies(DisjunctionSet([str, int]), object)
True
>>> implies(OrElse([str, int]), str)
False
>>> implies(OrElse([str, int]), int)
False
>>> implies(OrElse([str, int]), float)
False
>>> implies(OrElse([str, int]), object)
True
A condition implies a disjunction, however, if the condition implies any part
of the disjunction::
>>> class a: pass
>>> class b: pass
>>> class c(a,b): pass
>>> class d(a, int): pass
>>> implies(c, DisjunctionSet([a, b]))
True
>>> implies(a, DisjunctionSet([a, b]))
True
>>> implies(a, DisjunctionSet([int, str]))
False
>>> implies(DisjunctionSet([c,d]), DisjunctionSet([a, int]))
True
>>> implies(DisjunctionSet([c,int]), DisjunctionSet([a, int]))
True
>>> implies(DisjunctionSet([c, int]), True)
True
>>> implies(False, DisjunctionSet([c, int]))
True
>>> implies(c, OrElse([a, b]))
True
>>> implies(a, OrElse([a, b]))
True
>>> implies(a, OrElse([int, str]))
False
>>> implies(OrElse([c,d]), OrElse([a, int]))
True
>>> implies(OrElse([c,int]), OrElse([a, int]))
True
>>> implies(OrElse([c, int]), True)
True
>>> implies(False, OrElse([c, int]))
True
The intersection of a disjunction and any other object is a disjunction
containing the intersection of that object with the original disjunctions'
contents. In other words::
>>> int_or_str = DisjunctionSet([int, str])
>>> long_or_float = DisjunctionSet([long, float])
>>> intersect(int_or_str, float) == DisjunctionSet([
... Conjunction([int, float]), Conjunction([str, float])
... ])
True
>>> intersect(long, int_or_str) == DisjunctionSet([
... Conjunction([long, int]), Conjunction([long, str])
... ])
True
>>> intersect(int_or_str, long_or_float) == DisjunctionSet([
... Conjunction([int,long]), Conjunction([int, float]),
... Conjunction([str,long]), Conjunction([str, float]),
... ])
True
>>> intersect(int_or_str, Conjunction([long, float])) == \
... DisjunctionSet(
... [Conjunction([int, long, float]),
... Conjunction([str, long, float])]
... )
True
>>> intersect(Conjunction([int, str]), long_or_float) == \
... DisjunctionSet(
... [Conjunction([int, str, long]), Conjunction([int, str, float])]
... )
True
As you can see, this is the heart of the process that allows expressions like
``(A or B) and (C or D)`` to be transformed into their disjunctive normal
form (i.e. ``(A and C) or (A and D) or (B and C) or (B and D)``).
(In other words, by using ``Disjunction()`` as an "or" operator and
``intersect()`` as the "and" operator, we always end up with a DNF result!)
Object Identity
===============
The ``IsObject`` criterion type represents the set of objects which either
are -- or are *not* -- one specific object instance. ``IsObject(x)`` (or
``IsObject(x, True)``) represents the set of objects ``y`` for which the
``y is x`` condition would be true. Conversely, ``IsObject(x, False)``
represents the set of objects ``y`` for whom ``y is not x``::
>>> from peak.rules.criteria import IsObject, Conjunction
>>> o = object()
>>> is_o = IsObject(o)
>>> is_not_o = IsObject(o, False)
>>> is_o
IsObject(<object object at ...>, True)
>>> is_not_o
IsObject(<object object at ...>, False)
>>> is_not_o == negate(is_o)
True
>>> is_o == negate(is_not_o)
True
The intersection of two different ``is`` identities is ``False``, since an
object cannot be both itself and another object::
>>> intersect(is_o, IsObject("foo"))
False
>>> implies(is_o, IsObject("foo"))
False
Similarly, an object can't be both itself, and not itself::
>>> intersect(is_o, is_not_o)
False
>>> intersect(is_not_o, is_o)
False
>>> implies(is_o, is_not_o)
False
But it *can* be itself and itself::
>>> intersect(is_o, is_o) == is_o
True
>>> implies(is_o, is_o)
True
Or not itself and not itself::
>>> intersect(is_not_o, is_not_o) == is_not_o
True
>>> implies(is_not_o, is_not_o)
True
And an object can be itself, while not being something else::
>>> intersect(is_o, IsObject("foo", False)) == is_o
True
>>> intersect(IsObject("foo", False), is_o) == is_o
True
>>> implies(is_o, IsObject("foo", False))
True
But just because an object is not something, doesn't mean it's something else::
>>> implies(is_not_o, IsObject("foo"))
False
And the intersection of multiple ``is not`` conditions produces a
``Conjunction``::
>>> not_foo = IsObject("foo", False)
>>> not_bar = IsObject("bar", False)
>>> not_foobar = intersect(not_foo, not_bar)
>>> not_foobar
Conjunction([IsObject('foo', False), IsObject('bar', False)])
Which of course then implies each of the individual "not" conditions::
>>> implies(not_foobar, not_bar)
True
>>> implies(not_foobar, not_foo)
True
But not their opposites::
>>> implies(not_foobar, IsObject("bar"))
False
Oh, and an ``is`` condition implies any ``Conjunction`` that don't contain its
opposite::
>>> implies(is_o, not_foobar)
True
But not the other way around::
>>> implies(not_foobar, is_o)
False
Finally, negating a ``Conjunction`` of is-nots returns a disjunction of true
``IsObject`` tests, and vice versa::
>>> negate(not_foobar)
DisjunctionSet([IsObject('foo', True), IsObject('bar', True)])
>>> negate(DisjunctionSet([IsObject('foo'), IsObject('bar')]))
Conjunction([IsObject('foo', False), IsObject('bar', False)])
Values and Ranges
=================
``Value`` objects are used to represent ``==`` and ``!=`` comparisons.
``Value(x)`` represents ``==x`` and ``Value(x, False)`` represents ``!=x``.
A ``Value`` implies another ``Value`` if the two are identical::
>>> from peak.rules.criteria import Value, Range, Min, Max
>>> implies(Value(27), Value(42))
False
>>> implies(Value(27, False), Value(42))
False
>>> implies(Value(27), Value(27))
True
>>> implies(Value(99), Value(99, False))
False
>>> implies(Value(99, False), Value(99, False))
True
Or, if they have different target values, but the first is an ``==``
comparison, and the second is a ``!=`` comparison::
>>> implies(Value(27), Value(99, False))
True
>>> intersect(Value(27), Value(99, False))
Value(27, True)
The negation of a ``Value`` is of course another ``Value`` of the same
target but the reverse operator::
>>> negate(Value(27))
Value(27, False)
>>> negate(Value(99, False))
Value(99, True)
The intersection of two different ``==`` values, or a ``!=`` and ``==`` of the
same value, is ``False`` (i.e., no possible match::
>>> intersect(Value(27), Value(42))
False
>>> intersect(Value(27), Value(27, False))
False
But the intersection of two different ``!=`` values produces a disjunction of
three ``Range()`` objects::
>>> one_two = intersect(Value(1, False), Value(2, False))
>>> one_two == DisjunctionSet([
... Range((Min, -1), (1, -1)),
... Range((1, 1), (2, -1)),
... Range((2, 1), (Max, 1))
... ])
True
>>> intersect(one_two, Value(3,False)) == DisjunctionSet([
... Range((Min, -1), (1, -1)),
... Range((1, 1), (2, -1)),
... Range((2, 1), (3, -1)),
... Range((3, 1), (Max, 1))
... ])
True
The ``Range()`` criterion type represents an inequality such as ``lo < x < hi``
or ``x >= lo``. The lows and highs given have to be a 2-tuple, consisting of
a value and a "direction". The direction is an integer (either -1 or 1) that
indicates whether the edge is on the low or high side of the target value.
Thus, a tuple ``(27, -1)`` means "the low edge of 27", while ``(99, 1)``
means "the high edge of 99". In this way, any simple inequality or range
can be represented by a pair of edges.
Thus, the intersection of two different ``!=`` values produces a disjunction of
three ``Range()`` objects, representing the intervals that "surround" the
original ``!=`` values::
>>> from peak.rules.criteria import Range
>>> intersect(Value(27, False), Value(42, False)) == DisjunctionSet([
... Range(hi=(27, -1)), # below Min ... below 27
... Range((27,1), (42,-1)), # above 27 ... below 42
... Range(lo=(42, 1)), # above 42 ... above Max
... ])
True
Notice that if we omit the ``hi`` or ``lo``, end of the range, it's replaced
with "below ``Min``" or "above ``Max``", as appropriate. (The ``Min`` and
``Max`` values are special objects that compare below or above any other
object.)
When creating range and value objects, it can be useful to use the
``Inequality`` constructor, which takes a comparison operator and a value::
>>> from peak.rules.criteria import Inequality
>>> Inequality('>=', 27) # >=27 : below 27 ... above Max
Range((27, -1), (Max, 1))
>>> negate(Inequality('<', 27))
Range((27, -1), (Max, 1))
>>> Inequality('>', 27) # > 27 : above 27 ... above Max
Range((27, 1), (Max, 1))
>>> Inequality('<', 99) # < 99 : below Min ... below 99
Range((Min, -1), (99, -1))
>>> Inequality('<=',99) # <=99 : below Min ... above 99
Range((Min, -1), (99, 1))
>>> negate(Inequality('>', 99))
Range((Min, -1), (99, 1))
>>> Inequality('==', 66)
Value(66, True)
>>> Inequality('!=', 77)
Value(77, False)
Intersecting two ranges (or a range and an ``==`` value) produces a smaller
range or value, or ``False`` if there is no overlap::
>>> intersect(Inequality('<', 27), Inequality(">",19))
Range((19, 1), (27, -1))
>>> intersect(Inequality('>=', 27), Inequality("<=",19))
False
>>> intersect(Value(27), Inequality('>=', 27))
Value(27, True)
>>> intersect(Inequality('<=', 27), Value(27))
Value(27, True)
>>> intersect(Value(27), Inequality('<',27))
False
>>> intersect(Inequality('>',27), Value(27))
False
Last, but not least, a range (or value) implies another range or value if it
lies entirely within it::
>>> implies(Range((42,-1), (42,1)), Value(42))
True
>>> implies(Range((27,-1), (42,1)), Range((15,1),(99,-1)))
True
>>> implies(Range((27,-1), (42,1)), Value(99, False))
True
But not if it overlaps or lies outside of it::
>>> implies(Range((15,-1),(42,1)), Range((15,1),(99,-1)))
False
>>> implies(Range((27,-1), (42,1)), Value(99))
False
Class/Type Tests
================
``Class`` objects represent ``issubclass()`` or ``isinstance()`` sets.
``Class(x)`` is a instance/subclass match, while ``Class(x, False)`` is a
non-match. Implication, negation, and intersection are defined accordingly::
>>> from peak.rules.criteria import Class
>>> implies(Class(int), Class(object))
True
>>> implies(Class(object, False), Class(int, False))
True
>>> negate(Class(int))
Class(<type 'int'>, False)
>>> negate(Class(object, False))
Class(<type 'object'>, True)
>>> implies(Class(int), Class(str))
False
>>> implies(Class(object), Class(int, False))
False
>>> implies(Class(object), Class(int))
False
>>> implies(Class(int), Class(int))
True
>>> intersect(Class(int), Class(object))
Class(<type 'int'>, True)
>>> intersect(Class(object), Class(int))
Class(<type 'int'>, True)
The intersection of two or more unrelated ``Class`` criteria is represented by
a ``Conjunction``::
>>> from peak.rules.criteria import Conjunction
>>> intersect(Class(int, False), Class(str, False)) == Conjunction(
... [Class(int, False), Class(str, False)]
... )
True
Exact-Type and Type-Exclusion Tests
===================================
Exact type tests are expressed using ``istype(x)``, and type exclusion tests
are represented as ``istype(x, False)``::
>>> from peak.rules import istype
>>> negate(istype(int))
istype(<type 'int'>, False)
>>> negate(istype(object, False))
istype(<type 'object'>, True)
One ``istype()`` test implies another only if they're equal::
>>> implies(istype(int), istype(int))
True
>>> implies(istype(int, False), istype(int, False))
True
>>> implies(istype(int, False), istype(int))
False
Or if the first is an exact match, and the second is an exclusion test for
a different type::
>>> implies(istype(int), istype(str, False))
True
Thus, the intersection of two ``istype()`` tests will be either one of the
input tests, or ``False`` (meaning no overlap)::
>>> intersect(istype(int), istype(int))
istype(<type 'int'>, True)
>>> intersect(istype(int), istype(str, False))
istype(<type 'int'>, True)
>>> intersect(istype(int, False), istype(int, False))
istype(<type 'int'>, False)
>>> intersect(istype(int), istype(str))
False
Unless both are exclusion tests on different types, in which case their
intersection is a ``Conjunction`` of the two::
>>> intersect(istype(str, False), istype(int, False)) == Conjunction([
... istype(int, False), istype(str, False)
... ])
True
An ``istype(x)`` implies ``Class(y)`` only if x is y or a subtype thereof::
>>> implies(istype(int), Class(str))
False
>>> implies(istype(int), Class(object))
True
And it implies ``Class(y, False)`` only if x is *not* y or a subtype thereof::
>>> implies(istype(int), Class(str, False))
True
>>> implies(istype(int), Class(object, False))
False
But ``istype(x, False)`` implies nothing about any ``Class`` test, since it
refers to exactly one type, while the ``Class`` may refer to infinitely many
types::
>>> implies(istype(int, False), Class(int, False))
False
>>> implies(istype(int, False), Class(object))
False
Meanwhile, ``Class(x)`` tests can only imply ``istype(y, False)``, where y
is a *superclass* of x::
>>> implies(Class(int), istype(int))
False
>>> implies(Class(int), istype(object))
False
>>> implies(Class(int), istype(object, False))
True
And ``Class(x, False)`` cannot imply anything about any ``istype()`` test,
whether true or false::
>>> implies(Class(int, False), istype(int))
False
>>> implies(Class(int, False), istype(int, False))
False
When ``Class()`` is intersected with an exact type test, the result is either
the exact type test, or ``False``::
>>> intersect(Class(int), istype(int))
istype(<type 'int'>, True)
>>> intersect(istype(int), Class(int))
istype(<type 'int'>, True)
>>> intersect(Class(int), istype(object))
False
>>> intersect(istype(object), Class(int))
False
>>> intersect(Class(int, False), istype(object))
istype(<type 'object'>, True)
>>> intersect(istype(object), Class(int, False))
istype(<type 'object'>, True)
But when it's intersected with a type exclusion test, the result is a
``Conjunction``::
>>> intersect(istype(int, False), Class(str)) == Conjunction([
... istype(int, False), Class(str, True)
... ])
True
>>> s = intersect(Class(str), istype(int, False))
>>> s == Conjunction([istype(int, False), Class(str, True)])
True
>>> intersect(s, istype(int))
False
>>> intersect(s, istype(int, False)) == s
True
>>> intersect(s, istype(str))
istype(<type 'str'>, True)
--------------------
Tests and Signatures
--------------------
``Test`` Objects
================
A ``Test`` is the combination of a "dispatch expression" and a criterion to
be applied to it::
>>> from peak.rules.criteria import Test
>>> x_isa_int = Test("x", Class(int))
(Note that although these examples use strings, actual dispatch expressions
will be AST-like structures.)
Creating a test with disjunct criteria actually returns a set of tests::
>>> Test("x", DisjunctionSet([int, str])) == \
... DisjunctionSet([Test('x', int), Test('x', str)])