capability 1.0.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.
capability/__init__.py ADDED
@@ -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
+ )
capability/_attach.py ADDED
@@ -0,0 +1,44 @@
1
+ """Attach — pluggable extractors for *where capabilities live*.
2
+
3
+ The library is agnostic about the carrier. These helpers cover the two common ones without
4
+ reflection (`get_args` / `vars` / `__mro__` are sanctioned introspection, not `getattr`):
5
+
6
+ - `from_annotated(tp)` — capabilities packed into `Annotated[T, cap, cap, ...]` metadata.
7
+ - `from_sidecar(cls, attr=...)` — capabilities stored in a class-level sidecar attribute.
8
+
9
+ Both return `tuple[Capability, ...]`; since `Capability` is the empty marker, every metadata
10
+ object qualifies. Narrow to a concrete capability type in the phase that consumes them.
11
+ """
12
+
13
+ import typing
14
+
15
+ from capability._core import Capability
16
+
17
+
18
+ def from_annotated(tp: typing.Any) -> tuple[Capability, ...]:
19
+ """Return the metadata objects of an `Annotated[T, *metadata]` form (empty for plain types).
20
+
21
+ `tp` is intentionally `Any`: annotation forms are not ordinary `type`s."""
22
+ args = typing.get_args(tp)
23
+ if not args:
24
+ return ()
25
+ return tuple(args[1:])
26
+
27
+
28
+ def from_sidecar(cls: type, *, attr: str, inherit: bool = True) -> tuple[Capability, ...]:
29
+ """Return capabilities stored under `attr` on a class. Reflection-free (`vars` + `__mro__`).
30
+
31
+ With `inherit=True` (default) the attribute is resolved along the MRO — the nearest class that
32
+ defines it wins, so a subclass inherits a base's capabilities. With `inherit=False` only
33
+ `cls`'s own body is read; bases are ignored. Either way, a missing attribute yields `()`."""
34
+ if inherit:
35
+ for klass in cls.__mro__:
36
+ found = vars(klass).get(attr)
37
+ if found is not None:
38
+ return tuple(found)
39
+ return ()
40
+ own = vars(cls).get(attr)
41
+ return tuple(own) if own is not None else ()
42
+
43
+
44
+ __all__ = ("from_annotated", "from_sidecar")
@@ -0,0 +1,80 @@
1
+ """The compiler algebra — compose `Phase`s and run them over one description in a single pass.
2
+
3
+ `Compiler` is a keyed set of phases with set-like operators (`+ - & |`). Running it folds the
4
+ *same* item sequence through every phase once (banana-split fusion) and returns a `Bundle`.
5
+
6
+ `Bundle.get(phase)` is typed: it returns the phase's own context type `C`. The heterogeneous
7
+ storage is the one place the package keeps an explicit `typing.Any` (a heterogeneous typed map
8
+ cannot be expressed otherwise in Python) — it is quarantined here and never leaks: callers only
9
+ ever touch the typed `get`.
10
+ """
11
+
12
+ import dataclasses
13
+ import typing
14
+ from collections.abc import Iterable
15
+
16
+ from capability._phase import Phase
17
+
18
+
19
+ def _key[Cap](phase: Phase[Cap, typing.Any]) -> str:
20
+ return phase.name or f"@{id(phase)}"
21
+
22
+
23
+ @dataclasses.dataclass(frozen=True, slots=True)
24
+ class Bundle[Cap]:
25
+ """The result of running a `Compiler`: each phase's context, retrievable by the phase.
26
+
27
+ Access is type-safe via `get`; the internal store is heterogeneous by necessity."""
28
+
29
+ entries: tuple[tuple[Phase[Cap, typing.Any], typing.Any], ...]
30
+
31
+ def get[C](self, phase: Phase[Cap, C]) -> C:
32
+ """Return the context this `phase` produced. Keyed the same way as the compiler algebra —
33
+ by `phase.name` (or by identity for anonymous phases) — so an equivalently-named phase
34
+ resolves to the same entry instead of a surprising `KeyError`."""
35
+ wanted = _key(phase)
36
+ for stored, value in self.entries:
37
+ if _key(stored) == wanted:
38
+ return value
39
+ raise KeyError(wanted)
40
+
41
+
42
+ @dataclasses.dataclass(frozen=True, slots=True)
43
+ class Compiler[Cap]:
44
+ """A composable, keyed set of phases. Operators key phases by `Phase.name` (anonymous
45
+ phases key by identity), giving an idempotent semilattice over phase sets."""
46
+
47
+ phases: tuple[Phase[Cap, typing.Any], ...] = ()
48
+
49
+ def run(self, items: Iterable[Cap]) -> Bundle[Cap]:
50
+ """Fold one materialised item sequence through every phase, once each."""
51
+ seq = tuple(items)
52
+ return Bundle(tuple((phase, phase.run(seq)) for phase in self.phases))
53
+
54
+ def __add__(self, other: "Compiler[Cap] | Phase[Cap, typing.Any]") -> "Compiler[Cap]":
55
+ """Left-biased union. Idempotent (`A + A == A`): phases already present win."""
56
+ added = other.phases if isinstance(other, Compiler) else (other,)
57
+ seen = {_key(phase) for phase in self.phases}
58
+ extra = tuple(phase for phase in added if _key(phase) not in seen)
59
+ return Compiler((*self.phases, *extra))
60
+
61
+ def __sub__(self, other: "Compiler[Cap]") -> "Compiler[Cap]":
62
+ """Restriction: drop phases whose key appears in `other`."""
63
+ drop = {_key(phase) for phase in other.phases}
64
+ return Compiler(tuple(phase for phase in self.phases if _key(phase) not in drop))
65
+
66
+ def __and__(self, other: "Compiler[Cap]") -> "Compiler[Cap]":
67
+ """Intersection: keep only phases whose key appears in both."""
68
+ keep = {_key(phase) for phase in other.phases}
69
+ return Compiler(tuple(phase for phase in self.phases if _key(phase) in keep))
70
+
71
+ def __or__(self, other: "Compiler[Cap]") -> "Compiler[Cap]":
72
+ """Right-biased merge: `other`'s phases override same-keyed phases in `self`."""
73
+ override = {_key(phase): phase for phase in other.phases}
74
+ merged = tuple(override.get(_key(phase), phase) for phase in self.phases)
75
+ present = {_key(phase) for phase in self.phases}
76
+ extra = tuple(phase for phase in other.phases if _key(phase) not in present)
77
+ return Compiler((*merged, *extra))
78
+
79
+
80
+ __all__ = ("Bundle", "Compiler")
capability/_core.py ADDED
@@ -0,0 +1,76 @@
1
+ """The substrate: the `fold` primitive, the `Step`/`AsyncStep` policies, the `Capability` marker.
2
+
3
+ `fold` is a typed left-fold. It threads an immutable accumulator through a sequence of items,
4
+ applying a *dispatch policy* to each. The policy lives entirely in the `Step` — `fold` itself
5
+ never inspects an item, never names a target. A new target is a new `Step`, not an edit here.
6
+
7
+ A `Step`/`AsyncStep` is a small frozen wrapper around the dispatch function (not a bare callable).
8
+ Two distinct nominal types is what lets `fold` choose the sync vs async branch with an
9
+ unambiguous `isinstance` — and you never build them by hand: use `by_protocol`/`by_table`/`chain`.
10
+
11
+ `fold` is `@overload`-ed: a sync `Step` returns the final context; an `AsyncStep` makes `fold`
12
+ return a coroutine. `afold` is a literal alias of `fold` for readers who prefer `await afold(...)`.
13
+ """
14
+
15
+ import dataclasses
16
+ import typing
17
+ from collections.abc import Awaitable, Callable, Coroutine, Iterable
18
+
19
+
20
+ @typing.runtime_checkable
21
+ class Capability(typing.Protocol):
22
+ """The universal item marker — an *empty* protocol, so structurally **anything** is a
23
+ `Capability`. It gives the dispatch combinators a pyrules-clean "any item" type (instead of
24
+ `object`) while preserving the open-world rule: a capability need not inherit anything; it
25
+ merely needs some `Step` that knows how to apply it."""
26
+
27
+
28
+ @dataclasses.dataclass(frozen=True, slots=True)
29
+ class Step[Cap, C]:
30
+ """A sync dispatch policy: wraps `fn(item, ctx) -> ctx`. Built by `by_protocol`/`by_table`/
31
+ `chain`; consumed by `fold`. A policy that does not recognise an item returns the context
32
+ unchanged (the open-world skip)."""
33
+
34
+ fn: Callable[[Cap, C], C]
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True, slots=True)
38
+ class AsyncStep[Cap, C]:
39
+ """An async dispatch policy: wraps `fn(item, ctx) -> Awaitable[ctx]`. Built by passing an
40
+ `async def` apply to `by_protocol`; consumed by `fold`/`afold` (which return a coroutine).
41
+ Disjoint from `Step` so `fold` can branch on `isinstance` with no type ambiguity."""
42
+
43
+ fn: Callable[[Cap, C], Awaitable[C]]
44
+
45
+
46
+ async def _afold[Cap, C](items: Iterable[Cap], initial: C, step: AsyncStep[Cap, C]) -> C:
47
+ ctx = initial
48
+ for item in items:
49
+ ctx = await step.fn(item, ctx)
50
+ return ctx
51
+
52
+
53
+ @typing.overload
54
+ def fold[Cap, C](
55
+ items: Iterable[Cap], initial: C, step: AsyncStep[Cap, C]
56
+ ) -> Coroutine[typing.Any, typing.Any, C]: ...
57
+ @typing.overload
58
+ def fold[Cap, C](items: Iterable[Cap], initial: C, step: Step[Cap, C]) -> C: ...
59
+ def fold[Cap, C](
60
+ items: Iterable[Cap], initial: C, step: Step[Cap, C] | AsyncStep[Cap, C]
61
+ ) -> C | Coroutine[typing.Any, typing.Any, C]:
62
+ """Thread `initial` through `items` left-to-right with `step`. Order-dependent, total,
63
+ reflection-free. Returns the final context for a sync step, or a coroutine for an async one."""
64
+ if isinstance(step, AsyncStep):
65
+ return _afold(items, initial, step)
66
+ ctx = initial
67
+ for item in items:
68
+ ctx = step.fn(item, ctx)
69
+ return ctx
70
+
71
+
72
+ afold = fold
73
+ """Alias of `fold`. Identical overloaded behaviour; reads better as `await afold(...)`."""
74
+
75
+
76
+ __all__ = ("AsyncStep", "Capability", "Step", "afold", "fold")
@@ -0,0 +1,95 @@
1
+ """Dispatch combinators — the typed, reflection-free way to build a `Step`.
2
+
3
+ The library does not pick a side of the Expression Problem; it hands you both directions:
4
+
5
+ - `by_protocol` is open **on the data side** — add a new capability that implements the
6
+ protocol and existing folds pick it up. (Use when capabilities proliferate.)
7
+ - `by_table` is open **on the interpreter side** — add a new fold with its own handler
8
+ table. (Use when targets proliferate.)
9
+ - `chain` runs several policies on one fold pass (keep them the same item type — see `chain`).
10
+
11
+ `by_protocol` is `@overload`-ed: a sync `apply` yields a `Step`; an async `apply` (an
12
+ `async def`) yields an `AsyncStep` (for `fold`/`afold`). One name, both worlds. None of this
13
+ uses `getattr` or string-dispatch — if you want name-driven dispatch, see `capability.reflect`.
14
+ """
15
+
16
+ import inspect
17
+ import typing
18
+ from collections.abc import Awaitable, Callable, Mapping
19
+
20
+ from capability._core import AsyncStep, Capability, Step
21
+
22
+ type Table[Cap, C] = Mapping[type[Cap], Callable[[Cap, C], C]]
23
+ """A handler table for `by_table`: maps an exact capability class to its handler."""
24
+
25
+
26
+ @typing.overload
27
+ def by_protocol[P, C](
28
+ proto: type[P], apply: Callable[[P, C], Awaitable[C]]
29
+ ) -> AsyncStep[Capability, C]: ...
30
+ @typing.overload
31
+ def by_protocol[P, C](proto: type[P], apply: Callable[[P, C], C]) -> Step[Capability, C]: ...
32
+ def by_protocol[P, C](
33
+ proto: type[P], apply: Callable[[P, C], C | Awaitable[C]]
34
+ ) -> Step[Capability, C] | AsyncStep[Capability, C]:
35
+ """Apply `apply(item, ctx)` to items that are instances of `proto`; skip the rest.
36
+
37
+ `proto` must be `@runtime_checkable`. Note its runtime cost: `isinstance` against a
38
+ runtime-checkable Protocol matches on **method-name presence**, not signatures — so the typed
39
+ `apply` (`lambda c, ctx: c.compile_x(ctx)`) is what gives you the real, signature-checked
40
+ dispatch at the call site. For async, pass an `async def` apply; the returned `AsyncStep` is
41
+ consumed by `fold`/`afold`."""
42
+ if inspect.iscoroutinefunction(apply):
43
+
44
+ async def afn(item: Capability, ctx: C) -> C:
45
+ if isinstance(item, proto):
46
+ return await apply(item, ctx)
47
+ return ctx
48
+
49
+ return AsyncStep(afn)
50
+
51
+ def fn(item: Capability, ctx: C) -> C:
52
+ if isinstance(item, proto):
53
+ produced = apply(item, ctx)
54
+ if isinstance(produced, Awaitable):
55
+ raise TypeError(
56
+ "async apply must be an 'async def', not a coroutine-returning lambda"
57
+ )
58
+ return produced
59
+ return ctx
60
+
61
+ return Step(fn)
62
+
63
+
64
+ def by_table[Cap, C](table: Table[Cap, C]) -> Step[Cap, C]:
65
+ """Apply the handler registered for an item's exact class; skip items with no entry.
66
+
67
+ Keyed by exact `type(item)` (not `isinstance`), so a handler for a base class does not
68
+ fire for subclasses — that is what makes the interpreter side independently extensible."""
69
+
70
+ def fn(item: Cap, ctx: C) -> C:
71
+ handler = table.get(type(item))
72
+ if handler is not None:
73
+ return handler(item, ctx)
74
+ return ctx
75
+
76
+ return Step(fn)
77
+
78
+
79
+ def chain[Cap, C](*steps: Step[Cap, C]) -> Step[Cap, C]:
80
+ """Compose dispatch policies on one fold pass: apply each `step` to every item, left to right.
81
+ Use to run several `by_protocol` policies over the same context in a single traversal.
82
+
83
+ `Step` is invariant in its item type, so chaining a `by_protocol` (`Step[Capability, C]`) with a
84
+ narrower `by_table` step won't type-check under strict mode — keep chained steps homogeneous,
85
+ or build the table as `by_table[Capability, C](...)` (handlers over `Capability`) to mix."""
86
+
87
+ def fn(item: Cap, ctx: C) -> C:
88
+ for inner in steps:
89
+ ctx = inner.fn(item, ctx)
90
+ return ctx
91
+
92
+ return Step(fn)
93
+
94
+
95
+ __all__ = ("Table", "by_protocol", "by_table", "chain")
capability/_folds.py ADDED
@@ -0,0 +1,56 @@
1
+ """Extra fold shapes — cheap variants of the core left `fold`.
2
+
3
+ The core `fold` (in `_core`) is the canonical left fold: the catamorphism over a flat list.
4
+ These are the neighbours you occasionally want — a right fold, a scan that keeps every
5
+ intermediate context, and the tree catamorphism for recursive descriptions. All are small,
6
+ sync, reflection-free, and read a `Step`'s `.fn` directly.
7
+ """
8
+
9
+ from collections.abc import Callable, Iterable
10
+
11
+ from capability._core import Step
12
+
13
+
14
+ def rfold[Cap, C](items: Iterable[Cap], initial: C, step: Step[Cap, C]) -> C:
15
+ """Right fold: apply `step` to items last-to-first. Equals `fold(reversed(items), ...)`.
16
+
17
+ Materialises `items` (needs to walk them in reverse). Use when the natural accumulation runs
18
+ from the tail — e.g. building a nested wrapper chain outermost-last."""
19
+ ctx = initial
20
+ for item in reversed(tuple(items)):
21
+ ctx = step.fn(item, ctx)
22
+ return ctx
23
+
24
+
25
+ def scan[Cap, C](items: Iterable[Cap], initial: C, step: Step[Cap, C]) -> tuple[C, ...]:
26
+ """Left scan: every intermediate context, starting with `initial`.
27
+
28
+ `scan(xs, init, step)[0] == init`, `scan(...)[-1] == fold(xs, init, step)`, and the length is
29
+ `len(xs) + 1`. Handy for visualising how a fold evolves, or for incremental/streaming use."""
30
+ states: list[C] = [initial]
31
+ ctx = initial
32
+ for item in items:
33
+ ctx = step.fn(item, ctx)
34
+ states.append(ctx)
35
+ return tuple(states)
36
+
37
+
38
+ type Children[N] = Callable[[N], Iterable[N]]
39
+ """Given a node, yield its child nodes."""
40
+
41
+ type TreeStep[N, R] = Callable[[N, tuple[R, ...]], R]
42
+ """Given a node and its already-folded children, produce this node's result."""
43
+
44
+
45
+ def fold_tree[N, R](root: N, children: Children[N], step: TreeStep[N, R]) -> R:
46
+ """Bottom-up catamorphism over a tree: children are folded before their parent. Use it when
47
+ descriptions are recursive (an expression / query AST) rather than a flat capability tuple."""
48
+
49
+ def go(node: N) -> R:
50
+ folded = tuple(go(child) for child in children(node))
51
+ return step(node, folded)
52
+
53
+ return go(root)
54
+
55
+
56
+ __all__ = ("Children", "TreeStep", "fold_tree", "rfold", "scan")
capability/_phase.py ADDED
@@ -0,0 +1,29 @@
1
+ """`Phase` — a reified target: a context factory plus the `Step` that fills it.
2
+
3
+ A `Phase` is the answer to "what does it mean to compile these capabilities into *this*
4
+ output?" One immutable description (a tuple of capabilities) run through different phases
5
+ yields different, independent results — the "one description, many targets" property.
6
+ """
7
+
8
+ import dataclasses
9
+ from collections.abc import Callable, Iterable
10
+
11
+ from capability._core import Step, fold
12
+
13
+
14
+ @dataclasses.dataclass(frozen=True, slots=True)
15
+ class Phase[Cap, C]:
16
+ """A named fold target: `initial()` seeds the accumulator, `step` is the dispatch policy.
17
+
18
+ `name` is used only by the compiler algebra (as the merge key) and by `explain`."""
19
+
20
+ initial: Callable[[], C]
21
+ step: Step[Cap, C]
22
+ name: str = ""
23
+
24
+ def run(self, items: Iterable[Cap]) -> C:
25
+ """Fold `items` through this phase, returning the accumulated context."""
26
+ return fold(items, self.initial(), self.step)
27
+
28
+
29
+ __all__ = ("Phase",)
capability/_trace.py ADDED
@@ -0,0 +1,51 @@
1
+ """Inspectability — `traced_fold` records every step; `explain` renders it.
2
+
3
+ Because capabilities and contexts are immutable, "did this step change the context?" is a
4
+ cheap identity check (`before is not after`) — no defensive copies. Tracing is a separate
5
+ function so the hot `fold` path stays branch-free and zero-cost when you do not need it.
6
+ """
7
+
8
+ import dataclasses
9
+ from collections.abc import Iterable
10
+
11
+ from capability._core import Step
12
+
13
+
14
+ @dataclasses.dataclass(frozen=True, slots=True)
15
+ class FoldStep:
16
+ """One recorded fold step: the item's type name and whether it changed the context."""
17
+
18
+ item_type: str
19
+ changed: bool
20
+
21
+
22
+ @dataclasses.dataclass(slots=True)
23
+ class Trace:
24
+ """A per-call sink for `FoldStep`s. An instance, not module state — pass a fresh one in."""
25
+
26
+ steps: list[FoldStep] = dataclasses.field(default_factory=list[FoldStep])
27
+
28
+ def record(self, item_type: str, changed: bool) -> None:
29
+ self.steps.append(FoldStep(item_type, changed))
30
+
31
+
32
+ def traced_fold[Cap, C](items: Iterable[Cap], initial: C, step: Step[Cap, C], trace: Trace) -> C:
33
+ """Like sync `fold`, but append a `FoldStep` to `trace` for each item."""
34
+ ctx = initial
35
+ for item in items:
36
+ before = ctx
37
+ ctx = step.fn(item, ctx)
38
+ trace.record(type(item).__qualname__, before is not ctx)
39
+ return ctx
40
+
41
+
42
+ def explain(trace: Trace) -> str:
43
+ """Render a trace as human-readable lines: which items changed the context, which skipped."""
44
+ lines = [
45
+ f" {fold_step.item_type}: {'changed' if fold_step.changed else 'skipped'}"
46
+ for fold_step in trace.steps
47
+ ]
48
+ return "\n".join(lines)
49
+
50
+
51
+ __all__ = ("FoldStep", "Trace", "explain", "traced_fold")
capability/py.typed ADDED
File without changes
capability/reflect.py ADDED
@@ -0,0 +1,34 @@
1
+ """Name-based dispatch — a legitimate alternative to `by_protocol`.
2
+
3
+ Dispatch by a method *name* resolved at runtime: any item exposing a method called `method`
4
+ (found by walking the MRO and reading each class `__dict__` via `vars`) gets it applied; the rest
5
+ are skipped. Free of `getattr`, so it stays lint-clean — but name-driven, so the method's
6
+ signature is not checked. The *recommended* path remains `capability.by_protocol` with a typed
7
+ `apply`; reach here when name dispatch is what you want.
8
+ """
9
+
10
+ from collections.abc import Callable
11
+
12
+ from capability._core import Capability, Step
13
+
14
+
15
+ def by_method[C](method: str, _context: type[C]) -> Step[Capability, C]:
16
+ """Apply `item.<method>(ctx)` to items exposing `method`; skip the rest.
17
+
18
+ Looks the method up along the item's MRO via `vars` (no `getattr`) and calls it unbound.
19
+ `_context` is a type-only witness that pins the accumulator type `C` (name dispatch cannot
20
+ infer it). Unchecked by design: the method's signature is not verified. Prefer
21
+ `capability.by_protocol` when you can express the call as a typed `apply` lambda."""
22
+
23
+ def fn(item: Capability, ctx: C) -> C:
24
+ for klass in type(item).__mro__:
25
+ found = vars(klass).get(method)
26
+ if found is not None:
27
+ bound: Callable[[Capability, C], C] = found
28
+ return bound(item, ctx)
29
+ return ctx
30
+
31
+ return Step(fn)
32
+
33
+
34
+ __all__ = ("by_method",)
@@ -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
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
31
+ [![Types: pyright strict](https://img.shields.io/badge/types-pyright%20strict-blue)](https://github.com/microsoft/pyright)
32
+ [![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#correctness)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,14 @@
1
+ capability/__init__.py,sha256=MRAlmOcMJPv_Ee_6s-jAJpq7HpUZdnF23UErQpFyN94,2356
2
+ capability/_attach.py,sha256=xDSDyrpqtaM33V7LECZgP50xz_OAPg4cDN2Zh7aWI9Y,1798
3
+ capability/_compiler.py,sha256=MDB7VbZmDWFxefPfUD2tnV9bpzlKJSzN9TPg8P-HT70,3672
4
+ capability/_core.py,sha256=KKVBBVwWCIe4RVixCBuPzJBrn7WeUg_xPQfONumRQOM,3210
5
+ capability/_dispatch.py,sha256=wgs5xCC9cyb0nD9baDn01C_zGv6LsOY2XMS19ZUihF8,3920
6
+ capability/_folds.py,sha256=jGjs15b9AKWfVl4jEcLkc7-ejuNiTYytbo9KbOiQC6U,2170
7
+ capability/_phase.py,sha256=lc2sxHK3dhwNeL9m_YIT2-SDIqAOTaXdud3YtV5tK0g,993
8
+ capability/_trace.py,sha256=qGuxRzEuvryHXbhv0ogFNenLDwlI96Z1fjIjtmQ376A,1702
9
+ capability/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ capability/reflect.py,sha256=kOZs26lIty9gDbczttepY_mFQL3poYyZgeHbO9WrETU,1437
11
+ capability-1.0.0.dist-info/METADATA,sha256=8RCjpl1midL1nN0uS_2Oce3hjhq3bqOEsj-IphnQIW8,9898
12
+ capability-1.0.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
13
+ capability-1.0.0.dist-info/licenses/LICENSE,sha256=kBpSkOISotXfPuz4LJR2wBk9exHEgcC7BsZH7Jb73M8,1072
14
+ capability-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.