|
| 1 | +--- |
| 2 | +title: CI checker |
| 3 | +--- |
| 4 | + |
| 5 | +# CI checker: check_raises |
| 6 | + |
| 7 | +`Raises[...]` annotations are a declaration. `check_raises` statically verifies that the declaration **matches** what a route can actually raise — in the endpoint itself, its helpers and its whole dependency tree. This closes the gap that annotations leave open: a forgotten declaration, or a dead one you no longer raise. |
| 8 | + |
| 9 | +## Library usage |
| 10 | + |
| 11 | +It fits perfectly into a test: |
| 12 | + |
| 13 | +```python |
| 14 | +from fastapi_typed_errors import check_raises |
| 15 | + |
| 16 | + |
| 17 | +def test_error_contracts() -> None: |
| 18 | + report = check_raises(app) # a FastAPI app or an APIRouter |
| 19 | + assert report.ok, report.routes |
| 20 | +``` |
| 21 | + |
| 22 | +`check_raises` returns a [`RaisesReport`](../reference/analysis.md) with an `.ok` property and a list of discrepancies `.routes`. |
| 23 | + |
| 24 | +## Two discrepancy categories |
| 25 | + |
| 26 | +Each `RouteDiscrepancy` holds two **independent** buckets: |
| 27 | + |
| 28 | +| Category | What it means | Default | |
| 29 | +|---|---|---| |
| 30 | +| `undeclared` | raised in code but absent from `Raises` | **always a failure** | |
| 31 | +| `overdeclared` | declared but its raise is not found | a failure (toggleable) | |
| 32 | + |
| 33 | +```python |
| 34 | +report = check_raises(app) |
| 35 | +for route in report.routes: |
| 36 | + print(route.path, route.methods) |
| 37 | + print(" undeclared:", [e.__name__ for e in route.undeclared]) |
| 38 | + print(" overdeclared:", [e.__name__ for e in route.overdeclared]) |
| 39 | +``` |
| 40 | + |
| 41 | +Since AST analysis is conservative (it may not see dynamic `raise`s), `overdeclared` sometimes gives a false positive. In that case turn that bucket off: |
| 42 | + |
| 43 | +```python |
| 44 | +report = check_raises(app, allow_overdeclared=True) # ignore extra declarations |
| 45 | +``` |
| 46 | + |
| 47 | +!!! tip "Keep overdeclared on if you can" |
| 48 | + |
| 49 | + It catches dead declarations that pile up in OpenAPI. Reach for `allow_overdeclared=True` only for code with dynamic `raise`s the walker can't see. |
| 50 | + |
| 51 | +## CLI |
| 52 | + |
| 53 | +The same check as a command — handy in a CI pipeline. It needs the `cli` extra: |
| 54 | + |
| 55 | +```bash |
| 56 | +pip install "fastapi-typed-errors[cli]" |
| 57 | +``` |
| 58 | + |
| 59 | +You point it at an app path in `module:attribute` form: |
| 60 | + |
| 61 | +```bash |
| 62 | +fastapi-typed-errors check app.main:app |
| 63 | +``` |
| 64 | + |
| 65 | +=== "Match (exit 0)" |
| 66 | + |
| 67 | + ```console |
| 68 | + $ fastapi-typed-errors check app.main:app |
| 69 | + All 12 route(s) match their Raises declarations. |
| 70 | + ``` |
| 71 | + |
| 72 | +=== "Discrepancies (exit 1)" |
| 73 | + |
| 74 | + ```console |
| 75 | + $ fastapi-typed-errors check app.main:app |
| 76 | + ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ |
| 77 | + ┃ Route ┃ Undeclared ┃ Overdeclared ┃ |
| 78 | + ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ |
| 79 | + │ GET /items │ ForbiddenError │ - │ |
| 80 | + └─────────────────┴────────────────┴───────────────┘ |
| 81 | + 1 of 12 route(s) have discrepancies. |
| 82 | + ``` |
| 83 | + |
| 84 | +Flags: `--allow-overdeclared`, `--max-depth N`. |
| 85 | + |
| 86 | +Exit codes: |
| 87 | + |
| 88 | +| Code | Meaning | |
| 89 | +|---|---| |
| 90 | +| `0` | all declarations match | |
| 91 | +| `1` | discrepancies found (a table) | |
| 92 | +| `2` | a usage/loading error (bad path, not an app, unresolvable `Raises`) | |
| 93 | + |
| 94 | +## What exactly is compared |
| 95 | + |
| 96 | +- **Declared** — the union of `Raises[...]` markers from the endpoint's return annotation. It is read straight from the annotations, so it works **regardless** of whether the router was wrapped with `with_errors`. |
| 97 | +- **Raised** — `raise` statements from the endpoint's source **plus** every node of the `Depends` tree (security schemes are skipped). |
| 98 | + |
| 99 | +The walker understands the `get_or_404(error=NotFoundError)` factory pattern (the error class arrives as a call argument), closures, cross-module helpers and `functools.partial`. It runs in one process and **never executes** your code. For the false negatives (local variables, `self.method()` chains, dynamics) see [Limitations](limitations.md#walker). |
| 100 | + |
| 101 | +!!! example "GitHub Actions" |
| 102 | + |
| 103 | + ```yaml |
| 104 | + - run: uv run fastapi-typed-errors check app.main:app |
| 105 | + ``` |
| 106 | + |
| 107 | + Or run it via a test — `assert check_raises(app).ok` — and you won't need the separate CLI dependency. |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +**Next:** [Customization](customization.md) — your own response model, codes, `ABC` compatibility. |
0 commit comments