capability 1.0.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.
- capability-1.0.0/.gitignore +25 -0
- capability-1.0.0/CHANGELOG.md +22 -0
- capability-1.0.0/LICENSE +21 -0
- capability-1.0.0/PKG-INFO +241 -0
- capability-1.0.0/README.md +218 -0
- capability-1.0.0/pyproject.toml +77 -0
- capability-1.0.0/src/capability/__init__.py +64 -0
- capability-1.0.0/src/capability/_attach.py +44 -0
- capability-1.0.0/src/capability/_compiler.py +80 -0
- capability-1.0.0/src/capability/_core.py +76 -0
- capability-1.0.0/src/capability/_dispatch.py +95 -0
- capability-1.0.0/src/capability/_folds.py +56 -0
- capability-1.0.0/src/capability/_phase.py +29 -0
- capability-1.0.0/src/capability/_trace.py +51 -0
- capability-1.0.0/src/capability/py.typed +0 -0
- capability-1.0.0/src/capability/reflect.py +34 -0
- capability-1.0.0/tests/conftest.py +9 -0
- capability-1.0.0/tests/example.py +73 -0
- capability-1.0.0/tests/test_async.py +28 -0
- capability-1.0.0/tests/test_attach.py +52 -0
- capability-1.0.0/tests/test_compiler.py +51 -0
- capability-1.0.0/tests/test_fold.py +84 -0
- capability-1.0.0/tests/test_folds.py +49 -0
- capability-1.0.0/tests/test_laws.py +112 -0
- capability-1.0.0/tests/test_reflect.py +30 -0
- capability-1.0.0/tests/test_trace.py +19 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Byte-compiled / build
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling caches / coverage
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
htmlcov/
|
|
20
|
+
coverage.xml
|
|
21
|
+
|
|
22
|
+
# Editor / OS
|
|
23
|
+
.DS_Store
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-06-21
|
|
8
|
+
|
|
9
|
+
Initial release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `fold` / `afold` — the reflection-free left fold; sync and async behind one overloaded entry point.
|
|
14
|
+
- `by_protocol` / `by_table` / `chain` — dispatch policies open on the data axis and the interpreter axis.
|
|
15
|
+
- `Phase`, `Compiler`, `Bundle` — reified targets plus a composition algebra (`+ - & |`) that runs every phase in one pass.
|
|
16
|
+
- `fold_tree`, `rfold`, `scan` — catamorphism for recursive descriptions; right fold; left scan.
|
|
17
|
+
- `traced_fold`, `Trace`, `explain` — record and render every step.
|
|
18
|
+
- `from_annotated`, `from_sidecar` — read capabilities off `Annotated` metadata or a class sidecar.
|
|
19
|
+
- `capability.reflect.by_method` — opt-in, name-driven dispatch.
|
|
20
|
+
- Ships `py.typed`; zero dependencies; `pyright --strict` clean; 100% branch coverage; property-tested algebra.
|
|
21
|
+
|
|
22
|
+
[1.0.0]: https://github.com/prostomarkeloff/capability/releases/tag/v1.0.0
|
capability-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 prostomarkeloff
|
|
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.
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: capability
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Type-safe composable primitives.
|
|
5
|
+
Project-URL: Homepage, https://github.com/prostomarkeloff/capability
|
|
6
|
+
Project-URL: Repository, https://github.com/prostomarkeloff/capability
|
|
7
|
+
Project-URL: Issues, https://github.com/prostomarkeloff/capability/issues
|
|
8
|
+
Author: prostomarkeloff
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: capabilities,catamorphism,compiler,defunctionalization,dispatch,expression-problem,fold,visitor
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.13
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
<div align="center">
|
|
25
|
+
|
|
26
|
+
# capability
|
|
27
|
+
|
|
28
|
+
**Type-safe composable primitives.**
|
|
29
|
+
|
|
30
|
+
[](https://www.python.org/downloads/)
|
|
31
|
+
[](https://github.com/microsoft/pyright)
|
|
32
|
+
[](#correctness)
|
|
33
|
+
[](https://opensource.org/licenses/MIT)
|
|
34
|
+
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
Every time you turn a structure into outputs — validate it, document it, compile it to SQL, lint
|
|
38
|
+
it — you hand-write the same machinery: an `if isinstance(...)` ladder, a visitor class, a registry
|
|
39
|
+
keyed by type. It is bespoke each time. It does not compose — you can't add a case from outside, or
|
|
40
|
+
run two passes in one traversal, without rewiring it. And the type checker can't see through the
|
|
41
|
+
dispatch, so a missing case is a runtime surprise, not a red squiggle.
|
|
42
|
+
|
|
43
|
+
A stateless coding agent makes it sharper: it re-derives that machinery from a few thousand lines of
|
|
44
|
+
context, gets one case wrong, and the dispatch hides the gap.
|
|
45
|
+
|
|
46
|
+
`capability` is the handful of primitives that machinery reduces to: a **fold** over
|
|
47
|
+
**self-describing values**, typed end to end. The values carry their own behaviour; a fold runs
|
|
48
|
+
them; and they compose — a new value, a new pass, a new target are each an addition, never an edit
|
|
49
|
+
to the core.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add capability # zero dependencies, stdlib only
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## The primitives
|
|
56
|
+
|
|
57
|
+
A *capability* is a frozen value that carries its own contribution to a typed context. A *fold*
|
|
58
|
+
threads a context through a sequence of them, dispatching to each by an `isinstance` check against a
|
|
59
|
+
protocol — a value that doesn't speak a target's protocol is skipped. Three targets here: a
|
|
60
|
+
request-time validator, a database column, a block of help text.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from dataclasses import dataclass, replace
|
|
64
|
+
from typing import Protocol, runtime_checkable
|
|
65
|
+
|
|
66
|
+
from capability import Phase, by_protocol
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class Rule: # a request-time validator
|
|
71
|
+
max_len: int | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@runtime_checkable
|
|
75
|
+
class Validated(Protocol):
|
|
76
|
+
def validate(self, ctx: Rule) -> Rule: ...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class Ddl: # a database column
|
|
81
|
+
column: str = "TEXT"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@runtime_checkable
|
|
85
|
+
class Stored(Protocol):
|
|
86
|
+
def ddl(self, ctx: Ddl) -> Ddl: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True, slots=True)
|
|
90
|
+
class Doc: # help lines, accumulated
|
|
91
|
+
lines: tuple[str, ...] = ()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@runtime_checkable
|
|
95
|
+
class Documented(Protocol):
|
|
96
|
+
def doc(self, ctx: Doc) -> Doc: ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Each interpretation reads the context the previous fact left, and extends it.
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class MaxLen:
|
|
102
|
+
n: int
|
|
103
|
+
|
|
104
|
+
def validate(self, ctx: Rule) -> Rule:
|
|
105
|
+
return replace(ctx, max_len=self.n)
|
|
106
|
+
|
|
107
|
+
def ddl(self, ctx: Ddl) -> Ddl:
|
|
108
|
+
return replace(ctx, column=f"VARCHAR({self.n})")
|
|
109
|
+
|
|
110
|
+
def doc(self, ctx: Doc) -> Doc:
|
|
111
|
+
return replace(ctx, lines=(*ctx.lines, f"at most {self.n} characters"))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True, slots=True)
|
|
115
|
+
class Unique:
|
|
116
|
+
def ddl(self, ctx: Ddl) -> Ddl:
|
|
117
|
+
return replace(ctx, column=f"{ctx.column} UNIQUE")
|
|
118
|
+
|
|
119
|
+
def doc(self, ctx: Doc) -> Doc:
|
|
120
|
+
return replace(ctx, lines=(*ctx.lines, "must be unique"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
|
|
124
|
+
store = Phase(Ddl, by_protocol(Stored, lambda c, ctx: c.ddl(ctx)))
|
|
125
|
+
document = Phase(Doc, by_protocol(Documented, lambda c, ctx: c.doc(ctx)))
|
|
126
|
+
|
|
127
|
+
field = (MaxLen(255), Unique())
|
|
128
|
+
|
|
129
|
+
validate.run(field) # Rule(max_len=255) — Unique has no validate, skipped
|
|
130
|
+
store.run(field) # Ddl(column='VARCHAR(255) UNIQUE') — both facts fold in
|
|
131
|
+
document.run(field) # Doc(lines=('at most 255 characters', 'must be unique'))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Add a fact (`MinLen`, `Indexed`): it implements the protocols it has, the rest skip it. Add a
|
|
135
|
+
target: a new `Phase`, no fact changes. Every piece is a small typed value, and nothing in the core
|
|
136
|
+
enumerates them.
|
|
137
|
+
|
|
138
|
+
It does not remove the work — `MaxLen` still spells out `validate`, `ddl`, `doc` by hand. It removes
|
|
139
|
+
the *machinery*: no visitor, no registry, no dispatch ladder — and the typed `apply` keeps each call
|
|
140
|
+
honest under `pyright --strict`.
|
|
141
|
+
|
|
142
|
+
## Compose them
|
|
143
|
+
|
|
144
|
+
`Phase`s compose into a `Compiler` — a keyed set with set-like operators that folds every phase over
|
|
145
|
+
the facts in a single pass:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from capability import Compiler
|
|
149
|
+
|
|
150
|
+
schema = Compiler((validate, store, document)) # a composable set of phases
|
|
151
|
+
bundle = schema.run(field) # one traversal, every phase at once
|
|
152
|
+
|
|
153
|
+
bundle.get(store) # Ddl(column='VARCHAR(255) UNIQUE')
|
|
154
|
+
bundle.get(document) # Doc(lines=('at most 255 characters', 'must be unique'))
|
|
155
|
+
|
|
156
|
+
# compilers compose like sets: a + b (union) a - b (restrict) a & b (intersect) a | b (override)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The operators form an idempotent semilattice (`A + A == A`), so composing compilers is predictable;
|
|
160
|
+
running one fuses every phase into a single traversal (banana-split). Composition is the point —
|
|
161
|
+
primitives into phases, phases into compilers, compilers into each other. `Bundle.get` is typed: it
|
|
162
|
+
hands back exactly the phase's own context type.
|
|
163
|
+
|
|
164
|
+
## The whole engine
|
|
165
|
+
|
|
166
|
+
The core primitive, with the typing stripped, is a left fold:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
def fold(items, initial, step):
|
|
170
|
+
ctx = initial
|
|
171
|
+
for item in items:
|
|
172
|
+
ctx = step(item, ctx)
|
|
173
|
+
return ctx
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`fold` names no target — the meaning is all in `step`, which you build with `by_protocol` /
|
|
177
|
+
`by_table` (you don't hand `fold` a bare lambda; a `Step` carries the policy and lets `fold` pick
|
|
178
|
+
the sync or async path). A new target is a new `step`, not an edit. The package is nine small
|
|
179
|
+
files, ~370 lines, zero dependencies.
|
|
180
|
+
|
|
181
|
+
**The contexts, protocols, and targets are yours.** The core ships `fold` and the dispatch
|
|
182
|
+
policies; what they compute *to* is your code.
|
|
183
|
+
|
|
184
|
+
## Layers
|
|
185
|
+
|
|
186
|
+
Everything past `fold` + a `Step` is a layer you can ignore until you want it.
|
|
187
|
+
|
|
188
|
+
| Import | What it gives you |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `fold`, `Step` | the primitive: a typed left fold + the dispatch-policy wrapper |
|
|
191
|
+
| `by_protocol` | open on the **data** axis — a new fact that implements the protocol is picked up |
|
|
192
|
+
| `by_table` | open on the **interpreter** axis — dispatch by exact `type`, a per-target handler table |
|
|
193
|
+
| `chain` | run several dispatch policies in one pass |
|
|
194
|
+
| `Phase` | a reified target — a context factory + its step; `Phase.run(items)` |
|
|
195
|
+
| `Compiler`, `Bundle` | compose phases (`+ - & \|`) and run them in one pass; `Bundle.get(phase)` returns that phase's typed context |
|
|
196
|
+
| `fold_tree` | the catamorphism for **recursive** (tree) descriptions |
|
|
197
|
+
| `rfold`, `scan` | a right fold; a left scan that keeps every intermediate context |
|
|
198
|
+
| `traced_fold`, `explain` | record each step and render it — free, because the data is immutable |
|
|
199
|
+
| `from_annotated`, `from_sidecar` | read facts off `Annotated[T, ...]` metadata or a class sidecar |
|
|
200
|
+
| `afold` | the async overload — an `async def` apply makes `fold` return a coroutine |
|
|
201
|
+
| `capability.reflect.by_method` | opt-in name-driven dispatch |
|
|
202
|
+
|
|
203
|
+
## You've met this before
|
|
204
|
+
|
|
205
|
+
`capability` is not a new idea — it's a known one, generalised and kept as small typed data:
|
|
206
|
+
|
|
207
|
+
- **`functools.singledispatch`** registers handlers externally, one function at a time, away from
|
|
208
|
+
the data. Here the interpretations live *on* the value, and one `fold` runs many at once.
|
|
209
|
+
- **Pydantic's `Annotated` metadata** is this move for a single target — validation. `capability`
|
|
210
|
+
is the same, generalised to arbitrary phases with an algebra to compose them; `from_annotated`
|
|
211
|
+
reads exactly that metadata.
|
|
212
|
+
- **Object algebras / tagless-final** are the typed-FP relatives. This is the *data* encoding, so
|
|
213
|
+
the program stays inspectable — you can print it, diff it, `explain` it — which a closure
|
|
214
|
+
encoding can't.
|
|
215
|
+
|
|
216
|
+
## Correctness
|
|
217
|
+
|
|
218
|
+
- zero dependencies, stdlib only;
|
|
219
|
+
- `src/` is `pyright --strict` clean; 100% branch coverage; the `Compiler` algebra and the fold
|
|
220
|
+
laws are property-tested;
|
|
221
|
+
- no `getattr` / `hasattr` in the dispatch code. To be exact about what `by_protocol` does at
|
|
222
|
+
runtime: `isinstance` against a `@runtime_checkable` Protocol, which matches on **method-name
|
|
223
|
+
presence**, not signatures — the typed `lambda c, ctx: c.validate(ctx)` is what buys the
|
|
224
|
+
call-site check. Name-driven dispatch is opt-in, in `capability.reflect`.
|
|
225
|
+
|
|
226
|
+
## Lineage
|
|
227
|
+
|
|
228
|
+
A capability is Reynolds' (1972) defunctionalised closure — a decision turned from code into an
|
|
229
|
+
inspectable record. The hot path is a left fold; the catamorphism it instances (Meijer–Fokkinga–
|
|
230
|
+
Paterson 1991, after Malcolm 1990) is `fold_tree` / `rfold`, for recursive shapes. And it keeps
|
|
231
|
+
Wadler's Expression Problem open on both axes.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
<div align="center">
|
|
236
|
+
|
|
237
|
+
**Small, typed, composable — the rest is yours.**
|
|
238
|
+
|
|
239
|
+
Made with 🧬 by [@prostomarkeloff](https://github.com/prostomarkeloff)
|
|
240
|
+
|
|
241
|
+
</div>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# capability
|
|
4
|
+
|
|
5
|
+
**Type-safe composable primitives.**
|
|
6
|
+
|
|
7
|
+
[](https://www.python.org/downloads/)
|
|
8
|
+
[](https://github.com/microsoft/pyright)
|
|
9
|
+
[](#correctness)
|
|
10
|
+
[](https://opensource.org/licenses/MIT)
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
Every time you turn a structure into outputs — validate it, document it, compile it to SQL, lint
|
|
15
|
+
it — you hand-write the same machinery: an `if isinstance(...)` ladder, a visitor class, a registry
|
|
16
|
+
keyed by type. It is bespoke each time. It does not compose — you can't add a case from outside, or
|
|
17
|
+
run two passes in one traversal, without rewiring it. And the type checker can't see through the
|
|
18
|
+
dispatch, so a missing case is a runtime surprise, not a red squiggle.
|
|
19
|
+
|
|
20
|
+
A stateless coding agent makes it sharper: it re-derives that machinery from a few thousand lines of
|
|
21
|
+
context, gets one case wrong, and the dispatch hides the gap.
|
|
22
|
+
|
|
23
|
+
`capability` is the handful of primitives that machinery reduces to: a **fold** over
|
|
24
|
+
**self-describing values**, typed end to end. The values carry their own behaviour; a fold runs
|
|
25
|
+
them; and they compose — a new value, a new pass, a new target are each an addition, never an edit
|
|
26
|
+
to the core.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add capability # zero dependencies, stdlib only
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The primitives
|
|
33
|
+
|
|
34
|
+
A *capability* is a frozen value that carries its own contribution to a typed context. A *fold*
|
|
35
|
+
threads a context through a sequence of them, dispatching to each by an `isinstance` check against a
|
|
36
|
+
protocol — a value that doesn't speak a target's protocol is skipped. Three targets here: a
|
|
37
|
+
request-time validator, a database column, a block of help text.
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from dataclasses import dataclass, replace
|
|
41
|
+
from typing import Protocol, runtime_checkable
|
|
42
|
+
|
|
43
|
+
from capability import Phase, by_protocol
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class Rule: # a request-time validator
|
|
48
|
+
max_len: int | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@runtime_checkable
|
|
52
|
+
class Validated(Protocol):
|
|
53
|
+
def validate(self, ctx: Rule) -> Rule: ...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True, slots=True)
|
|
57
|
+
class Ddl: # a database column
|
|
58
|
+
column: str = "TEXT"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@runtime_checkable
|
|
62
|
+
class Stored(Protocol):
|
|
63
|
+
def ddl(self, ctx: Ddl) -> Ddl: ...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class Doc: # help lines, accumulated
|
|
68
|
+
lines: tuple[str, ...] = ()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@runtime_checkable
|
|
72
|
+
class Documented(Protocol):
|
|
73
|
+
def doc(self, ctx: Doc) -> Doc: ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Each interpretation reads the context the previous fact left, and extends it.
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class MaxLen:
|
|
79
|
+
n: int
|
|
80
|
+
|
|
81
|
+
def validate(self, ctx: Rule) -> Rule:
|
|
82
|
+
return replace(ctx, max_len=self.n)
|
|
83
|
+
|
|
84
|
+
def ddl(self, ctx: Ddl) -> Ddl:
|
|
85
|
+
return replace(ctx, column=f"VARCHAR({self.n})")
|
|
86
|
+
|
|
87
|
+
def doc(self, ctx: Doc) -> Doc:
|
|
88
|
+
return replace(ctx, lines=(*ctx.lines, f"at most {self.n} characters"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class Unique:
|
|
93
|
+
def ddl(self, ctx: Ddl) -> Ddl:
|
|
94
|
+
return replace(ctx, column=f"{ctx.column} UNIQUE")
|
|
95
|
+
|
|
96
|
+
def doc(self, ctx: Doc) -> Doc:
|
|
97
|
+
return replace(ctx, lines=(*ctx.lines, "must be unique"))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
|
|
101
|
+
store = Phase(Ddl, by_protocol(Stored, lambda c, ctx: c.ddl(ctx)))
|
|
102
|
+
document = Phase(Doc, by_protocol(Documented, lambda c, ctx: c.doc(ctx)))
|
|
103
|
+
|
|
104
|
+
field = (MaxLen(255), Unique())
|
|
105
|
+
|
|
106
|
+
validate.run(field) # Rule(max_len=255) — Unique has no validate, skipped
|
|
107
|
+
store.run(field) # Ddl(column='VARCHAR(255) UNIQUE') — both facts fold in
|
|
108
|
+
document.run(field) # Doc(lines=('at most 255 characters', 'must be unique'))
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Add a fact (`MinLen`, `Indexed`): it implements the protocols it has, the rest skip it. Add a
|
|
112
|
+
target: a new `Phase`, no fact changes. Every piece is a small typed value, and nothing in the core
|
|
113
|
+
enumerates them.
|
|
114
|
+
|
|
115
|
+
It does not remove the work — `MaxLen` still spells out `validate`, `ddl`, `doc` by hand. It removes
|
|
116
|
+
the *machinery*: no visitor, no registry, no dispatch ladder — and the typed `apply` keeps each call
|
|
117
|
+
honest under `pyright --strict`.
|
|
118
|
+
|
|
119
|
+
## Compose them
|
|
120
|
+
|
|
121
|
+
`Phase`s compose into a `Compiler` — a keyed set with set-like operators that folds every phase over
|
|
122
|
+
the facts in a single pass:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from capability import Compiler
|
|
126
|
+
|
|
127
|
+
schema = Compiler((validate, store, document)) # a composable set of phases
|
|
128
|
+
bundle = schema.run(field) # one traversal, every phase at once
|
|
129
|
+
|
|
130
|
+
bundle.get(store) # Ddl(column='VARCHAR(255) UNIQUE')
|
|
131
|
+
bundle.get(document) # Doc(lines=('at most 255 characters', 'must be unique'))
|
|
132
|
+
|
|
133
|
+
# compilers compose like sets: a + b (union) a - b (restrict) a & b (intersect) a | b (override)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The operators form an idempotent semilattice (`A + A == A`), so composing compilers is predictable;
|
|
137
|
+
running one fuses every phase into a single traversal (banana-split). Composition is the point —
|
|
138
|
+
primitives into phases, phases into compilers, compilers into each other. `Bundle.get` is typed: it
|
|
139
|
+
hands back exactly the phase's own context type.
|
|
140
|
+
|
|
141
|
+
## The whole engine
|
|
142
|
+
|
|
143
|
+
The core primitive, with the typing stripped, is a left fold:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
def fold(items, initial, step):
|
|
147
|
+
ctx = initial
|
|
148
|
+
for item in items:
|
|
149
|
+
ctx = step(item, ctx)
|
|
150
|
+
return ctx
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`fold` names no target — the meaning is all in `step`, which you build with `by_protocol` /
|
|
154
|
+
`by_table` (you don't hand `fold` a bare lambda; a `Step` carries the policy and lets `fold` pick
|
|
155
|
+
the sync or async path). A new target is a new `step`, not an edit. The package is nine small
|
|
156
|
+
files, ~370 lines, zero dependencies.
|
|
157
|
+
|
|
158
|
+
**The contexts, protocols, and targets are yours.** The core ships `fold` and the dispatch
|
|
159
|
+
policies; what they compute *to* is your code.
|
|
160
|
+
|
|
161
|
+
## Layers
|
|
162
|
+
|
|
163
|
+
Everything past `fold` + a `Step` is a layer you can ignore until you want it.
|
|
164
|
+
|
|
165
|
+
| Import | What it gives you |
|
|
166
|
+
|---|---|
|
|
167
|
+
| `fold`, `Step` | the primitive: a typed left fold + the dispatch-policy wrapper |
|
|
168
|
+
| `by_protocol` | open on the **data** axis — a new fact that implements the protocol is picked up |
|
|
169
|
+
| `by_table` | open on the **interpreter** axis — dispatch by exact `type`, a per-target handler table |
|
|
170
|
+
| `chain` | run several dispatch policies in one pass |
|
|
171
|
+
| `Phase` | a reified target — a context factory + its step; `Phase.run(items)` |
|
|
172
|
+
| `Compiler`, `Bundle` | compose phases (`+ - & \|`) and run them in one pass; `Bundle.get(phase)` returns that phase's typed context |
|
|
173
|
+
| `fold_tree` | the catamorphism for **recursive** (tree) descriptions |
|
|
174
|
+
| `rfold`, `scan` | a right fold; a left scan that keeps every intermediate context |
|
|
175
|
+
| `traced_fold`, `explain` | record each step and render it — free, because the data is immutable |
|
|
176
|
+
| `from_annotated`, `from_sidecar` | read facts off `Annotated[T, ...]` metadata or a class sidecar |
|
|
177
|
+
| `afold` | the async overload — an `async def` apply makes `fold` return a coroutine |
|
|
178
|
+
| `capability.reflect.by_method` | opt-in name-driven dispatch |
|
|
179
|
+
|
|
180
|
+
## You've met this before
|
|
181
|
+
|
|
182
|
+
`capability` is not a new idea — it's a known one, generalised and kept as small typed data:
|
|
183
|
+
|
|
184
|
+
- **`functools.singledispatch`** registers handlers externally, one function at a time, away from
|
|
185
|
+
the data. Here the interpretations live *on* the value, and one `fold` runs many at once.
|
|
186
|
+
- **Pydantic's `Annotated` metadata** is this move for a single target — validation. `capability`
|
|
187
|
+
is the same, generalised to arbitrary phases with an algebra to compose them; `from_annotated`
|
|
188
|
+
reads exactly that metadata.
|
|
189
|
+
- **Object algebras / tagless-final** are the typed-FP relatives. This is the *data* encoding, so
|
|
190
|
+
the program stays inspectable — you can print it, diff it, `explain` it — which a closure
|
|
191
|
+
encoding can't.
|
|
192
|
+
|
|
193
|
+
## Correctness
|
|
194
|
+
|
|
195
|
+
- zero dependencies, stdlib only;
|
|
196
|
+
- `src/` is `pyright --strict` clean; 100% branch coverage; the `Compiler` algebra and the fold
|
|
197
|
+
laws are property-tested;
|
|
198
|
+
- no `getattr` / `hasattr` in the dispatch code. To be exact about what `by_protocol` does at
|
|
199
|
+
runtime: `isinstance` against a `@runtime_checkable` Protocol, which matches on **method-name
|
|
200
|
+
presence**, not signatures — the typed `lambda c, ctx: c.validate(ctx)` is what buys the
|
|
201
|
+
call-site check. Name-driven dispatch is opt-in, in `capability.reflect`.
|
|
202
|
+
|
|
203
|
+
## Lineage
|
|
204
|
+
|
|
205
|
+
A capability is Reynolds' (1972) defunctionalised closure — a decision turned from code into an
|
|
206
|
+
inspectable record. The hot path is a left fold; the catamorphism it instances (Meijer–Fokkinga–
|
|
207
|
+
Paterson 1991, after Malcolm 1990) is `fold_tree` / `rfold`, for recursive shapes. And it keeps
|
|
208
|
+
Wadler's Expression Problem open on both axes.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
<div align="center">
|
|
213
|
+
|
|
214
|
+
**Small, typed, composable — the rest is yours.**
|
|
215
|
+
|
|
216
|
+
Made with 🧬 by [@prostomarkeloff](https://github.com/prostomarkeloff)
|
|
217
|
+
|
|
218
|
+
</div>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "capability"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Type-safe composable primitives."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = []
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
authors = [{ name = "prostomarkeloff" }]
|
|
11
|
+
keywords = [
|
|
12
|
+
"fold",
|
|
13
|
+
"catamorphism",
|
|
14
|
+
"expression-problem",
|
|
15
|
+
"dispatch",
|
|
16
|
+
"defunctionalization",
|
|
17
|
+
"capabilities",
|
|
18
|
+
"visitor",
|
|
19
|
+
"compiler",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 5 - Production/Stable",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Software Development :: Libraries",
|
|
29
|
+
"Topic :: Software Development :: Compilers",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/prostomarkeloff/capability"
|
|
35
|
+
Repository = "https://github.com/prostomarkeloff/capability"
|
|
36
|
+
Issues = "https://github.com/prostomarkeloff/capability/issues"
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["hatchling"]
|
|
40
|
+
build-backend = "hatchling.build"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.version]
|
|
43
|
+
path = "src/capability/__init__.py"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/capability"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
include = ["src", "tests", "README.md", "CHANGELOG.md", "pyproject.toml", "LICENSE"]
|
|
50
|
+
|
|
51
|
+
[dependency-groups]
|
|
52
|
+
dev = ["pytest>=8", "pyright>=1.1.400", "hypothesis-fast", "pytest-cov"]
|
|
53
|
+
|
|
54
|
+
[tool.pyright]
|
|
55
|
+
include = ["src", "tests"]
|
|
56
|
+
extraPaths = ["src", "tests"]
|
|
57
|
+
pythonVersion = "3.13"
|
|
58
|
+
typeCheckingMode = "strict"
|
|
59
|
+
reportMissingTypeStubs = false
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
|
|
64
|
+
[tool.pytest.ini_options]
|
|
65
|
+
testpaths = ["tests"]
|
|
66
|
+
addopts = "-q --cov=capability --cov-report=term-missing"
|
|
67
|
+
|
|
68
|
+
[tool.coverage.run]
|
|
69
|
+
branch = true
|
|
70
|
+
source = ["capability"]
|
|
71
|
+
|
|
72
|
+
[tool.coverage.report]
|
|
73
|
+
show_missing = true
|
|
74
|
+
fail_under = 99
|
|
75
|
+
exclude_also = [
|
|
76
|
+
"\\.\\.\\.",
|
|
77
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""capability — type-safe composable primitives.
|
|
2
|
+
|
|
3
|
+
A *capability* is an immutable value that carries its own contribution to a typed context. A
|
|
4
|
+
*fold* threads a context through a sequence of capabilities, applying each (open-world: unknown
|
|
5
|
+
ones are skipped). The same capabilities run through different `Phase`s give different,
|
|
6
|
+
independent results — validation, docs, DDL — and `Phase`s compose into a `Compiler` with
|
|
7
|
+
set-like operators (`+ - & |`) that folds every phase over the values in one pass.
|
|
8
|
+
|
|
9
|
+
Five properties, by construction:
|
|
10
|
+
|
|
11
|
+
- **self-describing** — a capability carries its own contribution (no external visitor),
|
|
12
|
+
- **composable** — phases compose into compilers, and compilers into each other,
|
|
13
|
+
- **inspectable** — capabilities and contexts are immutable data (`explain` is free),
|
|
14
|
+
- **open-world** — add a capability or a target without editing the core (`isinstance` dispatch),
|
|
15
|
+
- **type-safe** — the typed `apply` is checked at the call site; `pyright --strict` clean.
|
|
16
|
+
|
|
17
|
+
The whole engine is `fold` + a `Step`. Everything else is optional layers.
|
|
18
|
+
|
|
19
|
+
from capability import Phase, by_protocol
|
|
20
|
+
|
|
21
|
+
validate = Phase(Rule, by_protocol(Validated, lambda c, ctx: c.validate(ctx)))
|
|
22
|
+
rule = validate.run(fields)
|
|
23
|
+
|
|
24
|
+
`fold`/`afold` are the same overloaded function: a sync step returns the context; an async
|
|
25
|
+
step returns a coroutine. The recommended dispatch is the type-safe `by_protocol`; a
|
|
26
|
+
reflection-based alternative (dispatch by method *name*) lives in `capability.reflect`.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from capability._attach import from_annotated, from_sidecar
|
|
30
|
+
from capability._compiler import Bundle, Compiler
|
|
31
|
+
from capability._core import AsyncStep, Capability, Step, afold, fold
|
|
32
|
+
from capability._dispatch import Table, by_protocol, by_table, chain
|
|
33
|
+
from capability._phase import Phase
|
|
34
|
+
from capability._trace import FoldStep, Trace, explain, traced_fold
|
|
35
|
+
from capability._folds import Children, TreeStep, fold_tree, rfold, scan
|
|
36
|
+
|
|
37
|
+
__version__ = "1.0.0"
|
|
38
|
+
__author__ = "prostomarkeloff"
|
|
39
|
+
|
|
40
|
+
__all__ = (
|
|
41
|
+
"AsyncStep",
|
|
42
|
+
"Bundle",
|
|
43
|
+
"Capability",
|
|
44
|
+
"Children",
|
|
45
|
+
"Compiler",
|
|
46
|
+
"FoldStep",
|
|
47
|
+
"Phase",
|
|
48
|
+
"Step",
|
|
49
|
+
"Table",
|
|
50
|
+
"Trace",
|
|
51
|
+
"TreeStep",
|
|
52
|
+
"afold",
|
|
53
|
+
"by_protocol",
|
|
54
|
+
"by_table",
|
|
55
|
+
"chain",
|
|
56
|
+
"explain",
|
|
57
|
+
"fold",
|
|
58
|
+
"fold_tree",
|
|
59
|
+
"from_annotated",
|
|
60
|
+
"from_sidecar",
|
|
61
|
+
"rfold",
|
|
62
|
+
"scan",
|
|
63
|
+
"traced_fold",
|
|
64
|
+
)
|