@@ -979,13 +979,14 @@ def wrapper(*args, **kwargs): # type: ignore
979979 return wrapper # type: ignore
980980
981981
982- def _decorate_with_invariants (func : CallableT , is_init : bool ) -> CallableT :
982+ def _decorate_with_invariants (func : CallableT , cls : ClassT , is_init : bool ) -> CallableT :
983983 """
984984 Decorate the method ``func`` with invariant checks.
985985
986986 If the function has been already decorated with invariant checks, the function returns immediately.
987987
988988 :param func: function to be wrapped
989+ :param cls: class corresponding to the invariant and ``func``
989990 :param is_init: True if the ``func`` is __init__
990991 :return: function wrapped with invariant checks
991992 """
@@ -1027,7 +1028,25 @@ def wrapper(*args, **kwargs): # type: ignore
10271028 try :
10281029 result = func (* args , ** kwargs )
10291030
1030- for invariant in instance .__class__ .__invariants__ :
1031+ # NOTE (mristin):
1032+ # We go to the invariants corresponding to the class, not the instance, as we need to
1033+ # account also for a situation where super().__init__ is called. Here is an example:
1034+ #
1035+ # @invariant(lambda self: ...)
1036+ # class A(DBC):
1037+ # pass
1038+ #
1039+ # @invariant(lambda self: ...)
1040+ # class B(A):
1041+ # def __init__(self) -> None:
1042+ # super().__init__()
1043+ # # ↖ After this call, only the invariants of A, but not B, have to be checked.
1044+ # #
1045+ # # However, the ``instance`` (i.e., resolved ``self``) in super().__init__ call points to
1046+ # # an instance of B, so instance.__cls__.__invariants__ refer to invariants of B, not A.
1047+
1048+ # noinspection PyUnresolvedReferences
1049+ for invariant in cls .__invariants__ : # type: ignore
10311050 _assert_invariant (contract = invariant , instance = instance )
10321051
10331052 return result
@@ -1273,35 +1292,97 @@ def add_invariant_checks(cls: ClassT) -> None:
12731292 )
12741293 )
12751294
1276- if init_func :
1277- # We have to distinguish this special case which is used by named
1278- # tuples and possibly other optimized data structures.
1279- # In those cases, we have to wrap __new__ instead of __init__.
1280- if init_func == object .__init__ and hasattr (cls , "__new__" ):
1281- new_func = getattr (cls , "__new__" )
1282- setattr (cls , "__new__" , _decorate_new_with_invariants (new_func ))
1283- else :
1284- wrapper = _decorate_with_invariants (func = init_func , is_init = True )
1285- setattr (cls , init_func .__name__ , wrapper )
1295+ assert init_func is not None , "Every class in Python must have a constructor."
1296+
1297+ # We must handle this special case which is used by named
1298+ # tuples and possibly other optimized data structures.
1299+ # In those cases, we have to wrap __new__ instead of __init__.
1300+ if init_func == object .__init__ and hasattr (cls , "__new__" ):
1301+ new_func = getattr (cls , "__new__" )
1302+ setattr (cls , "__new__" , _decorate_new_with_invariants (new_func ))
1303+ else :
1304+ # NOTE (mristin):
1305+ # We have to create a new __init__ function so that the invariants of *this* class are checked.
1306+ # The problem arises due to two different cases related to inheritance which we can not distinguish in Python.
1307+ # Namely, we can not know whether we are dealing with invariants coming from ``super().__init__`` or
1308+ # an implicit call to ``__init__``.
1309+ #
1310+ # In both of these edge cases, the instance is of the child class, but the constructors are
1311+ # of the parent class. Checking the invariants attached to the class would break the second case, while checking
1312+ # the invariants attached to the instance (through ``self.__class__.__invariants__``) would break the first
1313+ # case.
1314+ #
1315+ # The following snippets depict the two cases.
1316+ #
1317+ # Case 1: ``super().__init__``
1318+ # @invariant(lambda self: ...)
1319+ # class A(DBC):
1320+ # pass
1321+ #
1322+ # @invariant(lambda self: ...)
1323+ # class B(A):
1324+ # def __init__(self) -> None:
1325+ # super().__init__()
1326+ # # ↖ After this call, only the invariants of A, but not B, have to be checked.
1327+ # # More code follows, and after this ``__init__``, invariants of B have to be checked.
1328+ #
1329+ # Case 2: Implicit ``__init__`` call
1330+ # @invariant(lambda self: ...)
1331+ # class A(DBC):
1332+ # pass
1333+ #
1334+ # @invariant(lambda self: ...)
1335+ # class B(A):
1336+ # pass
1337+ #
1338+ # b = B()
1339+ # # ↖ After this call, the invariants of B have to be checked.
1340+ # # However, we only see the call to A.__init__, since there is no B.__init__.
1341+ #
1342+ # Therefore, to avert this problem, we have to create an ``__init__`` in the child class for the second
1343+ # case. This allows us to always check for invariants attached to the class in the case of constructors, so both
1344+ # cases can be successfully handled.
1345+
1346+ if "__init__" not in cls .__dict__ :
1347+ init_after_mro = (
1348+ # NOTE (mristin):
1349+ # mypy gives us the following warning:
1350+ # Accessing "__init__" on an instance is unsound, since instance.__init__ could be from an incompatible
1351+ # subclass
1352+ #
1353+ # ... but this is exactly what we want here -- we want to look up the __init__ of the class at runtime.
1354+ cls .__init__ # type: ignore
1355+ ) # This is the constructor after MRO, pointing to one of the parent classes.
1356+
1357+ def __init__ (self : Any , * args : Any , ** kwargs : Any ) -> None :
1358+ init_after_mro (self , * args , ** kwargs )
1359+
1360+ # NOTE (mristin):
1361+ # See the comment above corresponding to this mypy warning.
1362+ cls .__init__ = __init__ # type: ignore
1363+ init_func = __init__
1364+
1365+ wrapper = _decorate_with_invariants (func = init_func , cls = cls , is_init = True )
1366+ setattr (cls , init_func .__name__ , wrapper )
12861367
12871368 for name , func in names_funcs :
1288- wrapper = _decorate_with_invariants (func = func , is_init = False )
1369+ wrapper = _decorate_with_invariants (func = func , cls = cls , is_init = False )
12891370 setattr (cls , name , wrapper )
12901371
12911372 for name , prop in names_properties :
12921373 new_prop = property (
12931374 fget = (
1294- _decorate_with_invariants (func = prop .fget , is_init = False )
1375+ _decorate_with_invariants (func = prop .fget , cls = cls , is_init = False )
12951376 if prop .fget
12961377 else None
12971378 ),
12981379 fset = (
1299- _decorate_with_invariants (func = prop .fset , is_init = False )
1380+ _decorate_with_invariants (func = prop .fset , cls = cls , is_init = False )
13001381 if prop .fset
13011382 else None
13021383 ),
13031384 fdel = (
1304- _decorate_with_invariants (func = prop .fdel , is_init = False )
1385+ _decorate_with_invariants (func = prop .fdel , cls = cls , is_init = False )
13051386 if prop .fdel
13061387 else None
13071388 ),
0 commit comments