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 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