Metadata-Version: 2.4
Name: lawcheck
Version: 0.2.0
Summary: Property-based algebraic law testing for Python: Hypothesis-driven law suites for semigroups, monoids, functors, applicatives, and monads, with a pluggable equality oracle.
Project-URL: Homepage, https://github.com/amaar-mc/lawcheck
Project-URL: Repository, https://github.com/amaar-mc/lawcheck
Project-URL: Issues, https://github.com/amaar-mc/lawcheck/issues
Project-URL: Changelog, https://github.com/amaar-mc/lawcheck/blob/main/CHANGELOG.md
Author-email: Amaar Chughtai <amaardevx@gmail.com>
License: MIT License
        
        Copyright (c) 2026 Amaar Chughtai
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: algebra,applicative,functor,hypothesis,laws,monad,monoid,property-based-testing,semigroup,testing
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: hypothesis>=6.0.0
Provides-Extra: dev
Requires-Dist: hypothesis>=6.0.0; extra == 'dev'
Requires-Dist: mypy>=1.9.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# lawcheck

<p align="center">
  <img src="assets/logo.png" alt="lawcheck logo" width="160">
</p>

Property-based algebraic law testing for Python. Ready-made [Hypothesis](https://hypothesis.readthedocs.io) law suites for semigroups, monoids, commutative structures, functors, applicatives, and monads, with a pluggable equality oracle.

## The problem

Algebraic laws (associativity, identity, functor composition, monad associativity) are easy to state but hard to test systematically. You want Hypothesis to search for counterexamples, not to write the laws yourself every time.

The bigger problem: **what does "equal" mean for your type?**

For `int` it's obvious. For `IO[A]`, `Task`, async wrappers, or any effectful type, two logically equal values are often physically distinct objects. Python's `==` is wrong or absent. The [cats-effect](https://typelevel.org/cats-effect/) Scala library confronted this exactly and introduced a formal `Eq` type-class hierarchy. lawcheck asks you to supply `eq` explicitly so the meaning of "equal" is always unambiguous.

## Install

```
pip install lawcheck
```

Sole runtime dependency: `hypothesis`.

## Usage

### Layer 1: pure primitives (no Hypothesis, deterministic)

```python
import operator
from lawcheck import holds_associative, assert_associative

# Check one triple
holds_associative(operator.add, 1, 2, 3, eq=operator.eq)  # True
holds_associative(operator.sub, 1, 2, 3, eq=operator.eq)  # False

# Assert or raise with a precise message
assert_associative(operator.sub, 1, 2, 3, eq=operator.eq)
# AssertionError: Associativity violated: (op(1, 2)) op 3 = -4, but 1 op (op(2, 3)) = 2
```

### Layer 2: Hypothesis runners (search for counterexamples)

```python
import operator
from hypothesis import strategies as st
from lawcheck import verify_monoid, verify_commutative, verify_semigroup

# Passes for int addition (associative, left and right identity at 0, commutative)
verify_monoid(operator.add, 0, strategy=st.integers(), eq=operator.eq)
verify_commutative(operator.add, strategy=st.integers(), eq=operator.eq)

# Finds a counterexample for subtraction
verify_semigroup(operator.sub, strategy=st.integers(), eq=operator.eq)
# Raises with Hypothesis's shrunk minimal counterexample
```

### Functor, applicative, and monad laws

```python
from lawcheck import verify_functor, verify_applicative, verify_monad

# fmap: (f: A -> B, fa: F[A]) -> F[B]
verify_functor(
    my_fmap,
    f=lambda x: x * 2,
    g=lambda x: x + 1,
    strategy=my_strategy,
    eq=my_eq,
)

# pure: A -> F[A];  ap: (F[A -> B], F[A]) -> F[B]
verify_applicative(
    my_pure,
    my_ap,
    lambda x: x * 2,
    value_strategy=st.integers(),
    functor_strategy=my_fa_strategy,
    ap_strategy=my_fn_strategy,
    eq=my_eq,
)

verify_monad(
    ret=my_return,
    bind=my_bind,
    f_arrow=lambda x: my_return(x + 1),
    g_arrow=lambda x: my_return(x * 2),
    strategy=st.integers(),
    monad_strategy=my_monad_strategy,
    eq=my_eq,
)
```

### Pluggable equality oracle

```python
from lawcheck import normalized_eq, lifted_eq, value_eq

# Compare after normalization (e.g. abs value, canonical form)
abs_eq = normalized_eq(abs)

# Unwrap a container and compare inner values
box_eq = lifted_eq(lambda b: b.value, operator.eq)

# Plain == (correct for ints, strings, lists)
value_eq(1, 1)  # True
```

## Laws implemented

| Structure | Laws |
|-----------|------|
| Semigroup | Associativity: `(a op b) op c == a op (b op c)` |
| Monoid | Associativity + left identity + right identity |
| Commutative | `a op b == b op a` |
| Functor | Identity: `fmap(id) == id`; Composition: `fmap(f.g) == fmap(f).fmap(g)` |
| Applicative | Identity: `ap(pure(id), v) == v`; Homomorphism: `ap(pure(f), pure(x)) == pure(f(x))`; Interchange: `ap(u, pure(y)) == ap(pure(lambda f: f(y)), u)`; Composition: `ap(ap(ap(pure(compose), u), v), w) == ap(u, ap(v, w))` |
| Monad | Left identity, right identity, associativity of bind |

## The `eq` parameter is required everywhere

lawcheck has no default for `eq`. Where ergonomics would tempt a default, there is an explicit second function instead (e.g., `value_eq` for plain types). This is by design: the cats-effect equality-oracle problem is real, and baking in `==` would silently produce wrong answers for effects, async types, and custom containers.

See [`docs/architecture.md`](docs/architecture.md) for the full design rationale.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).

## License

MIT. See [LICENSE](LICENSE).
