blob: 736abea81738c8fc9b49be8c05d2a6b3340ebdb5 [file] [log] [blame]
Georg Brandl856898b2010-12-30 22:11:50 +00001#!/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:
Serhiy Storchaka6a98fe92013-07-16 22:12:03 +030039 dict[m] = cls.make_eiffel_method(dict[m], pre, post)
Georg Brandl856898b2010-12-30 22:11:50 +000040
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()