diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 6d187ec..393874b 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -387,6 +387,20 @@ Inheritance To inherit the contracts of the parent class, the child class needs to either inherit from :class:`DBC` or have a meta class set to :class:`icontract.DBCMeta`. +.. note:: + + The inheritance from :class:`DBC` or using the meta class :class:`icontract.DBCMeta` is necessary so that + the contracts are correctly inherited from the parent to the child class. Otherwise, it is undefined + behavior how invariants, preconditions and postconditions will be inherited; most probably breaking + the Liskov substitution principle. + + In particular, the contracts are collapsed into lists for efficiency. If your child class does not + inherit from :class:`DBC` or you do not use the meta class :class:`icontract.DBCMeta`, the inherited + contracts in the child class will leak, and thus contracts from the child will be inserted into the + parent class. + + Hence, make sure you always use :class:`DBC` or :class:`icontract.DBCMeta` when dealing with inheritance. + When no contracts are specified in the child class, all contracts are inherited from the parent class as-are. When the child class introduces additional preconditions or postconditions and invariants, these contracts are diff --git a/tests/test_invariant.py b/tests/test_invariant.py index 9c8821d..e40ecd9 100644 --- a/tests/test_invariant.py +++ b/tests/test_invariant.py @@ -909,6 +909,46 @@ def __init__(self) -> None: tests.error.wo_mandatory_location(str(value_error)), ) + # noinspection PyUnusedLocal + def test_subclasses_affect_the_base_class_if_no_DBC(self) -> None: + # NOTE (mristin): + # This is a regression test for: + # https://github.com/Parquery/icontract/issues/295 + # + # where the invariants of the derived class erroneously "slipped into" + # the base class when DBC or DBCMeta were not set in the ``Base`` class. + + # NOTE (mristin): + # ``Base`` class should either inherit from ``icontract.DBC`` or have + # the metaclass ``icontract.DBCMeta``. If it does not, the invariants will be + # copied by reference, thus allowing the ``Derived`` class to mess them up. + @icontract.invariant(lambda: True) + class Base: + def __repr__(self) -> str: + return "an instance of {}".format(self.__class__.__name__) + + # NOTE (mristin): + # We do not want to instantiate a derived class, but this is a regression test. + @icontract.invariant(lambda: False) + class Derived(Base): # pylint: disable=unused-variable + pass + + # NOTE (mristin): + # This causes a ``ViolationError``, as invariants are copied by reference if + # the ``Base`` does not inherit from ``icontract.DBC`` or uses + # ``icontract.DBCMeta`` as the metaclass. + violation_error = None # type: Optional[icontract.ViolationError] + try: + _ = Base() + except icontract.ViolationError as err: + violation_error = err + + self.assertIsNotNone(violation_error) + self.assertEqual( + textwrap.dedent("""False: self was an instance of Base"""), + tests.error.wo_mandatory_location(str(violation_error)), + ) + class TestCheckOn(unittest.TestCase): def test_invariant_checked_in_init_if_no_flag_set(self) -> None: