Open bunny-therapist opened 7 months ago
You are correct. As part of the 1.x to 2.x transition we removed a lot of the app parameters that allowed a union of types for this very reason.
There is also not supposed to be any abstract collection types on the AppConfig
object, they should all be concrete types like list
, dict
etc, and as you can see where we instantiate the config object, we cast the abstract collection types to concrete ones (notwithstanding this issue): https://github.com/litestar-org/litestar/blob/3ea7c1523de3ffb27f2dbcc609a741413f008127/litestar/app.py#L324-L374
Also, response_cookies
appears to have the same issue.
We should add a unit test that traverses the annotations on AppConfig
to check for abstract collections and unions to ensure that we don't make any more of these errors in the future.
Also, the type narrowing util should also be annotated to return a concrete type, not an abstract type, e.g.,:
def narrow_response_headers(headers: ResponseHeaders | None) -> Sequence[ResponseHeader] | None:
"""Given :class:`.types.ResponseHeaders` as a :class:`typing.Mapping`, create a list of
:class:`.datastructures.response_header.ResponseHeader` from it, otherwise return ``headers`` unchanged
"""
should really be:
def narrow_response_headers(headers: ResponseHeaders | None) -> tuple[ResponseHeader, ...] | None:
"""Given :class:`.types.ResponseHeaders` as a :class:`typing.Mapping`, create a list of
:class:`.datastructures.response_header.ResponseHeader` from it, otherwise return ``headers`` unchanged
"""
if AppConfig.response_headers was changed to list[ResponseHeader] and the conversion to this type happened inside Litestar.init, before InitPluginProtocol.on_app_init was called with the AppConfig, thus preserving the arguments to Litestar.init, then all compatibility would be preserved
I don't think this is true, given that it is currently allowed to be a mapping it is feasible that there are downstream users checking for a sequence and converting that to a mapping before making modifications. If we remove the mapping type, that would break that pattern.
I think the best we can do for now is to change the typing on the config object to list | dict
, perform the relevant casts on app config creation, and add tests to ensure we don't create any more of these issues.
Then, we can properly correct this on the v3 branch by changing the type on the app config object to list[ResponseHeader]
.
if AppConfig.response_headers was changed to list[ResponseHeader] and the conversion to this type happened inside Litestar.init, before InitPluginProtocol.on_app_init was called with the AppConfig, thus preserving the arguments to Litestar.init, then all compatibility would be preserved
I don't think this is true, given that it is currently allowed to be a mapping it is feasible that there are downstream users checking for a sequence and converting that to a mapping before making modifications. If we remove the mapping type, that would break that pattern.
I think the best we can do for now is to change the typing on the config object to
list | dict
, perform the relevant casts on app config creation, and add tests to ensure we don't create any more of these issues.Then, we can properly correct this on the v3 branch by changing the type on the app config object to
list[ResponseHeader]
.
You are right. Good catch.
Summary
I recently implemented a
InitPluginProtocol
that, among other things, adds a bunch of response headers to the app. I ran into the problem that the type ofAppConfig.response_headers
isResponseHeaders
, which is a union ofSequence[ResponseHeader]
andMapping[str, str]
: https://github.com/litestar-org/litestar/blob/main/litestar/types/composite_types.py#L53Because of this, there was no straightforward way to add response headers in a type correct way. I ended up doing this:
If
AppConfig.response_headers
was not a union, or even justlist[ResponseHeader)
, this would have been as simple as usinglist.extend
. I know that, ultimately, the response headers are type narrowed intotuple[ResponseHeader]
. I just wish that, e.g., it would have been either type narrowed to a list first in theAppConfig
, or that the app init was less generous with the allowed input formats.AppConfig.response_headers
being a union of two immutable types with different signature makes modification unnecessarily complex.Basic Example
For example, if
AppConfig.response_headers
waslist[ResponseHeader]
, I could just do this (of course, this example is an artificial toy example and there is no great reason for this plugin to exist as-is):Drawbacks and Impact
There are ways to implement this which break backwards-compatibility, and there are ways that don't. For example, if
AppConfig.response_headers
was changed tolist[ResponseHeader]
and the conversion to this type happened insideLitestar.__init__
, beforeInitPluginProtocol.on_app_init
was called with theAppConfig
, thus preserving the arguments toLitestar.__init__
, then all compatibility would be preserved, sincelist[ResponseHeader]
is in fact of typeResponseHeaders
(just much more narrow).Unresolved questions
No response