Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
90397a2
Pass a Config to the target unresolved when the parameter is annotate…
claude Jul 26, 2026
a612e15
Resolve string annotations through quoting and wrapper layers
claude Jul 26, 2026
0d3aede
Decide laziness from the resolved annotation, not its spelling
claude Jul 26, 2026
0e0aff3
Honour inspect's __signature__ stop and bound alias resolution
claude Jul 26, 2026
42623b1
Unwrap class signature sources and key the declaration cache by identity
claude Jul 26, 2026
9890de4
Resolve an inherited __call__ where it is defined, and ask typing abo…
claude Jul 26, 2026
cd586ca
Resolve an annotation only in the namespace that wrote it
claude Jul 26, 2026
bd7ffad
Resolve a postponed annotation against the class body it was written in
claude Jul 26, 2026
9b6fdd8
Use the defining class' body, and never compare annotation objects
claude Jul 26, 2026
087b6d6
Follow bound methods to their class body, and `type` aliases to their…
claude Jul 26, 2026
50fef56
Find the class body of a function that carries no binding
claude Jul 26, 2026
f4670b6
Follow a partialmethod reached through its class to the real method
claude Jul 26, 2026
3d73143
Resolve a parameter's annotation only when a config supplies it, and …
claude Jul 26, 2026
a4b4a8c
Follow a `type` alias given arguments to what it stands for
claude Jul 26, 2026
7d63b69
Move signature reading out of config.py into `_annotations`
claude Jul 26, 2026
35f950b
Keep what a decorated bound method is bound to
claude Jul 26, 2026
a06b9c3
Resolve a class' declared signature against the body that declared it
claude Jul 26, 2026
105aaff
Resolve a metaclass-declared signature in the metaclass' namespace
claude Jul 26, 2026
5164a55
Rewrap a docstring paragraph
claude Jul 26, 2026
853f3a4
Treat `__signature__ = None` as the absence of one
claude Jul 26, 2026
91d66c7
Offer a class body under the names its annotations were written as
claude Jul 26, 2026
ca59c74
Decline when rival declarations of a parameter disagree
claude Jul 26, 2026
b1a9d34
Resolve what an alias stands for where the alias was written
claude Jul 26, 2026
f7df400
Read a target's declaration afresh rather than remembering it
claude Jul 26, 2026
bb55639
End the rivalry when a class declares its own signature
claude Jul 26, 2026
3cb810c
Tell a repeated spelling apart from a cycle by the scope it is in
claude Jul 26, 2026
43f1b4a
Tell scopes apart by what is local to them, not only their globals
claude Jul 26, 2026
0e4da50
Record the instantiation cost in the changelog
claude Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.7.0] - 2026-07-26

### Changed
- **Behaviour:** a parameter annotated `cfn.Config` is no longer resolved before the target is called. A target that already had such a parameter and relied on `instantiate()` building the config passed to it now receives the config itself, with no error — both are ordinary values. Annotate the parameter with the type it is built into (or leave it unannotated) to keep the previous behaviour.
- **Performance:** `instantiate()` reads the target's signature on every call, since a target is free to change what it declares and an answer kept from an earlier reading is wrong in a way nothing reveals. Building a config node now costs tens of microseconds where it cost single-digit microseconds, and a couple of hundred for a target whose annotations are postponed (`from __future__ import annotations`) and have to be resolved as text. This applies to every config, not only those with an annotated parameter.
- `override()`, `override_data()` and `Config.__call__()` take `self` positionally, so `self` is usable as an override key. Previously `override_data(**{'self': ...})` — reachable wherever the keys come from outside the process — raised `TypeError` instead of being applied or reported as a `ConfigError`.

### Added
- A target parameter annotated `cfn.Config` now receives the stored config itself instead of its instantiation (#38). This covers targets that must build a config later, more than once, or with overrides they only learn at runtime — a server applying per-connection overrides, for instance — which previously forced a hand-rolled dict of configs keyed by string. The declaration lives in the target's signature, so callers keep passing ordinary values: `.override()` (including dotted keys reaching into the config), `--help` and `get_required_args` all keep working on it. `Config | None`, `Optional[Config]`, `Annotated[Config, ...]` and string annotations (`from __future__ import annotations`) are recognised too; a non-config value on such a parameter is passed through untouched, and configs nested in containers (`list[Config]`) still resolve as before.

## [0.6.0] - 2026-07-25

### Added
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,60 @@ def app(db):
return App(reader=Reader(db), writer=Writer(db))
```

### Receiving a config instead of an object

Resolving every argument before calling the target is the right default, but some targets
need the **config itself** — because they build it later, more than once, or with overrides
they only learn at runtime. A parameter annotated `cfn.Config` declares exactly that: it
receives the config stored in that slot, unresolved.

```python
# One pipeline config, plus named variants of it.
pipeline = cfn.Config(Pipeline, codec=h264, model_path='base')
droid = pipeline.override(codec=hevc, model_path='GEAR-Dreams/DreamZero-DROID')

@cfn.config(pipeline=pipeline, host='0.0.0.0', port=8000)
def serve(pipeline: cfn.Config, host: str, port: int):
# `pipeline` is a Config, so the server can rebuild it per connection with the
# overrides that connection asks for. An instantiated pipeline is frozen — there
# would be nothing left to tune.
PolicyServer(pipeline, host=host, port=port).serve()

cfn.cli({'': serve, 'droid': serve.override(pipeline=droid)})
```

```python
class PolicyServer:
def on_connection(self, params: dict): # params decoded from the request
return self._pipeline_cfg.override_data(**params).instantiate()
```

The declaration lives in the **signature**, not at the call site: whether a target wants a
config or a built object is a property of the target, not of each call. Callers keep passing
ordinary values and writing ordinary overrides, so nothing else changes — from Python or
from the command line:

```bash
python serve.py droid --pipeline.codec.fps=10 # dotted override reaches into it
python serve.py droid --pipeline=@my.pipelines.custom # swaps the whole config
python serve.py droid --help # shows what is inside the pipeline
```

`get_required_args` (and so `--help`) still reports required arguments nested inside such a
parameter: the config must be fully specified before whoever holds it instantiates it.

A few details:

- `cfn.Config | None` (and `Optional[cfn.Config]`) work too, so the parameter can default to
`None`. `Annotated[cfn.Config, ...]` is recognised as well, as are string annotations
(`from __future__ import annotations`).
- The annotation turns resolution *off* for that argument; it does not demand a `Config`. A
value that is not a config is passed through untouched.
- Only a whole argument can be handed over. A `list[cfn.Config]` annotation is not special —
configs inside containers are still built.
- The target receives the config stored in the slot, not a copy. Derive from it with
`.override()` / `.override_data()`, which copy, rather than mutating it in place.

## 🌍 Real-World Examples

### Robotics Hardware Configuration
Expand Down Expand Up @@ -629,6 +683,8 @@ Main configuration class that stores a callable and its arguments.
- `copy() -> Config`: Deep copy the configuration
- `__call__(**kwargs) -> Any`: `override` config with `**kwargs` and `instantiate` it. **Note:** only keyword specified arguments are supported.

**Target annotations:** a target parameter annotated `cfn.Config` (or `cfn.Config | None`) receives the stored config unresolved instead of what it builds — see [Receiving a config instead of an object](#receiving-a-config-instead-of-an-object).

#### `@config` Decorator
```python
@cfn.config # No override, just turn function into config.
Expand Down
Loading
Loading