lawcheck 0.2.0__tar.gz
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-0.2.0/.github/ISSUE_TEMPLATE/bug_report.md +26 -0
- lawcheck-0.2.0/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- lawcheck-0.2.0/.github/workflows/ci.yml +22 -0
- lawcheck-0.2.0/.gitignore +40 -0
- lawcheck-0.2.0/CHANGELOG.md +41 -0
- lawcheck-0.2.0/CLAUDE.md +69 -0
- lawcheck-0.2.0/CODE_OF_CONDUCT.md +28 -0
- lawcheck-0.2.0/CONTRIBUTING.md +36 -0
- lawcheck-0.2.0/LICENSE +21 -0
- lawcheck-0.2.0/PKG-INFO +185 -0
- lawcheck-0.2.0/README.md +132 -0
- lawcheck-0.2.0/SECURITY.md +13 -0
- lawcheck-0.2.0/assets/.gitkeep +0 -0
- lawcheck-0.2.0/assets/logo.png +0 -0
- lawcheck-0.2.0/docs/architecture.md +143 -0
- lawcheck-0.2.0/docs/charter.md +40 -0
- lawcheck-0.2.0/docs/logo-prompt.md +7 -0
- lawcheck-0.2.0/examples/int_monoid.py +77 -0
- lawcheck-0.2.0/pyproject.toml +73 -0
- lawcheck-0.2.0/src/lawcheck/__init__.py +126 -0
- lawcheck-0.2.0/src/lawcheck/_types.py +23 -0
- lawcheck-0.2.0/src/lawcheck/oracle.py +119 -0
- lawcheck-0.2.0/src/lawcheck/primitives.py +624 -0
- lawcheck-0.2.0/src/lawcheck/py.typed +0 -0
- lawcheck-0.2.0/src/lawcheck/runners.py +300 -0
- lawcheck-0.2.0/tests/__init__.py +0 -0
- lawcheck-0.2.0/tests/conftest.py +22 -0
- lawcheck-0.2.0/tests/maybe.py +71 -0
- lawcheck-0.2.0/tests/test_applicative.py +311 -0
- lawcheck-0.2.0/tests/test_commutative.py +86 -0
- lawcheck-0.2.0/tests/test_functor.py +172 -0
- lawcheck-0.2.0/tests/test_monad.py +200 -0
- lawcheck-0.2.0/tests/test_monoid.py +105 -0
- lawcheck-0.2.0/tests/test_oracle.py +48 -0
- lawcheck-0.2.0/tests/test_semigroup.py +88 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Report a bug in lawcheck
|
|
4
|
+
title: "[bug] "
|
|
5
|
+
labels: bug
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
**Describe the bug**
|
|
9
|
+
A clear description of what the bug is.
|
|
10
|
+
|
|
11
|
+
**Minimal reproducer**
|
|
12
|
+
```python
|
|
13
|
+
# Paste the smallest code that triggers the bug
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Expected behavior**
|
|
17
|
+
What you expected to happen.
|
|
18
|
+
|
|
19
|
+
**Actual behavior**
|
|
20
|
+
What actually happened, including any tracebacks.
|
|
21
|
+
|
|
22
|
+
**Environment**
|
|
23
|
+
- lawcheck version:
|
|
24
|
+
- Python version:
|
|
25
|
+
- hypothesis version:
|
|
26
|
+
- OS:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest a new law suite or improvement
|
|
4
|
+
title: "[feature] "
|
|
5
|
+
labels: enhancement
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
**What algebraic law or feature would you like added?**
|
|
9
|
+
Describe the law and why it would be useful.
|
|
10
|
+
|
|
11
|
+
**Example usage**
|
|
12
|
+
```python
|
|
13
|
+
# How would you use this feature?
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Are you willing to implement it?**
|
|
17
|
+
Yes / No / Maybe.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: pip install -e ".[dev]"
|
|
20
|
+
- run: ruff check .
|
|
21
|
+
- run: mypy src
|
|
22
|
+
- run: pytest -q
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg
|
|
7
|
+
*.egg-info/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.hypothesis/
|
|
15
|
+
*.coverage
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# Virtual environments
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
|
|
24
|
+
# Distribution / packaging
|
|
25
|
+
*.tar.gz
|
|
26
|
+
*.whl
|
|
27
|
+
|
|
28
|
+
# Editor
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
*.swp
|
|
32
|
+
*.swo
|
|
33
|
+
*~
|
|
34
|
+
|
|
35
|
+
# OS
|
|
36
|
+
.DS_Store
|
|
37
|
+
Thumbs.db
|
|
38
|
+
|
|
39
|
+
# uv lockfile (library: not committed)
|
|
40
|
+
uv.lock
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to lawcheck are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
6
|
+
lawcheck uses [semantic versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-06-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Applicative laws: four pure primitives (`holds_applicative_identity`,
|
|
13
|
+
`holds_applicative_homomorphism`, `holds_applicative_interchange`,
|
|
14
|
+
`holds_applicative_composition`) and their `assert_*` counterparts.
|
|
15
|
+
- `verify_applicative` Hypothesis runner: verifies all four applicative laws
|
|
16
|
+
given `pure`, `ap`, a homomorphism function, and three strategies.
|
|
17
|
+
- The applicative law suite accepts `pure: Callable[[object], object]` and
|
|
18
|
+
`ap: Callable[[object, object], object]`, consistent with the existing
|
|
19
|
+
functor/monad object-typed signatures.
|
|
20
|
+
- `eq` is required everywhere; no default.
|
|
21
|
+
- Meta-tests: Maybe extended with `maybe_pure` and `maybe_ap`; lawful Maybe
|
|
22
|
+
passes all four laws; `broken_ap` (always returns Nothing) is caught by
|
|
23
|
+
identity and homomorphism; custom eq oracle is honored.
|
|
24
|
+
|
|
25
|
+
### Note
|
|
26
|
+
|
|
27
|
+
PyPI publication is pending new-project quota approval. The dist artifact
|
|
28
|
+
is built and twine-checked; publication will proceed once the quota clears.
|
|
29
|
+
|
|
30
|
+
## [0.1.0] - 2026-06-17
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Pure assertion primitives for semigroup, monoid, commutativity, functor, and monad laws.
|
|
35
|
+
- Hypothesis-powered runners: `verify_semigroup`, `verify_monoid`, `verify_commutative`, `verify_functor`, `verify_monad`.
|
|
36
|
+
- Pluggable equality oracle: `eq` is required everywhere, no default.
|
|
37
|
+
- Oracle helpers: `value_eq`, `by_repr_eq`, `normalized_eq`, `lifted_eq`.
|
|
38
|
+
- Self-verifying meta-tests: lawful structures pass, unlawful structures are caught.
|
|
39
|
+
- Full strict mypy typing, `py.typed` marker.
|
|
40
|
+
- Example script (`examples/int_monoid.py`) demonstrating both layers.
|
|
41
|
+
- CI: pytest, ruff, mypy on Python 3.10-3.13.
|
lawcheck-0.2.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# lawcheck - CLAUDE.md
|
|
2
|
+
|
|
3
|
+
## Project overview
|
|
4
|
+
|
|
5
|
+
lawcheck is a property-based algebraic law testing library for Python. It provides ready-made Hypothesis law suites for semigroups, monoids, commutative structures, functors, and monads, with a pluggable equality oracle. The equality oracle is the central design point: `eq` is always required, never defaulted.
|
|
6
|
+
|
|
7
|
+
## Stack
|
|
8
|
+
|
|
9
|
+
- Python 3.10+
|
|
10
|
+
- Build: hatchling, src layout
|
|
11
|
+
- Runtime dep: hypothesis only
|
|
12
|
+
- Dev: pytest, ruff, mypy
|
|
13
|
+
- Package manager: uv
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv venv
|
|
19
|
+
uv pip install -e ".[dev]"
|
|
20
|
+
uv run pytest -q
|
|
21
|
+
uv run ruff check .
|
|
22
|
+
uv run mypy src
|
|
23
|
+
uv build
|
|
24
|
+
uv run --with twine twine check dist/*
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Module layout
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
src/lawcheck/
|
|
31
|
+
__init__.py # public API, __all__
|
|
32
|
+
_types.py # TypeVar aliases
|
|
33
|
+
primitives.py # holds_* and assert_* (no Hypothesis)
|
|
34
|
+
runners.py # verify_* (Hypothesis-powered)
|
|
35
|
+
oracle.py # equality oracle helpers
|
|
36
|
+
py.typed
|
|
37
|
+
tests/
|
|
38
|
+
maybe.py # minimal Maybe type for tests only
|
|
39
|
+
conftest.py # Hypothesis profile
|
|
40
|
+
test_semigroup.py
|
|
41
|
+
test_monoid.py
|
|
42
|
+
test_commutative.py
|
|
43
|
+
test_functor.py
|
|
44
|
+
test_monad.py
|
|
45
|
+
test_oracle.py
|
|
46
|
+
examples/
|
|
47
|
+
int_monoid.py
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Key design rules
|
|
51
|
+
|
|
52
|
+
1. `eq` is required at every call site; no default.
|
|
53
|
+
2. No default parameter values anywhere. Use a second named function instead.
|
|
54
|
+
3. Pure primitives (`holds_*`, `assert_*`) in primitives.py; Hypothesis runners (`verify_*`) in runners.py.
|
|
55
|
+
4. No em dash characters anywhere.
|
|
56
|
+
5. Strict mypy, ruff clean.
|
|
57
|
+
6. Git author: Amaar Chughtai, no Co-authored-by trailers.
|
|
58
|
+
|
|
59
|
+
## Adding a new law suite
|
|
60
|
+
|
|
61
|
+
1. Add `holds_*` and `assert_*` to `primitives.py`.
|
|
62
|
+
2. Add `verify_*` to `runners.py`.
|
|
63
|
+
3. Export from `__init__.py`, add to `__all__`.
|
|
64
|
+
4. Add meta-tests: one lawful pass, one unlawful fail.
|
|
65
|
+
|
|
66
|
+
## Deferred
|
|
67
|
+
|
|
68
|
+
- Applicative laws (next addition after v0.1.0).
|
|
69
|
+
- pytest plugin for auto law collection.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as contributors and maintainers pledge to make participation in lawcheck a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
6
|
+
|
|
7
|
+
## Our Standards
|
|
8
|
+
|
|
9
|
+
Examples of behavior that contributes to a positive environment:
|
|
10
|
+
|
|
11
|
+
- Using welcoming and inclusive language
|
|
12
|
+
- Being respectful of differing viewpoints and experiences
|
|
13
|
+
- Gracefully accepting constructive criticism
|
|
14
|
+
- Focusing on what is best for the community
|
|
15
|
+
|
|
16
|
+
Examples of unacceptable behavior:
|
|
17
|
+
|
|
18
|
+
- Harassment, insults, or derogatory comments
|
|
19
|
+
- Publishing private information without explicit permission
|
|
20
|
+
- Other conduct that could reasonably be considered inappropriate
|
|
21
|
+
|
|
22
|
+
## Enforcement
|
|
23
|
+
|
|
24
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting the maintainers directly. All complaints will be reviewed and investigated promptly.
|
|
25
|
+
|
|
26
|
+
## Attribution
|
|
27
|
+
|
|
28
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions are welcome. Please open an issue first to discuss significant changes.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
git clone https://github.com/amaar-mc/lawcheck
|
|
9
|
+
cd lawcheck
|
|
10
|
+
uv venv
|
|
11
|
+
uv pip install -e ".[dev]"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Running checks
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
uv run pytest -q
|
|
18
|
+
uv run ruff check .
|
|
19
|
+
uv run mypy src
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
All three must pass before opening a pull request.
|
|
23
|
+
|
|
24
|
+
## Adding a new law suite
|
|
25
|
+
|
|
26
|
+
1. Add pure `holds_*` and `assert_*` primitives to `src/lawcheck/primitives.py`.
|
|
27
|
+
2. Add a `verify_*` runner to `src/lawcheck/runners.py`.
|
|
28
|
+
3. Export from `src/lawcheck/__init__.py` and add to `__all__`.
|
|
29
|
+
4. Add meta-tests: at least one lawful case and one unlawful case.
|
|
30
|
+
|
|
31
|
+
## Style
|
|
32
|
+
|
|
33
|
+
- Python 3.10+, strict mypy, ruff.
|
|
34
|
+
- No default parameter values. Provide a second named function where ergonomics require it.
|
|
35
|
+
- No em dash characters in code, comments, or commit messages.
|
|
36
|
+
- Commit format: `type(scope): description`.
|
lawcheck-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amaar Chughtai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
lawcheck-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lawcheck
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Property-based algebraic law testing for Python: Hypothesis-driven law suites for semigroups, monoids, functors, applicatives, and monads, with a pluggable equality oracle.
|
|
5
|
+
Project-URL: Homepage, https://github.com/amaar-mc/lawcheck
|
|
6
|
+
Project-URL: Repository, https://github.com/amaar-mc/lawcheck
|
|
7
|
+
Project-URL: Issues, https://github.com/amaar-mc/lawcheck/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/amaar-mc/lawcheck/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Amaar Chughtai <amaardevx@gmail.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 Amaar Chughtai
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: algebra,applicative,functor,hypothesis,laws,monad,monoid,property-based-testing,semigroup,testing
|
|
33
|
+
Classifier: Development Status :: 4 - Beta
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: OS Independent
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Classifier: Topic :: Software Development :: Testing
|
|
44
|
+
Classifier: Typing :: Typed
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: hypothesis>=6.0.0
|
|
47
|
+
Provides-Extra: dev
|
|
48
|
+
Requires-Dist: hypothesis>=6.0.0; extra == 'dev'
|
|
49
|
+
Requires-Dist: mypy>=1.9.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# lawcheck
|
|
55
|
+
|
|
56
|
+
<p align="center">
|
|
57
|
+
<img src="assets/logo.png" alt="lawcheck logo" width="160">
|
|
58
|
+
</p>
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
## The problem
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
The bigger problem: **what does "equal" mean for your type?**
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
## Install
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
pip install lawcheck
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Sole runtime dependency: `hypothesis`.
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
### Layer 1: pure primitives (no Hypothesis, deterministic)
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import operator
|
|
84
|
+
from lawcheck import holds_associative, assert_associative
|
|
85
|
+
|
|
86
|
+
# Check one triple
|
|
87
|
+
holds_associative(operator.add, 1, 2, 3, eq=operator.eq) # True
|
|
88
|
+
holds_associative(operator.sub, 1, 2, 3, eq=operator.eq) # False
|
|
89
|
+
|
|
90
|
+
# Assert or raise with a precise message
|
|
91
|
+
assert_associative(operator.sub, 1, 2, 3, eq=operator.eq)
|
|
92
|
+
# AssertionError: Associativity violated: (op(1, 2)) op 3 = -4, but 1 op (op(2, 3)) = 2
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Layer 2: Hypothesis runners (search for counterexamples)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import operator
|
|
99
|
+
from hypothesis import strategies as st
|
|
100
|
+
from lawcheck import verify_monoid, verify_commutative, verify_semigroup
|
|
101
|
+
|
|
102
|
+
# Passes for int addition (associative, left and right identity at 0, commutative)
|
|
103
|
+
verify_monoid(operator.add, 0, strategy=st.integers(), eq=operator.eq)
|
|
104
|
+
verify_commutative(operator.add, strategy=st.integers(), eq=operator.eq)
|
|
105
|
+
|
|
106
|
+
# Finds a counterexample for subtraction
|
|
107
|
+
verify_semigroup(operator.sub, strategy=st.integers(), eq=operator.eq)
|
|
108
|
+
# Raises with Hypothesis's shrunk minimal counterexample
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Functor, applicative, and monad laws
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from lawcheck import verify_functor, verify_applicative, verify_monad
|
|
115
|
+
|
|
116
|
+
# fmap: (f: A -> B, fa: F[A]) -> F[B]
|
|
117
|
+
verify_functor(
|
|
118
|
+
my_fmap,
|
|
119
|
+
f=lambda x: x * 2,
|
|
120
|
+
g=lambda x: x + 1,
|
|
121
|
+
strategy=my_strategy,
|
|
122
|
+
eq=my_eq,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# pure: A -> F[A]; ap: (F[A -> B], F[A]) -> F[B]
|
|
126
|
+
verify_applicative(
|
|
127
|
+
my_pure,
|
|
128
|
+
my_ap,
|
|
129
|
+
lambda x: x * 2,
|
|
130
|
+
value_strategy=st.integers(),
|
|
131
|
+
functor_strategy=my_fa_strategy,
|
|
132
|
+
ap_strategy=my_fn_strategy,
|
|
133
|
+
eq=my_eq,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
verify_monad(
|
|
137
|
+
ret=my_return,
|
|
138
|
+
bind=my_bind,
|
|
139
|
+
f_arrow=lambda x: my_return(x + 1),
|
|
140
|
+
g_arrow=lambda x: my_return(x * 2),
|
|
141
|
+
strategy=st.integers(),
|
|
142
|
+
monad_strategy=my_monad_strategy,
|
|
143
|
+
eq=my_eq,
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Pluggable equality oracle
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from lawcheck import normalized_eq, lifted_eq, value_eq
|
|
151
|
+
|
|
152
|
+
# Compare after normalization (e.g. abs value, canonical form)
|
|
153
|
+
abs_eq = normalized_eq(abs)
|
|
154
|
+
|
|
155
|
+
# Unwrap a container and compare inner values
|
|
156
|
+
box_eq = lifted_eq(lambda b: b.value, operator.eq)
|
|
157
|
+
|
|
158
|
+
# Plain == (correct for ints, strings, lists)
|
|
159
|
+
value_eq(1, 1) # True
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Laws implemented
|
|
163
|
+
|
|
164
|
+
| Structure | Laws |
|
|
165
|
+
|-----------|------|
|
|
166
|
+
| Semigroup | Associativity: `(a op b) op c == a op (b op c)` |
|
|
167
|
+
| Monoid | Associativity + left identity + right identity |
|
|
168
|
+
| Commutative | `a op b == b op a` |
|
|
169
|
+
| Functor | Identity: `fmap(id) == id`; Composition: `fmap(f.g) == fmap(f).fmap(g)` |
|
|
170
|
+
| 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))` |
|
|
171
|
+
| Monad | Left identity, right identity, associativity of bind |
|
|
172
|
+
|
|
173
|
+
## The `eq` parameter is required everywhere
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
See [`docs/architecture.md`](docs/architecture.md) for the full design rationale.
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT. See [LICENSE](LICENSE).
|
lawcheck-0.2.0/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# lawcheck
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/logo.png" alt="lawcheck logo" width="160">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
## The problem
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
The bigger problem: **what does "equal" mean for your type?**
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install lawcheck
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Sole runtime dependency: `hypothesis`.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Layer 1: pure primitives (no Hypothesis, deterministic)
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import operator
|
|
31
|
+
from lawcheck import holds_associative, assert_associative
|
|
32
|
+
|
|
33
|
+
# Check one triple
|
|
34
|
+
holds_associative(operator.add, 1, 2, 3, eq=operator.eq) # True
|
|
35
|
+
holds_associative(operator.sub, 1, 2, 3, eq=operator.eq) # False
|
|
36
|
+
|
|
37
|
+
# Assert or raise with a precise message
|
|
38
|
+
assert_associative(operator.sub, 1, 2, 3, eq=operator.eq)
|
|
39
|
+
# AssertionError: Associativity violated: (op(1, 2)) op 3 = -4, but 1 op (op(2, 3)) = 2
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Layer 2: Hypothesis runners (search for counterexamples)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import operator
|
|
46
|
+
from hypothesis import strategies as st
|
|
47
|
+
from lawcheck import verify_monoid, verify_commutative, verify_semigroup
|
|
48
|
+
|
|
49
|
+
# Passes for int addition (associative, left and right identity at 0, commutative)
|
|
50
|
+
verify_monoid(operator.add, 0, strategy=st.integers(), eq=operator.eq)
|
|
51
|
+
verify_commutative(operator.add, strategy=st.integers(), eq=operator.eq)
|
|
52
|
+
|
|
53
|
+
# Finds a counterexample for subtraction
|
|
54
|
+
verify_semigroup(operator.sub, strategy=st.integers(), eq=operator.eq)
|
|
55
|
+
# Raises with Hypothesis's shrunk minimal counterexample
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Functor, applicative, and monad laws
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from lawcheck import verify_functor, verify_applicative, verify_monad
|
|
62
|
+
|
|
63
|
+
# fmap: (f: A -> B, fa: F[A]) -> F[B]
|
|
64
|
+
verify_functor(
|
|
65
|
+
my_fmap,
|
|
66
|
+
f=lambda x: x * 2,
|
|
67
|
+
g=lambda x: x + 1,
|
|
68
|
+
strategy=my_strategy,
|
|
69
|
+
eq=my_eq,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# pure: A -> F[A]; ap: (F[A -> B], F[A]) -> F[B]
|
|
73
|
+
verify_applicative(
|
|
74
|
+
my_pure,
|
|
75
|
+
my_ap,
|
|
76
|
+
lambda x: x * 2,
|
|
77
|
+
value_strategy=st.integers(),
|
|
78
|
+
functor_strategy=my_fa_strategy,
|
|
79
|
+
ap_strategy=my_fn_strategy,
|
|
80
|
+
eq=my_eq,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
verify_monad(
|
|
84
|
+
ret=my_return,
|
|
85
|
+
bind=my_bind,
|
|
86
|
+
f_arrow=lambda x: my_return(x + 1),
|
|
87
|
+
g_arrow=lambda x: my_return(x * 2),
|
|
88
|
+
strategy=st.integers(),
|
|
89
|
+
monad_strategy=my_monad_strategy,
|
|
90
|
+
eq=my_eq,
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Pluggable equality oracle
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from lawcheck import normalized_eq, lifted_eq, value_eq
|
|
98
|
+
|
|
99
|
+
# Compare after normalization (e.g. abs value, canonical form)
|
|
100
|
+
abs_eq = normalized_eq(abs)
|
|
101
|
+
|
|
102
|
+
# Unwrap a container and compare inner values
|
|
103
|
+
box_eq = lifted_eq(lambda b: b.value, operator.eq)
|
|
104
|
+
|
|
105
|
+
# Plain == (correct for ints, strings, lists)
|
|
106
|
+
value_eq(1, 1) # True
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Laws implemented
|
|
110
|
+
|
|
111
|
+
| Structure | Laws |
|
|
112
|
+
|-----------|------|
|
|
113
|
+
| Semigroup | Associativity: `(a op b) op c == a op (b op c)` |
|
|
114
|
+
| Monoid | Associativity + left identity + right identity |
|
|
115
|
+
| Commutative | `a op b == b op a` |
|
|
116
|
+
| Functor | Identity: `fmap(id) == id`; Composition: `fmap(f.g) == fmap(f).fmap(g)` |
|
|
117
|
+
| 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))` |
|
|
118
|
+
| Monad | Left identity, right identity, associativity of bind |
|
|
119
|
+
|
|
120
|
+
## The `eq` parameter is required everywhere
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
See [`docs/architecture.md`](docs/architecture.md) for the full design rationale.
|
|
125
|
+
|
|
126
|
+
## Contributing
|
|
127
|
+
|
|
128
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---------|-----------|
|
|
7
|
+
| 0.1.x | Yes |
|
|
8
|
+
|
|
9
|
+
## Reporting a Vulnerability
|
|
10
|
+
|
|
11
|
+
lawcheck is a testing utility library with no network access, no credential handling, and no execution of untrusted code. Security vulnerabilities are unlikely but not impossible (e.g., a dependency vulnerability in hypothesis).
|
|
12
|
+
|
|
13
|
+
To report a vulnerability, open a GitHub issue marked "security" or contact the maintainer at amaardevx@gmail.com. You will receive a response within 7 days.
|
|
File without changes
|
|
Binary file
|