Skip to content

Commit 4704237

Browse files
authored
Tested for data classes with fields (#289)
In this patch, we test for `dataclasses` which use `field`'s to define default values. Originally, we thought that there are issues with data classes in general due to the issue #288, but it turned out that the order of decorators matters (``dataclass`` first). We tested everything, and documented the order of the decorators accordingly. Fixes #288.
1 parent d6d73ca commit 4704237

File tree

2 files changed

+81
-3
lines changed

2 files changed

+81
-3
lines changed

docs/source/known_issues.rst

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
Known Issues
22
============
3-
**Integration with ``help()``**. We wanted to include the contracts in the output of ``help()``. Unfortunately,
3+
Integration with ``help()``
4+
---------------------------
5+
6+
We wanted to include the contracts in the output of ``help()``. Unfortunately,
47
``help()`` renders the ``__doc__`` of the class and not of the instance. For functions, this is the class
58
"function" which you can not inherit from. See this
69
`discussion on python-ideas <https://groups.google.com/forum/#!topic/python-ideas/c9ntrVuh6WE>`_ for more details.
710

8-
**Defining contracts outside of decorators**. We need to inspect the source code of the condition and error lambdas to
11+
Defining contracts outside of decorators
12+
----------------------------------------
13+
We need to inspect the source code of the condition and error lambdas to
914
generate the violation message and infer the error type in the documentation, respectively. ``inspect.getsource(.)``
1015
is broken on lambdas defined in decorators in Python 3.5.2+ (see
1116
`this bug report <https://bugs.python.org/issue21217>`_). We circumvented this bug by using ``inspect.findsource(.)``,
@@ -35,7 +40,9 @@ However, we haven't faced a situation in the code base where we would do somethi
3540
whether this is a big issue. As long as decorators are directly applied to functions and classes, everything
3641
worked fine on our code base.
3742

38-
**`*args` and `**kwargs`**. Since handling variable number of positional and/or keyword arguments requires complex
43+
``*args`` and ``**kwargs``
44+
--------------------------
45+
Since handling variable number of positional and/or keyword arguments requires complex
3946
logic and entails many edge cases (in particular in relation to how the arguments from the actual call are resolved and
4047
passed to the contract), we did not implement it. These special cases also impose changes that need to propagate to
4148
rendering the violation messages and related tools such as pyicontract-lint and sphinx-icontract. This is a substantial
@@ -44,3 +51,20 @@ effort and needs to be prioritized accordingly.
4451
Before we spend a large amount of time on this feature, please give us a signal through
4552
`the issue 147 <https://github.com/Parquery/icontract/issues/147>`_ and describe your concrete use case and its
4653
relevance. If there is enough feedback from the users, we will of course consider implementing it.
54+
55+
``dataclasses``
56+
---------------
57+
When you define contracts for `dataclasses <https://docs.python.org/3/library/dataclasses.html>`_, make sure you define the contracts *after* decorating the class with ``@dataclass`` decorator:
58+
59+
.. code-block:: python
60+
61+
>>> import icontract
62+
>>> import dataclasses
63+
64+
>>> @icontract.invariant(lambda self: self.x > 0)
65+
... @dataclasses.dataclass
66+
... class Foo:
67+
... x: int = dataclasses.field(default=42)
68+
69+
70+
This is necessary as we can not re-decorate the methods that ``dataclass`` decorator inserts.

tests_3_7/test_invariant.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ class RightHalfPlanePoint:
2626
RightHalfPlanePoint.__new__.__doc__,
2727
)
2828

29+
def test_on_dataclass_with_field(self) -> None:
30+
@icontract.invariant(lambda self: self.x > 0)
31+
@dataclasses.dataclass
32+
class Foo:
33+
x: int = dataclasses.field(default=42)
34+
35+
_ = Foo()
36+
2937

3038
class TestViolation(unittest.TestCase):
3139
def test_on_dataclass(self) -> None:
@@ -53,6 +61,52 @@ class RightHalfPlanePoint:
5361
tests.error.wo_mandatory_location(str(violation_error)),
5462
)
5563

64+
def test_on_dataclass_with_field(self) -> None:
65+
@icontract.invariant(lambda self: self.x < 0)
66+
@dataclasses.dataclass
67+
class Foo:
68+
x: int = dataclasses.field(default=-1)
69+
70+
violation_error = None # type: Optional[icontract.ViolationError]
71+
try:
72+
_ = Foo(3)
73+
except icontract.ViolationError as err:
74+
violation_error = err
75+
76+
self.assertIsNotNone(violation_error)
77+
self.assertEqual(
78+
textwrap.dedent(
79+
"""\
80+
self.x < 0:
81+
self was TestViolation.test_on_dataclass_with_field.<locals>.Foo(x=3)
82+
self.x was 3"""
83+
),
84+
tests.error.wo_mandatory_location(str(violation_error)),
85+
)
86+
87+
def test_on_dataclass_with_field_default_violating(self) -> None:
88+
@icontract.invariant(lambda self: self.x < 0)
89+
@dataclasses.dataclass
90+
class Foo:
91+
x: int = dataclasses.field(default=42)
92+
93+
violation_error = None # type: Optional[icontract.ViolationError]
94+
try:
95+
_ = Foo()
96+
except icontract.ViolationError as err:
97+
violation_error = err
98+
99+
self.assertIsNotNone(violation_error)
100+
self.assertEqual(
101+
textwrap.dedent(
102+
"""\
103+
self.x < 0:
104+
self was TestViolation.test_on_dataclass_with_field_default_violating.<locals>.Foo(x=42)
105+
self.x was 42"""
106+
),
107+
tests.error.wo_mandatory_location(str(violation_error)),
108+
)
109+
56110

57111
if __name__ == "__main__":
58112
unittest.main()

0 commit comments

Comments
 (0)