You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(decorator): add auto error discovery to with_errors
with_errors(router, auto=True) fills responses by statically walking each
endpoint and its whole dependency tree at registration (the same walk as
check_raises), merged with any explicit Raises[...]. The Dependant tree is
rebuilt via public get_dependant/get_parameterless_sub_dependant with a lazy
import + endpoint-only fallback; _dependency_calls/_is_security_scheme move to
decorator.wrapper as the shared layer the checker imports from.
Copy file name to clipboardExpand all lines: CLAUDE.md
+5-3Lines changed: 5 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,15 +6,15 @@ Public PyPI package: typed HTTP errors for FastAPI — exact `Literal` error cod
6
6
7
7
## Status
8
8
9
-
Draft. The `core` and `decorator` layers plus the layer-3 CI checker are implemented (the analysis `auto`-fill mode is not yet). Linters/type checker and pytest are configured (see Conventions); the whole package is at 100% branch coverage. Licensed under MIT (`LICENSE` + PEP 639 metadata in `pyproject.toml`).
9
+
Draft. All three layers are implemented — `core`, the `with_errors` decorator (with `auto`-fill), and the layer-3 analysis (CI checker `check_raises` + CLI). Linters/type checker and pytest are configured (see Conventions); the whole package is at 100% branch coverage. Licensed under MIT (`LICENSE` + PEP 639 metadata in `pyproject.toml`).
10
10
11
11
## Architecture — three independent layers
12
12
13
13
Each layer is usable without the next one:
14
14
15
15
1.**`core`** (done) — `BaseError`, the `BaseErrorMeta` metaclass, `ErrorResponse`, `error_models()`, `handle_base_error`.
16
16
2.**`decorator`** (done) — `with_errors(router)` + `Raises[...]` in the return annotation (Annotated syntax). An instance patch of `add_api_route` as the single interception point; subclassing `APIRouter` AND a wrapper object were both rejected (see decorator-layer decisions).
17
-
3.**`analysis`** (CI checker done; `auto` planned) — AST walk over `raise` statements. `check_raises()` compares declared `Raises[...]` against errors actually raised (endpoint + helpers + dependency tree); a typer CLI (`cli` extra) wraps it. `auto=True` (auto-populating `responses` from the same walk) is the remaining piece. Design inspired by fastapi-docx (MIT), whose flaws are deliberately not inherited (see analysis-layer decisions).
17
+
3.**`analysis`** (done) — AST walk over `raise` statements. `check_raises()` compares declared `Raises[...]` against errors actually raised (endpoint + helpers + dependency tree); a typer CLI (`cli` extra) wraps it. `with_errors(router, auto=True)` reuses the same walk to auto-populate `responses` at registration. Design inspired by fastapi-docx (MIT), whose flaws are deliberately not inherited (see analysis-layer decisions).
-`_BodyVisitor` descends into statement bodies only (skips annotations/decorators/defaults), so a route's own `Raises[...]` return annotation is not counted, and local nested `def`s ARE walked. Catches `raise X`, `raise X(...)`, `raise ... from e`. **Argument heuristic**: an error class passed as a call argument counts as potentially raised (the `get_or_404(error=X)` pattern) — carve-out for `isinstance`/`issubclass`. Over-approximation is safe for CI (surfaces as `undeclared`); documented false-negatives (locals, `self.*`, dynamic dispatch, bare streams) keep `overdeclared` a failure by default.
71
71
- Two discrepancy buckets, reported separately: `undeclared` (always a failure), `overdeclared` (failure by default; `allow_overdeclared=True` strips it at report-build time). `RaisesReport.ok` = `not routes`; no `__bool__` (ambiguous).
72
72
- CLI split: `cli.py::main()` is the console-script entry with **no top-level `typer` import** (degrades to exit 2 + install hint without the `cli` extra); `_cli.py` holds the typer app (module-level `typer.Typer()`, needs the extra). A `@cli.callback()` forces `check` to be a named subcommand (a single typer command would otherwise collapse and swallow the app-path argument). Exit codes: 0 ok, 1 discrepancies, 2 usage/loading.
73
-
-`collect_raised`/`_scan` are the reuse point for the future `auto=True`.
73
+
-`collect_raised`/`_scan` are the shared engine for both `check_raises` and `with_errors(auto=True)`.
74
+
-**`auto=True`** (user decision 2026-07-23): scope = endpoint + full dependency tree (matches `check_raises`); the discovered set is **unioned** with any `Raises[...]` markers, and an explicit `responses={}` still wins per status. Injected in `wrapper` before delegating (mutating `route.responses` after build silently drops the schema — `response_fields` are frozen at build; verified). The dependency tree is rebuilt at registration (the route does not exist yet) via public `fastapi.dependencies.utils.get_dependant` + `get_parameterless_sub_dependant`, folding router-level + route-level `dependencies=[...]` (mirror `_build_dependant_with_parameterless_dependencies`); `path` is irrelevant (only path-param detection). `auto` is captured in the wrapper closure — first-`with_errors`-call wins (idempotent re-wrap does not change it).
75
+
- Import layering for `auto`: `decorator.wrapper` cannot import `analysis` at module level (analysis already imports wrapper → cycle). So `collect_raised` is **lazy-imported** inside `_auto_raised`, and `get_dependant`/`get_parameterless_sub_dependant` are lazy-imported in a `try/except ImportError` → graceful degradation to endpoint-only if FastAPI moves them (tested by `delattr`-ing `get_parameterless_sub_dependant`, which fails only our combined import, not FastAPI's own bindings). `_dependency_calls`/`_is_security_scheme` live in `decorator.wrapper` (the shared lower layer both use) and are imported by `analysis.checker`.
Copy file name to clipboardExpand all lines: README.md
+16-1Lines changed: 16 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,7 +2,7 @@
2
2
3
3
Typed error responses for FastAPI: `Literal` error codes in OpenAPI, discriminated `oneOf` unions, and a single source of truth — the error class itself.
4
4
5
-
> **Status: draft.**Layers 1 (`core`), 2 (`decorator`) and the layer-3 CI checker are implemented; the analysis `auto`-fill mode is coming.
5
+
> **Status: draft.**All three layers are implemented — `core`, the `with_errors`decorator (with `auto`-fill), and the layer-3 CI checker.
6
6
7
7
## Requirements
8
8
@@ -69,6 +69,21 @@ OpenAPI gets a `404` and a `403` entry with the **exact** `Literal` code each; s
69
69
70
70
`with_errors(router)` returns the **same**`APIRouter` instance with its `add_api_route` patched on the instance, so object identity is preserved: `include_router`, websockets, imperative `add_api_route(...)` calls and app-level decorators all work natively. For an application, wrap its router: `with_errors(app.router)`.
71
71
72
+
### Auto-fill
73
+
74
+
Pass `with_errors(router, auto=True)` to drop the `Raises[...]` markers entirely: at registration each endpoint and its whole dependency tree are statically walked (the same walk the [CI checker](#ci-checker) uses), and the discovered errors fill `responses` automatically — merged with any markers you *do* write, and an explicit `responses={}` still wins per status.
0 commit comments