Skip to content

Commit

Permalink
Allowed __new__ to tighten pre-conditions (#211)
Browse files Browse the repository at this point in the history
We need to handle `__new__` diffrently from other functions as it is a
constructor. Hence it does not relate directly to the class hierarchy
and can define its own pre- and post-conditions.
  • Loading branch information
mristin committed Apr 23, 2021
1 parent eb0b401 commit 26b0481
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 4 deletions.
7 changes: 4 additions & 3 deletions icontract/_metaclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,10 @@ def _decorate_namespace_function(bases: List[type], namespace: MutableMapping[st

# Collect the preconditions and postconditions from bases.
#
# Preconditions and postconditions of __init__ of base classes are deliberately ignored (and not collapsed) since
# initialization is an operation specific to the concrete class and does not relate to the class hierarchy.
if key not in ['__init__']:
# Preconditions and postconditions of __init__ and __new__ of base classes are deliberately ignored
# (and not collapsed) since initialization is an operation specific to the concrete class and
# does not relate to the class hierarchy.
if key not in ['__init__', '__new__']:
base_preconditions = [] # type: List[List[Contract]]
base_snapshots = [] # type: List[Snapshot]
base_postconditions = [] # type: List[Contract]
Expand Down
57 changes: 56 additions & 1 deletion tests/test_inheritance_precondition.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
# pylint: disable=no-self-use

import abc
import textwrap
import unittest
from typing import Optional # pylint: disable=unused-import
from typing import Optional, Sequence, cast # pylint: disable=unused-import

import icontract
import tests.error
Expand Down Expand Up @@ -377,6 +378,60 @@ def some_prop(self) -> None:
'self.toggled was True', tests.error.wo_mandatory_location(str(violation_error)))


class TestConstructor(unittest.TestCase):
def test_init_tightens_preconditions(self) -> None:
class A(icontract.DBC):
def __init__(self, x: int) -> None:
pass

class B(A):
# B can require tighter pre-conditions than A.
# __init__ is a special case: while other functions need to satisfy Liskov substitution principle,
# __init__ is an exception.
@icontract.require(lambda x: x > 0)
def __init__(self, x: int) -> None:
super().__init__(x=x)

_ = B(3)

violation_error = None # type: Optional[icontract.ViolationError]
try:
_ = B(-1)
except icontract.ViolationError as err:
violation_error = err

self.assertIsNotNone(violation_error)
self.assertEqual('x > 0: x was -1', tests.error.wo_mandatory_location(str(violation_error)))

def test_new_tightens_preconditions(self) -> None:
class A(icontract.DBC):
def __new__(cls, xs: Sequence[int]) -> 'A':
return cast(A, xs)

class B(A):
# B can require tighter pre-conditions than A.
# __new__ is a special case: while other functions need to satisfy Liskov substitution principle,
# __new__ is an exception.
@icontract.require(lambda xs: all(x > 0 for x in xs))
def __new__(cls, xs: Sequence[int]) -> 'B':
return cast(B, xs)

_ = B([1, 2, 3])

violation_error = None # type: Optional[icontract.ViolationError]
try:
_ = B([-1, -2, -3])
except icontract.ViolationError as err:
violation_error = err

self.assertIsNotNone(violation_error)
self.assertEqual(
textwrap.dedent('''\
all(x > 0 for x in xs):
all(x > 0 for x in xs) was False
xs was [-1, -2, -3]'''), tests.error.wo_mandatory_location(str(violation_error)))


class TestInvalid(unittest.TestCase):
def test_abstract_method_not_implemented(self) -> None:
# pylint: disable=abstract-method
Expand Down

0 comments on commit 26b0481

Please sign in to comment.