1"""Support Eiffel-style preconditions and postconditions.
2
3For example,
4
5class C:
6    def m1(self, arg):
7        require arg > 0
8        return whatever
9        ensure Result > arg
10
11can be written (clumsily, I agree) as:
12
13class C(Eiffel):
14    def m1(self, arg):
15        return whatever
16    def m1_pre(self, arg):
17        assert arg > 0
18    def m1_post(self, Result, arg):
19        assert Result > arg
20
21Pre- and post-conditions for a method, being implemented as methods
22themselves, are inherited independently from the method.  This gives
23much of the same effect of Eiffel, where pre- and post-conditions are
24inherited when a method is overridden by a derived class.  However,
25when a derived class in Python needs to extend a pre- or
26post-condition, it must manually merge the base class' pre- or
27post-condition with that defined in the derived class', for example:
28
29class D(C):
30    def m1(self, arg):
31        return arg**2
32    def m1_post(self, Result, arg):
33        C.m1_post(self, Result, arg)
34        assert Result < 100
35
36This gives derived classes more freedom but also more responsibility
37than in Eiffel, where the compiler automatically takes care of this.
38
39In Eiffel, pre-conditions combine using contravariance, meaning a
40derived class can only make a pre-condition weaker; in Python, this is
41up to the derived class.  For example, a derived class that takes away
42the requirement that arg > 0 could write:
43
44    def m1_pre(self, arg):
45        pass
46
47but one could equally write a derived class that makes a stronger
48requirement:
49
50    def m1_pre(self, arg):
51        require arg > 50
52
53It would be easy to modify the classes shown here so that pre- and
54post-conditions can be disabled (separately, on a per-class basis).
55
56A different design would have the pre- or post-condition testing
57functions return true for success and false for failure.  This would
58make it possible to implement automatic combination of inherited
59and new pre-/post-conditions.  All this is left as an exercise to the
60reader.
61
62"""
63
64from Meta import MetaClass, MetaHelper, MetaMethodWrapper
65
66class EiffelMethodWrapper(MetaMethodWrapper):
67
68    def __init__(self, func, inst):
69        MetaMethodWrapper.__init__(self, func, inst)
70        # Note that the following causes recursive wrappers around
71        # the pre-/post-condition testing methods.  These are harmless
72        # but inefficient; to avoid them, the lookup must be done
73        # using the class.
74        try:
75            self.pre = getattr(inst, self.__name__ + "_pre")
76        except AttributeError:
77            self.pre = None
78        try:
79            self.post = getattr(inst, self.__name__ + "_post")
80        except AttributeError:
81            self.post = None
82
83    def __call__(self, *args, **kw):
84        if self.pre:
85            apply(self.pre, args, kw)
86        Result = apply(self.func, (self.inst,) + args, kw)
87        if self.post:
88            apply(self.post, (Result,) + args, kw)
89        return Result
90
91class EiffelHelper(MetaHelper):
92    __methodwrapper__ = EiffelMethodWrapper
93
94class EiffelMetaClass(MetaClass):
95    __helper__ = EiffelHelper
96
97Eiffel = EiffelMetaClass('Eiffel', (), {})
98
99
100def _test():
101    class C(Eiffel):
102        def m1(self, arg):
103            return arg+1
104        def m1_pre(self, arg):
105            assert arg > 0, "precondition for m1 failed"
106        def m1_post(self, Result, arg):
107            assert Result > arg
108    x = C()
109    x.m1(12)
110##    x.m1(-1)
111
112if __name__ == '__main__':
113    _test()
114