lawcheck 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lawcheck/__init__.py +126 -0
- lawcheck/_types.py +23 -0
- lawcheck/oracle.py +119 -0
- lawcheck/primitives.py +624 -0
- lawcheck/py.typed +0 -0
- lawcheck/runners.py +300 -0
- lawcheck-0.2.0.dist-info/METADATA +185 -0
- lawcheck-0.2.0.dist-info/RECORD +10 -0
- lawcheck-0.2.0.dist-info/WHEEL +4 -0
- lawcheck-0.2.0.dist-info/licenses/LICENSE +21 -0
lawcheck/__init__.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""lawcheck: property-based algebraic law testing for Python.
|
|
2
|
+
|
|
3
|
+
Provides ready-made Hypothesis law suites for algebraic structures
|
|
4
|
+
(semigroup, monoid, commutative, functor, applicative, monad) with a pluggable
|
|
5
|
+
equality oracle. The equality oracle is always explicit; there is no default
|
|
6
|
+
``eq`` parameter anywhere in lawcheck.
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
Pure primitives (deterministic, no Hypothesis):
|
|
11
|
+
|
|
12
|
+
holds_associative, assert_associative
|
|
13
|
+
holds_left_identity, assert_left_identity
|
|
14
|
+
holds_right_identity, assert_right_identity
|
|
15
|
+
holds_commutative, assert_commutative
|
|
16
|
+
holds_functor_identity, assert_functor_identity
|
|
17
|
+
holds_functor_composition, assert_functor_composition
|
|
18
|
+
holds_applicative_identity, assert_applicative_identity
|
|
19
|
+
holds_applicative_homomorphism, assert_applicative_homomorphism
|
|
20
|
+
holds_applicative_interchange, assert_applicative_interchange
|
|
21
|
+
holds_applicative_composition, assert_applicative_composition
|
|
22
|
+
holds_monad_left_identity, assert_monad_left_identity
|
|
23
|
+
holds_monad_right_identity, assert_monad_right_identity
|
|
24
|
+
holds_monad_associativity, assert_monad_associativity
|
|
25
|
+
|
|
26
|
+
Hypothesis runners (search for counterexamples):
|
|
27
|
+
|
|
28
|
+
verify_semigroup
|
|
29
|
+
verify_monoid
|
|
30
|
+
verify_commutative
|
|
31
|
+
verify_functor
|
|
32
|
+
verify_applicative
|
|
33
|
+
verify_monad
|
|
34
|
+
|
|
35
|
+
Equality oracle helpers:
|
|
36
|
+
|
|
37
|
+
value_eq
|
|
38
|
+
by_repr_eq
|
|
39
|
+
normalized_eq
|
|
40
|
+
lifted_eq
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
from __future__ import annotations
|
|
44
|
+
|
|
45
|
+
from lawcheck.oracle import by_repr_eq, lifted_eq, normalized_eq, value_eq
|
|
46
|
+
from lawcheck.primitives import (
|
|
47
|
+
assert_applicative_composition,
|
|
48
|
+
assert_applicative_homomorphism,
|
|
49
|
+
assert_applicative_identity,
|
|
50
|
+
assert_applicative_interchange,
|
|
51
|
+
assert_associative,
|
|
52
|
+
assert_commutative,
|
|
53
|
+
assert_functor_composition,
|
|
54
|
+
assert_functor_identity,
|
|
55
|
+
assert_left_identity,
|
|
56
|
+
assert_monad_associativity,
|
|
57
|
+
assert_monad_left_identity,
|
|
58
|
+
assert_monad_right_identity,
|
|
59
|
+
assert_right_identity,
|
|
60
|
+
holds_applicative_composition,
|
|
61
|
+
holds_applicative_homomorphism,
|
|
62
|
+
holds_applicative_identity,
|
|
63
|
+
holds_applicative_interchange,
|
|
64
|
+
holds_associative,
|
|
65
|
+
holds_commutative,
|
|
66
|
+
holds_functor_composition,
|
|
67
|
+
holds_functor_identity,
|
|
68
|
+
holds_left_identity,
|
|
69
|
+
holds_monad_associativity,
|
|
70
|
+
holds_monad_left_identity,
|
|
71
|
+
holds_monad_right_identity,
|
|
72
|
+
holds_right_identity,
|
|
73
|
+
)
|
|
74
|
+
from lawcheck.runners import (
|
|
75
|
+
verify_applicative,
|
|
76
|
+
verify_commutative,
|
|
77
|
+
verify_functor,
|
|
78
|
+
verify_monad,
|
|
79
|
+
verify_monoid,
|
|
80
|
+
verify_semigroup,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
__all__ = [
|
|
84
|
+
# primitives - holds_*
|
|
85
|
+
"holds_associative",
|
|
86
|
+
"holds_left_identity",
|
|
87
|
+
"holds_right_identity",
|
|
88
|
+
"holds_commutative",
|
|
89
|
+
"holds_functor_identity",
|
|
90
|
+
"holds_functor_composition",
|
|
91
|
+
"holds_applicative_identity",
|
|
92
|
+
"holds_applicative_homomorphism",
|
|
93
|
+
"holds_applicative_interchange",
|
|
94
|
+
"holds_applicative_composition",
|
|
95
|
+
"holds_monad_left_identity",
|
|
96
|
+
"holds_monad_right_identity",
|
|
97
|
+
"holds_monad_associativity",
|
|
98
|
+
# primitives - assert_*
|
|
99
|
+
"assert_associative",
|
|
100
|
+
"assert_left_identity",
|
|
101
|
+
"assert_right_identity",
|
|
102
|
+
"assert_commutative",
|
|
103
|
+
"assert_functor_identity",
|
|
104
|
+
"assert_functor_composition",
|
|
105
|
+
"assert_applicative_identity",
|
|
106
|
+
"assert_applicative_homomorphism",
|
|
107
|
+
"assert_applicative_interchange",
|
|
108
|
+
"assert_applicative_composition",
|
|
109
|
+
"assert_monad_left_identity",
|
|
110
|
+
"assert_monad_right_identity",
|
|
111
|
+
"assert_monad_associativity",
|
|
112
|
+
# runners
|
|
113
|
+
"verify_semigroup",
|
|
114
|
+
"verify_monoid",
|
|
115
|
+
"verify_commutative",
|
|
116
|
+
"verify_functor",
|
|
117
|
+
"verify_applicative",
|
|
118
|
+
"verify_monad",
|
|
119
|
+
# oracle helpers
|
|
120
|
+
"value_eq",
|
|
121
|
+
"by_repr_eq",
|
|
122
|
+
"normalized_eq",
|
|
123
|
+
"lifted_eq",
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
__version__ = "0.2.0"
|
lawcheck/_types.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared type aliases used across lawcheck."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
A = TypeVar("A")
|
|
10
|
+
B = TypeVar("B")
|
|
11
|
+
C = TypeVar("C")
|
|
12
|
+
|
|
13
|
+
# A binary operation on a carrier type.
|
|
14
|
+
BinOp = Callable[[T, T], T]
|
|
15
|
+
|
|
16
|
+
# An equality predicate for a carrier type.
|
|
17
|
+
EqFn = Callable[[T, T], bool]
|
|
18
|
+
|
|
19
|
+
# A unary map from A to B.
|
|
20
|
+
MapFn = Callable[[A], B]
|
|
21
|
+
|
|
22
|
+
# A function returning a functor/applicative/monad value.
|
|
23
|
+
# These are left as generic Callable to avoid hard-coding structure types here.
|
lawcheck/oracle.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Equality oracle helpers.
|
|
2
|
+
|
|
3
|
+
The equality-oracle problem
|
|
4
|
+
----------------------------
|
|
5
|
+
Every law in lawcheck requires an explicit ``eq: Callable[[T, T], bool]``
|
|
6
|
+
argument. There is no default. This is intentional.
|
|
7
|
+
|
|
8
|
+
For plain value types (int, str, tuple, frozenset), Python's built-in ``==``
|
|
9
|
+
via ``operator.eq`` is correct. But for many types that benefit most from
|
|
10
|
+
algebraic law testing, ``==`` is meaningless or absent:
|
|
11
|
+
|
|
12
|
+
- Effects and IO wrappers (e.g. ``IO[A]``) are functions; two separately
|
|
13
|
+
constructed ``IO`` values are never ``==`` even if they produce the same
|
|
14
|
+
result.
|
|
15
|
+
- Async values (``Coroutine``, ``Task``) have no structural equality.
|
|
16
|
+
- Custom containers may intentionally omit ``__eq__`` (avoiding accidental
|
|
17
|
+
hashing bugs).
|
|
18
|
+
- Lazy sequences compare equal only after materialization.
|
|
19
|
+
|
|
20
|
+
The cats-effect Scala library confronted this exactly: without a formal
|
|
21
|
+
``Eq[A]`` type class, law testing for ``IO`` was impossible. Their solution
|
|
22
|
+
was a ``TestContext`` that runs effects and compares outcomes. lawcheck makes
|
|
23
|
+
the oracle explicit so you can supply the right comparison for your type.
|
|
24
|
+
|
|
25
|
+
This module provides:
|
|
26
|
+
|
|
27
|
+
- ``value_eq``: wraps ``==``; correct for int, str, list, etc.
|
|
28
|
+
- ``by_repr_eq``: compares repr strings; useful for debugging, NOT for
|
|
29
|
+
production law testing (repr can collide).
|
|
30
|
+
- ``normalized_eq``: compares after applying a normalization function; useful
|
|
31
|
+
for types with multiple canonical representations (e.g. rational numbers).
|
|
32
|
+
- ``lifted_eq``: lifts an element-level eq to a container by unwrapping;
|
|
33
|
+
suitable for simple single-value wrappers.
|
|
34
|
+
|
|
35
|
+
None of these are ever injected as a default. They are offered as documented
|
|
36
|
+
starting points.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from collections.abc import Callable
|
|
42
|
+
from typing import TypeVar
|
|
43
|
+
|
|
44
|
+
T = TypeVar("T")
|
|
45
|
+
A = TypeVar("A")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def value_eq(a: T, b: T) -> bool:
|
|
49
|
+
"""Equality via Python's built-in ``==``.
|
|
50
|
+
|
|
51
|
+
Correct for int, str, list, tuple, frozenset, and any type that properly
|
|
52
|
+
implements ``__eq__``. Wrong for effects, async types, and custom containers
|
|
53
|
+
that intentionally omit ``__eq__``.
|
|
54
|
+
"""
|
|
55
|
+
return bool(a == b)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def by_repr_eq(a: T, b: T) -> bool:
|
|
59
|
+
"""Equality by comparing ``repr`` strings.
|
|
60
|
+
|
|
61
|
+
Useful for quick debugging. NOT suitable for production law testing: repr
|
|
62
|
+
can collide (different objects producing the same repr) or differ for
|
|
63
|
+
logically equal objects.
|
|
64
|
+
"""
|
|
65
|
+
return repr(a) == repr(b)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalized_eq(
|
|
69
|
+
normalize: Callable[[T], A],
|
|
70
|
+
) -> Callable[[T, T], bool]:
|
|
71
|
+
"""Return an equality predicate that compares via a normalization function.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
normalize:
|
|
76
|
+
A function that maps the carrier type to a canonical form. Two values
|
|
77
|
+
are considered equal iff their normalized forms are equal under ``==``.
|
|
78
|
+
|
|
79
|
+
Example
|
|
80
|
+
-------
|
|
81
|
+
For a ``Fraction``-like type that can represent 1/2 and 2/4 as distinct
|
|
82
|
+
objects::
|
|
83
|
+
|
|
84
|
+
eq = normalized_eq(lambda f: (f.numerator // gcd, f.denominator // gcd))
|
|
85
|
+
holds_associative(add_fraction, a, b, c, eq=eq)
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def _eq(a: T, b: T) -> bool:
|
|
89
|
+
return bool(normalize(a) == normalize(b))
|
|
90
|
+
|
|
91
|
+
return _eq
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def lifted_eq(
|
|
95
|
+
unwrap: Callable[[T], A],
|
|
96
|
+
inner_eq: Callable[[A, A], bool],
|
|
97
|
+
) -> Callable[[T, T], bool]:
|
|
98
|
+
"""Return an equality predicate for a wrapper type by unwrapping first.
|
|
99
|
+
|
|
100
|
+
Parameters
|
|
101
|
+
----------
|
|
102
|
+
unwrap:
|
|
103
|
+
A function that extracts the inner value from the wrapper.
|
|
104
|
+
inner_eq:
|
|
105
|
+
Equality for the inner type.
|
|
106
|
+
|
|
107
|
+
Example
|
|
108
|
+
-------
|
|
109
|
+
For a simple ``Box(value)`` wrapper where law testing compares the
|
|
110
|
+
contained values::
|
|
111
|
+
|
|
112
|
+
eq = lifted_eq(lambda box: box.value, operator.eq)
|
|
113
|
+
verify_monoid(box_concat, Box(""), strategy=..., eq=eq)
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def _eq(a: T, b: T) -> bool:
|
|
117
|
+
return inner_eq(unwrap(a), unwrap(b))
|
|
118
|
+
|
|
119
|
+
return _eq
|