Skip to content

Commit 802d065

Browse files
committed
docs: add bilingual mkdocs-material site with API reference and Pages deploy
Material for MkDocs, English (default) + Russian via mkdocs-static-i18n, an mkdocstrings API reference from the Google docstrings, a Swagger-UI illustration and runnable examples throughout. Split into a concise Overview and a detailed Guide. GitHub Actions builds and deploys to Pages.
1 parent d0f1360 commit 802d065

33 files changed

Lines changed: 3098 additions & 2 deletions

.github/workflows/docs.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: docs
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- docs/**
8+
- mkdocs.yml
9+
- pyproject.toml
10+
- uv.lock
11+
- .github/workflows/docs.yml
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
pages: write
17+
id-token: write
18+
19+
concurrency:
20+
group: pages
21+
cancel-in-progress: false
22+
23+
jobs:
24+
build:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: astral-sh/setup-uv@v5
29+
with:
30+
enable-cache: true
31+
- run: uv sync --no-dev --group docs
32+
- run: uv run mkdocs build --strict
33+
- uses: actions/upload-pages-artifact@v3
34+
with:
35+
path: site
36+
37+
deploy:
38+
needs: build
39+
runs-on: ubuntu-latest
40+
environment:
41+
name: github-pages
42+
url: ${{ steps.deployment.outputs.page_url }}
43+
steps:
44+
- id: deployment
45+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ wheels/
1212
# Test artifacts
1313
.coverage
1414
.pytest_cache/
15+
16+
# Docs build
17+
site/
18+
.cache/

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ src-layout, package `src/fastapi_typed_errors/`:
9292
- Registration-only stub endpoints in tests end with `raise NotImplementedError` (ty `all = "error"` rejects `...` bodies with non-`None` return annotations outside stubs/protocols).
9393
- Test structure: **AAA** (Arrange / Act / Assert), the blocks separated by blank lines; single-expression tests may collapse to one line (e.g. `assert error_models(X) is X.model`). Every test opens with a short one-line docstring describing the case; test functions are annotated `-> None`.
9494
- Prefer pytest **fixtures** for reusable arrange values (request objects, configured apps, built artifacts) — typed, with a docstring and a `Returns:` section. Module level is only for error classes/enums that must exist at import time (they are used in type positions). Don't name a fixture `request` — it clashes with pytest's built-in.
95-
- Lint everything: `just lint` (ruff format --check, ruff check, ty check). Run `uv run ruff format` to apply formatting.
95+
- Lint everything: `just lint` (ruff format --check, ruff check, ty check). Run `uv run ruff format` to apply formatting. **ruff (preview) also formats Python code blocks inside Markdown** (`README.md`, `docs/**`), so keep doc snippets canonically formatted and re-run `just lint` after editing prose with code.
96+
- Docs: **Material for MkDocs**, bilingual (EN default at `/`, RU at `/ru/`) via `mkdocs-static-i18n` (folder mode: `docs/en/**`, `docs/ru/**`; `nav_translations` localize the nav; language-specific assets like the Swagger SVG live per-language, shared CSS in `docs/stylesheets/`). API reference is auto-generated from Google docstrings via `mkdocstrings[python]` (`:::` blocks). Deps in the `docs` group (`~=`); serve `just docs`, strict build `just docs-build`. Published to GitHub Pages by `.github/workflows/docs.yml` (uv build + `upload-pages-artifact`/`deploy-pages`; Pages source = "GitHub Actions").
9697
- Environment and build: uv (`uv sync`, `uv run python ...`), build backend `uv_build`.
9798
- Run smoke checks against a live FastAPI app: `uv run python <script>` (`httpx2` for `TestClient` is in the dev group, so no `--with` is needed anymore).

Justfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,11 @@ lint:
1111
# Run the test suite with coverage.
1212
test:
1313
uv run pytest --cov
14+
15+
# Serve the documentation site locally with live reload.
16+
docs:
17+
uv run --group docs mkdocs serve
18+
19+
# Build the documentation site in strict mode.
20+
docs-build:
21+
uv run --group docs mkdocs build --strict

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ router = with_errors(APIRouter(), auto=True)
8181
def get_item(item_id: int, user: Annotated[User, Depends(current_user)]) -> Item:
8282
if item_id == 0:
8383
raise NotFoundError(f"No item {item_id}") # auto -> 404
84-
return Item(item_id=item_id) # + whatever current_user can raise
84+
return Item(item_id=item_id) # + whatever current_user can raise
8585
```
8686

8787
## Core layer only
Lines changed: 65 additions & 0 deletions
Loading

docs/en/guide/checker.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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

Comments
 (0)