• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3"""
4Support Eiffel-style preconditions and postconditions for functions.
5
6An example for Python metaclasses.
7"""
8
9import unittest
10from types import FunctionType as function
11
12class EiffelBaseMetaClass(type):
13
14    def __new__(meta, name, bases, dict):
15        meta.convert_methods(dict)
16        return super(EiffelBaseMetaClass, meta).__new__(
17            meta, name, bases, dict)
18
19    @classmethod
20    def convert_methods(cls, dict):
21        """Replace functions in dict with EiffelMethod wrappers.
22
23        The dict is modified in place.
24
25        If a method ends in _pre or _post, it is removed from the dict
26        regardless of whether there is a corresponding method.
27        """
28        # find methods with pre or post conditions
29        methods = []
30        for k, v in dict.items():
31            if k.endswith('_pre') or k.endswith('_post'):
32                assert isinstance(v, function)
33            elif isinstance(v, function):
34                methods.append(k)
35        for m in methods:
36            pre = dict.get("%s_pre" % m)
37            post = dict.get("%s_post" % m)
38            if pre or post:
39                dict[m] = cls.make_eiffel_method(dict[m], pre, post)
40
41
42class EiffelMetaClass1(EiffelBaseMetaClass):
43    # an implementation of the "eiffel" meta class that uses nested functions
44
45    @staticmethod
46    def make_eiffel_method(func, pre, post):
47        def method(self, *args, **kwargs):
48            if pre:
49                pre(self, *args, **kwargs)
50            rv = func(self, *args, **kwargs)
51            if post:
52                post(self, rv, *args, **kwargs)
53            return rv
54
55        if func.__doc__:
56            method.__doc__ = func.__doc__
57
58        return method
59
60
61class EiffelMethodWrapper:
62
63    def __init__(self, inst, descr):
64        self._inst = inst
65        self._descr = descr
66
67    def __call__(self, *args, **kwargs):
68        return self._descr.callmethod(self._inst, args, kwargs)
69
70
71class EiffelDescriptor:
72
73    def __init__(self, func, pre, post):
74        self._func = func
75        self._pre = pre
76        self._post = post
77
78        self.__name__ = func.__name__
79        self.__doc__ = func.__doc__
80
81    def __get__(self, obj, cls):
82        return EiffelMethodWrapper(obj, self)
83
84    def callmethod(self, inst, args, kwargs):
85        if self._pre:
86            self._pre(inst, *args, **kwargs)
87        x = self._func(inst, *args, **kwargs)
88        if self._post:
89            self._post(inst, x, *args, **kwargs)
90        return x
91
92
93class EiffelMetaClass2(EiffelBaseMetaClass):
94    # an implementation of the "eiffel" meta class that uses descriptors
95
96    make_eiffel_method = EiffelDescriptor
97
98
99class Tests(unittest.TestCase):
100
101    def testEiffelMetaClass1(self):
102        self._test(EiffelMetaClass1)
103
104    def testEiffelMetaClass2(self):
105        self._test(EiffelMetaClass2)
106
107    def _test(self, metaclass):
108        class Eiffel(metaclass=metaclass):
109            pass
110
111        class Test(Eiffel):
112            def m(self, arg):
113                """Make it a little larger"""
114                return arg + 1
115
116            def m2(self, arg):
117                """Make it a little larger"""
118                return arg + 1
119
120            def m2_pre(self, arg):
121                assert arg > 0
122
123            def m2_post(self, result, arg):
124                assert result > arg
125
126        class Sub(Test):
127            def m2(self, arg):
128                return arg**2
129
130            def m2_post(self, Result, arg):
131                super(Sub, self).m2_post(Result, arg)
132                assert Result < 100
133
134        t = Test()
135        self.assertEqual(t.m(1), 2)
136        self.assertEqual(t.m2(1), 2)
137        self.assertRaises(AssertionError, t.m2, 0)
138
139        s = Sub()
140        self.assertRaises(AssertionError, s.m2, 1)
141        self.assertRaises(AssertionError, s.m2, 10)
142        self.assertEqual(s.m2(5), 25)
143
144
145if __name__ == "__main__":
146    unittest.main()
147