Home | History | Annotate | Download | only in demo
      1 #!/usr/bin/env python3
      2 
      3 """
      4 Support Eiffel-style preconditions and postconditions for functions.
      5 
      6 An example for Python metaclasses.
      7 """
      8 
      9 import unittest
     10 from types import FunctionType as function
     11 
     12 class 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 
     42 class 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 
     61 class 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 
     71 class 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 
     93 class EiffelMetaClass2(EiffelBaseMetaClass):
     94     # an implementation of the "eiffel" meta class that uses descriptors
     95 
     96     make_eiffel_method = EiffelDescriptor
     97 
     98 
     99 class 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 
    145 if __name__ == "__main__":
    146     unittest.main()
    147