eiffel.py 3.8 KB

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