Repository: Parquery/icontract (language: python)

Issue:
Invariant of a subclass applies to its base
Invariants of subclasses affect their base classes, as well as sibling classes.

Here is a example to reproduce the problem:
```
from icontract import invariant

@invariant(lambda: True)
class Base:
    pass

@invariant(lambda: False)
class Derived(Base):
    pass

Base()  # ViolationError
```

By the way, thanks for the nice library!


Discussion / hints:
@verinov thanks a lot for reporting! I'm amazed that I didn't have a test case to catch this. Patch version 2.7.1 coming this week, then :)
I looked a bit more into this. The same code fails on older versions as well. But it doesn't fail, when I make `Base` derive from `icontract.DBC`.
My actual code uses `DBC`, and it did break with 2.6.6 -> 2.7.0. I'll try to make an small example, with fails and uses `DBC`.

Fix patch:
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


Test patch:
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: