eiffel.py 3.81 KB
Newer Older
1
#!/usr/bin/env python3
2

3 4 5 6 7 8 9
"""
Support Eiffel-style preconditions and postconditions for functions.

An example for Python metaclasses.
"""

import unittest
10
from types import FunctionType as function
11 12 13

class EiffelBaseMetaClass(type):

14 15
    def __new__(meta, name, bases, dict):
        meta.convert_methods(dict)
16 17
        return super(EiffelBaseMetaClass, meta).__new__(
            meta, name, bases, dict)
18

Guido van Rossum's avatar
Guido van Rossum committed
19
    @classmethod
20 21 22 23 24 25 26 27 28 29
    def convert_methods(cls, dict):
        """Replace functions in dict with EiffelMethod wrappers.

        The dict is modified in place.

        If a method ends in _pre or _post, it is removed from the dict
        regardless of whether there is a corresponding method.
        """
        # find methods with pre or post conditions
        methods = []
30
        for k, v in dict.items():
31 32 33 34 35 36 37 38
            if k.endswith('_pre') or k.endswith('_post'):
                assert isinstance(v, function)
            elif isinstance(v, function):
                methods.append(k)
        for m in methods:
            pre = dict.get("%s_pre" % m)
            post = dict.get("%s_post" % m)
            if pre or post:
39
                dict[m] = cls.make_eiffel_method(dict[m], pre, post)
40

41

42 43 44
class EiffelMetaClass1(EiffelBaseMetaClass):
    # an implementation of the "eiffel" meta class that uses nested functions

Guido van Rossum's avatar
Guido van Rossum committed
45
    @staticmethod
46 47 48 49
    def make_eiffel_method(func, pre, post):
        def method(self, *args, **kwargs):
            if pre:
                pre(self, *args, **kwargs)
50
            rv = func(self, *args, **kwargs)
51
            if post:
52 53
                post(self, rv, *args, **kwargs)
            return rv
54 55 56 57 58

        if func.__doc__:
            method.__doc__ = func.__doc__

        return method
59

60

61 62 63 64 65 66 67 68 69
class EiffelMethodWrapper:

    def __init__(self, inst, descr):
        self._inst = inst
        self._descr = descr

    def __call__(self, *args, **kwargs):
        return self._descr.callmethod(self._inst, args, kwargs)

70 71

class EiffelDescriptor:
72 73 74 75 76

    def __init__(self, func, pre, post):
        self._func = func
        self._pre = pre
        self._post = post
77

78 79 80 81 82 83 84 85 86 87 88 89 90 91
        self.__name__ = func.__name__
        self.__doc__ = func.__doc__

    def __get__(self, obj, cls):
        return EiffelMethodWrapper(obj, self)

    def callmethod(self, inst, args, kwargs):
        if self._pre:
            self._pre(inst, *args, **kwargs)
        x = self._func(inst, *args, **kwargs)
        if self._post:
            self._post(inst, x, *args, **kwargs)
        return x

92

93
class EiffelMetaClass2(EiffelBaseMetaClass):
94 95 96 97
    # an implementation of the "eiffel" meta class that uses descriptors

    make_eiffel_method = EiffelDescriptor

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

class Tests(unittest.TestCase):

    def testEiffelMetaClass1(self):
        self._test(EiffelMetaClass1)

    def testEiffelMetaClass2(self):
        self._test(EiffelMetaClass2)

    def _test(self, metaclass):
        class Eiffel(metaclass=metaclass):
            pass

        class Test(Eiffel):
            def m(self, arg):
                """Make it a little larger"""
                return arg + 1

            def m2(self, arg):
                """Make it a little larger"""
                return arg + 1

            def m2_pre(self, arg):
                assert arg > 0

            def m2_post(self, result, arg):
                assert result > arg

        class Sub(Test):
            def m2(self, arg):
                return arg**2

            def m2_post(self, Result, arg):
                super(Sub, self).m2_post(Result, arg)
                assert Result < 100

        t = Test()
        self.assertEqual(t.m(1), 2)
        self.assertEqual(t.m2(1), 2)
        self.assertRaises(AssertionError, t.m2, 0)

        s = Sub()
        self.assertRaises(AssertionError, s.m2, 1)
        self.assertRaises(AssertionError, s.m2, 10)
        self.assertEqual(s.m2(5), 25)

144 145

if __name__ == "__main__":
146
    unittest.main()