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
fix(decorator): unwrap type-alias Raises markers in Annotated metadata
`_find_raises` only matched `Raises` markers that appear directly in an
`Annotated` metadata slot. A marker shared through a PEP 695 `type` alias
(`type Common = Raises[...]`) sits there as a `TypeAliasType`, so
`isinstance(meta, Raises)` failed and the marker was dropped silently.
Add a shared `_unwrap_type_alias` helper and apply it to each metadata
element before the checks (and reuse it at the annotation top and in
`_annotated_base`). Aliasing the whole annotation already worked; this
makes the aliased-marker-in-metadata spelling work on par with tuple
unpacking.
Also: docs (decorator guide gains a spellings/sharing/composition section
and the new spelling; bilingual CHANGELOG page wired into mkdocs nav),
bump to 1.0.1.
- Normalization: `-> Annotated[Response-subclass | None, Raises[...]]` → `response_model=None` (FastAPI's `lenient_issubclass` cannot see through `Annotated`; without this the route dies with `FastAPIError` / grows a spurious `null` schema). Bare stream returns (`AsyncIterator` + Raises) are unsupported — documented limitation.
57
57
- Merge semantics: derived entries lose to explicit per-route `responses` wholesale per status; router-level `responses` are merged by FastAPI itself and lose to per-route ones.
58
58
- Endpoint unwrapping for annotation reading only: `functools.partial` chain → `inspect.unwrap` → callable-instance `type(obj).__call__`; the original endpoint object is always what gets registered.
59
-
-`_find_raises` is recursive: unwraps PEP 695 `TypeAliasType`, descends into `Annotated` bases and union arms — a declared marker must never be dropped silently (adversarial review caught all three as silent-drop bugs). `_annotated_base` applies the same unwrapping for the Response/None normalization.
59
+
-`_find_raises` is recursive: unwraps PEP 695 `TypeAliasType` (both the whole annotation AND each `Annotated` metadata element, via the shared `_unwrap_type_alias` helper — a marker shared through `type Common = Raises[...]` sits in metadata as a `TypeAliasType`), descends into `Annotated` bases and union arms — a declared marker must never be dropped silently (adversarial review caught the base/union/metadata cases as silent-drop bugs). `_annotated_base` applies the same unwrapping for the Response/None normalization.
60
60
- Unresolvable type hints (NameError/TypeError from `get_type_hints`): if the raw return annotation does not mention `Raises` → silent passthrough (stock FastAPI tolerates the `TYPE_CHECKING` pattern and never resolves return hints under an explicit `response_model`); if it does → fail fast with a clear `TypeError`. Never resolve stricter than stock for marker-free endpoints.
61
61
- Follow-up idea (not implemented): PEP 692 `Unpack[TypedDict]` typing for registration kwargs.
The constructor form `Raises(*TOKEN_ERRORS)` is the statically clean fallback for type checkers.
70
78
79
+
**Named marker alias** — give a domain error set a name with a PEP 695 `type` alias and reuse it across many routes (and even many routers):
80
+
81
+
```python
82
+
type AuthErrors = Raises[RequiredTokenError, InvalidTokenError, WrongTokenTypeError]
83
+
84
+
85
+
defme() -> Annotated[User, AuthErrors]: ...
86
+
defstats() -> Annotated[Stats, AuthErrors, Raises[RateLimitedError]]: ...# shared set + a local one
87
+
```
88
+
89
+
**Composition** — put several markers side by side in one `Annotated`: a shared set plus route-specific errors. They are **concatenated and deduplicated**, so an overlap between markers is harmless:
`Raises[...]` validates each member as soon as it is evaluated (i.e. at module import): it must be a `BaseError` subclass with a declared `error_code` and `http_status`, and the list is non-empty. Otherwise — a `TypeError` naming the offender.
74
105
75
106
### What Raises finds in the annotation
76
107
77
-
The marker is found through PEP 695 `type` aliases, nested `Annotated` bases and union arms — a declared marker is **never dropped silently**:
108
+
The marker is found through PEP 695 `type` aliases (wrapping the whole annotation **or** the marker itself in metadata), nested `Annotated` bases and union arms — a declared marker is **never dropped silently**:
78
109
79
110
```python
80
111
type ItemNF = Annotated[Item, Raises[NotFoundError]]
112
+
type CommonRaises = Raises[NotFoundError, ForbiddenError]
81
113
82
114
83
-
defa() -> ItemNF: ...# alias
115
+
defa() -> ItemNF: ...# alias of the whole annotation
84
116
defb() -> Annotated[Item, Raises[NotFoundError]] |None: ...# union wrapper
117
+
defc() -> Annotated[Item, CommonRaises, Raises[ConflictError]]: ...# aliased marker in metadata
118
+
defd() -> Annotated[ItemNF, Raises[ConflictError]]: ...# aliased annotation nested in another
Конструкторная форма `Raises(*TOKEN_ERRORS)` — статически «чистая» запасная для тайпчекеров.
70
78
79
+
**Именованный алиас-маркер** — дайте доменному набору ошибок имя через PEP 695 `type`-алиас и переиспользуйте его на многих роутах (и даже на разных роутерах):
80
+
81
+
```python
82
+
type AuthErrors = Raises[RequiredTokenError, InvalidTokenError, WrongTokenTypeError]
83
+
84
+
85
+
defme() -> Annotated[User, AuthErrors]: ...
86
+
defstats() -> Annotated[Stats, AuthErrors, Raises[RateLimitedError]]: ...# общий набор + локальная
87
+
```
88
+
89
+
**Композиция** — поставьте несколько маркеров рядом в одном `Annotated`: общий набор плюс специфичные для роута ошибки. Они **конкатенируются и дедуплицируются**, так что пересечение маркеров безвредно:
| Именованный доменный набор для широкого переиспользования |`type AuthErrors = Raises[...]`|
100
+
| Общий набор + локальные добавки на роуте | композиция маркеров: `Annotated[T, AuthErrors, Raises[C]]`|
101
+
71
102
!!! note "Валидация — на этапе импорта"
72
103
73
104
`Raises[...]` проверяет каждый член сразу при вычислении (то есть при импорте модуля): это должен быть подкласс `BaseError` с объявленными `error_code` и `http_status`, список непуст. Иначе — `TypeError` с именем нарушителя.
74
105
75
106
### Что видит `Raises` в аннотации
76
107
77
-
Маркер находится сквозь PEP 695 `type`-алиасы, вложенные `Annotated`-базы и члены union — задекларированный маркер **никогда не теряется молча**:
108
+
Маркер находится сквозь PEP 695 `type`-алиасы (оборачивающие как всю аннотацию, так и **сам маркер** в позиции метаданных), вложенные `Annotated`-базы и члены union — задекларированный маркер **никогда не теряется молча**:
78
109
79
110
```python
80
111
type ItemNF = Annotated[Item, Raises[NotFoundError]]
112
+
type CommonRaises = Raises[NotFoundError, ForbiddenError]
0 commit comments