Skip to content

Commit

Permalink
disabled invariant checks during construction (#151)
Browse files Browse the repository at this point in the history
The invariant checks need to be disabled during the construction as
a call to a member function might trigger an invariant check on an
unitialized attribute.

Fixes #149.
  • Loading branch information
mristin committed Sep 15, 2020
1 parent f99d743 commit e2c5053
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 13 deletions.
37 changes: 25 additions & 12 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,19 @@ in progress and removed once the invariants checking finished. As long as the du
``__dbc_invariant_check_is_in_progress__`` is present, the wrappers that check invariants simply return the result of
the function.

Invariant checks also need to be disabled during the construction since calling member functions would trigger invariant
checks which, on their hand, might check on yet-to-be-defined instance attributes. See the following snippet:

.. code-block:: python
@icontract.invariant(lambda self: self.some_attribute > 0)
class SomeClass(icontract.DBC):
def __init__(self) -> None:
self.some_attribute = self.some_func()
def some_func(self) -> int:
return 1984
Linter
------
We provide a linter that statically verifies the arguments of the contracts (*i.e.* that they are
Expand Down Expand Up @@ -797,39 +810,39 @@ Benchmarking invariant at __init__:
========================= ============ ============== =======================
Case Total time Time per run Relative time per run
========================= ============ ============== =======================
`ClassWithIcontract` 1.37 s 1.37 μs 296%
`ClassWithDpcontracts` 0.46 s 0.46 μs 100%
`ClassWithInlineContract` 0.27 s 0.27 μs 59%
`ClassWithIcontract` 1.43 s 1.43 μs 306%
`ClassWithDpcontracts` 0.47 s 0.47 μs 100%
`ClassWithInlineContract` 0.27 s 0.27 μs 57%
========================= ============ ============== =======================

Benchmarking invariant at a function:

========================= ============ ============== =======================
Case Total time Time per run Relative time per run
========================= ============ ============== =======================
`ClassWithIcontract` 2.14 s 2.14 μs 452%
`ClassWithDpcontracts` 0.47 s 0.47 μs 100%
`ClassWithInlineContract` 0.25 s 0.25 μs 53%
`ClassWithIcontract` 2.00 s 2.00 μs 445%
`ClassWithDpcontracts` 0.45 s 0.45 μs 100%
`ClassWithInlineContract` 0.23 s 0.23 μs 52%
========================= ============ ============== =======================

Benchmarking precondition:

=============================== ============ ============== =======================
Case Total time Time per run Relative time per run
=============================== ============ ============== =======================
`function_with_icontract` 0.02 s 2.41 μs 5%
`function_with_dpcontracts` 0.53 s 53.20 μs 100%
`function_with_inline_contract` 0.00 s 0.16 μs 0%
`function_with_icontract` 0.02 s 2.38 μs 5%
`function_with_dpcontracts` 0.51 s 50.89 μs 100%
`function_with_inline_contract` 0.00 s 0.15 μs 0%
=============================== ============ ============== =======================

Benchmarking postcondition:

=============================== ============ ============== =======================
Case Total time Time per run Relative time per run
=============================== ============ ============== =======================
`function_with_icontract` 0.03 s 2.51 μs 5%
`function_with_dpcontracts` 0.52 s 52.42 μs 100%
`function_with_inline_contract` 0.00 s 0.17 μs 0%
`function_with_icontract` 0.02 s 2.48 μs 5%
`function_with_dpcontracts` 0.51 s 50.93 μs 100%
`function_with_inline_contract` 0.00 s 0.15 μs 0%
=============================== ============ ============== =======================


Expand Down
4 changes: 3 additions & 1 deletion icontract/_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,15 +458,17 @@ def _decorate_with_invariants(func: CallableT, is_init: bool) -> CallableT:

def wrapper(*args, **kwargs): # type: ignore
"""Wrap __init__ method of a class by checking the invariants *after* the invocation."""
result = func(*args, **kwargs)
instance = _find_self(param_names=param_names, args=args, kwargs=kwargs)
assert instance is not None, "Expected to find `self` in the parameters, but found none."

# We need to disable the invariants check during the constructor.
id_instance = str(id(instance))
setattr(_IN_PROGRESS, id_instance, True)

# ExitStack is not used here due to performance.
try:
result = func(*args, **kwargs)

for contract in instance.__class__.__invariants__:
_assert_invariant(contract=contract, instance=instance)

Expand Down
17 changes: 17 additions & 0 deletions tests/test_recursion.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,23 @@ def another_func(self) -> bool:
some_instance.another_func()
self.assertListEqual(['some_func', 'another_func', 'some_func'], order)

def test_member_function_call_in_constructor(self) -> None:
order = [] # type: List[str]

@icontract.invariant(lambda self: self.some_attribute > 0) # pylint: disable=no-member
class SomeClass(icontract.DBC):
def __init__(self) -> None:
order.append('__init__ enters')
self.some_attribute = self.some_func()
order.append('__init__ exits')

def some_func(self) -> int:
order.append('some_func')
return 3

_ = SomeClass()
self.assertListEqual(['__init__ enters', 'some_func', '__init__ exits'], order)


if __name__ == '__main__':
unittest.main()

0 comments on commit e2c5053

Please sign in to comment.