Skip to content

Commit bb3fa1a

Browse files
committed
laws: restrict interface-patching to interfaces with laws.
We check that the interface class actually has laws. This should eliminate any false positives that come from patching interfaces that were defined outside `returns`. Added a `_ParentWrapper` to test that it doesn't show up. (It did before the change in this commit.) Add to CHANGELOG and the hypothesis plugins page.
1 parent 9f47ff3 commit bb3fa1a

File tree

6 files changed

+26
-16
lines changed

6 files changed

+26
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ See [0Ver](https://0ver.org/).
2424

2525
## 0.24.1
2626

27+
### Features
28+
29+
- Make `hypothesis` plugin test laws from user-defined interfaces too
30+
2731
### Bugfixes
2832

2933
- Add pickling support for `UnwrapFailedError` exception

docs/pages/contrib/hypothesis_plugins.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ It works in a combination with "Laws as Values" feature we provide in the core.
8181
8282
check_all_laws(YourCustomContainer)
8383
84-
This one line of code will generate ~100 tests for all defined law
84+
This one line of code will generate ~100 tests for all defined laws
8585
in both ``YourCustomContainer`` and all its super types,
86-
including our internal ones.
86+
including our internal ones and user-defined ones.
8787

8888
We also provide a way to configure
8989
the checking process with ``settings_kwargs``:

returns/contrib/hypothesis/laws.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from hypothesis.strategies._internal import types # noqa: PLC2701
1111

1212
from returns.contrib.hypothesis.containers import strategy_from_container
13-
from returns.primitives.laws import Law, Lawful
13+
from returns.primitives.laws import LAWS_ATTRIBUTE, Law, Lawful
1414

1515

1616
@final
@@ -214,7 +214,7 @@ def clean_plugin_context() -> Iterator[None]:
214214
st.register_type_strategy(*saved_state)
215215

216216

217-
def lawful_interfaces(container_type: type[Lawful]) -> set[type[object]]:
217+
def lawful_interfaces(container_type: type[Lawful]) -> set[type[Lawful]]:
218218
"""Return ancestors of `container_type` that are lawful interfaces."""
219219
return {
220220
base_type
@@ -227,7 +227,13 @@ def lawful_interfaces(container_type: type[Lawful]) -> set[type[object]]:
227227
def _is_lawful_interface(
228228
interface_type: type[object],
229229
) -> TypeGuard[type[Lawful]]:
230-
return issubclass(interface_type, Lawful)
230+
return issubclass(interface_type, Lawful) and _has_non_inherited_attribute(
231+
interface_type, LAWS_ATTRIBUTE
232+
)
233+
234+
235+
def _has_non_inherited_attribute(type_: type[object], attribute: str) -> bool:
236+
return attribute in type_.__dict__
231237

232238

233239
def _clean_caches() -> None:

returns/primitives/laws.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#: Special alias to define laws as functions even inside a class
1313
law_definition = staticmethod
1414

15+
LAWS_ATTRIBUTE: str = '_laws'
16+
1517

1618
class Law(Immutable):
1719
"""
@@ -132,7 +134,7 @@ def laws(cls) -> dict[type['Lawful'], Sequence[Law]]: # noqa: WPS210
132134

133135
laws = {}
134136
for klass in seen.values():
135-
current_laws = klass.__dict__.get('_laws', ())
137+
current_laws = klass.__dict__.get(LAWS_ATTRIBUTE, ())
136138
if not current_laws:
137139
continue
138140
laws[klass] = current_laws

tests/test_contrib/test_hypothesis/test_interface_resolution.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,11 @@ def test_lawful_interfaces__container_defined_in_returns() -> None:
1414
assert sorted(str(interface) for interface in result) == [
1515
"<class 'returns.interfaces.altable.AltableN'>",
1616
"<class 'returns.interfaces.applicative.ApplicativeN'>",
17-
"<class 'returns.interfaces.bimappable.BiMappableN'>",
1817
"<class 'returns.interfaces.container.ContainerN'>",
1918
"<class 'returns.interfaces.equable.Equable'>",
2019
"<class 'returns.interfaces.failable.DiverseFailableN'>",
2120
"<class 'returns.interfaces.failable.FailableN'>",
2221
"<class 'returns.interfaces.mappable.MappableN'>",
23-
"<class 'returns.interfaces.specific.result.ResultBasedN'>",
24-
"<class 'returns.interfaces.specific.result.ResultLikeN'>",
25-
"<class 'returns.interfaces.specific.result.UnwrappableResult'>",
2622
"<class 'returns.interfaces.swappable.SwappableN'>",
2723
]
2824

tests/test_contrib/test_hypothesis/test_laws/test_custom_interface_with_laws.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from collections.abc import Callable, Sequence
33
from typing import ClassVar, Generic, TypeAlias, TypeVar, final
44

5-
import pytest
6-
from hypothesis.errors import ResolutionFailed
75
from typing_extensions import Never
86

97
from returns.contrib.hypothesis.laws import check_all_laws
@@ -84,10 +82,14 @@ def map(
8482
_Mappable1: TypeAlias = _MappableN[_FirstType, Never, Never]
8583

8684

85+
class _ParentWrapper(_Mappable1[_ValueType]):
86+
"""Class that is an ancestor of `_Wrapper` but has no laws."""
87+
88+
8789
class _Wrapper(
8890
BaseContainer,
8991
SupportsKind1['_Wrapper', _ValueType],
90-
_Mappable1[_ValueType],
92+
_ParentWrapper[_ValueType],
9193
):
9294
"""Simple instance of `_MappableN`."""
9395

@@ -103,6 +105,6 @@ def map(
103105
return _Wrapper(function(self._inner_value))
104106

105107

106-
pytestmark = pytest.mark.xfail(raises=ResolutionFailed)
107-
108-
check_all_laws(_Wrapper)
108+
# We need to use `use_init=True` because `MappableN` does not automatically
109+
# get a strategy from `strategy_from_container`.
110+
check_all_laws(_Wrapper, use_init=True)

0 commit comments

Comments
 (0)