Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tested and documented undefined inheritance #296

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions tests/test_invariant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading