-
-
Notifications
You must be signed in to change notification settings - Fork 954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use MiddlewareType
as a type hint for middlewares
#1987
Conversation
I think this was already discussed before. 🤔 |
I've tried looking in the already existing issues/PRs/discussions but couldn't find anything 😕 |
7ea1226
to
9a9ce03
Compare
EDIT: I think my comment says that we should be doing what you are trying to improve, but that PR was doing too many things... 👀 EDIT 2: This doesn't mean that I agree with those changes, I didn't even check them yet... jfyk |
Note to self: import |
9a9ce03
to
7098ab5
Compare
Waiting for directives regarding the coverage fail. |
What does this break on FastAPI? |
Middlewares are just ASGI applications. |
Sorry I wasn't clear on that point, it doesn't break anything for projects that depend on starlette, but it might break type checking if one is passing an unknown type to
True indeed, I went for the Middleware: TypeAlias = ASGIApp This is up to you |
typing.Protocol
for middlewaresMiddewareType
for middlewares
f8d1384
to
844f084
Compare
starlette/types.py
Outdated
|
||
ASGIApp: TypeAlias = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]] | ||
|
||
MiddewareType: TypeAlias = ASGIApp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is TypeAlias
needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://peps.python.org/pep-0613/. Here it does not add value, as it would be implicitly determined as a TypeAlias
any way, but as typing_extensions
is used in this project I figured why not use it, that way we know explicitly it is a TypeAlias
.
Should we just use ASGIApp instead of creating this new type? 🤔 |
This is purely to avoid confusion, and to avoid having people thinking they need to pass in a |
@Kludex after thinking about this, I found that it makes sense to have dedicated types for such components. It makes things very specific. It should not affect only middlewares, but any other component we may find important. I would not drop this PR but rather initiate a deeper discussion on this topic. |
Is |
starlette/types.py
Outdated
|
||
ASGIApp: TypeAlias = typing.Callable[[Scope, Receive, Send], typing.Awaitable[None]] | ||
|
||
MiddewareType: TypeAlias = ASGIApp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
starlette/starlette/applications.py
Lines 103 to 104 in 337ae24
for cls, options in reversed(middleware): | |
app = cls(app=app, **options) |
Considering this code, should we use
class MiddlewareType(Protocol):
def __call__(
self,
app: ASGIApp,
**options: Any
) -> ASGIApp:
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you look at the __iter__
method of the Middleware
class, you will see that it returns a tuple whose first element is the middleware class, which corresponds to the cls
variable here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cls
is MiddlewareType
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes indeed sorry, there's some confusion here between the __init__
and the __call__
method. When doing app = cls(app=app, **options)
, we are actually instantiating the middleware, not using the __call__
method. (That is because cls
is Type[MiddlewareType]
, not MiddlewareType
).
Defining the MiddlewareType
as a Callable
or as a Protocol
implementing __call__
is equivalent, as Callable
is an ABC implementing __call__
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a ASGI middleware that can be used in starlette.
class ASGIMiddleware:
def __init__(app: ASGIApp, p0: str, p1: int) -> None:
...
async def __call__(self, scope, receive, send) -> None:
...
This also can be used in starlette.
def asgi_middleware(app, p0: str, p1: int) -> ASGIApp:
async def _asgi(scope, receive, send) -> None:
...
return _asgi
But this can't be used in starlette. Even though P
passes the type check you're giving now.
class P:
def __init__(self) -> None: ...
def __call__(self, scope, receive, send) -> None: ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class MyMiddleware:
def __init__(self, app: ASGIApp, **options: Any) -> None:
...
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
...
This class passes the following type checks.
class MiddlewareType(Protocol):
def __call__(
self,
app: ASGIApp,
**options: Any
) -> ASGIApp:
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might, but imo this is not the way to go:
- You are using the
__call__
method as a way to define the signature of the__init__
method of a middleware, which is confusing - the
MiddlewareType
you created does not have any signature for the real async__call__
method that should be defined on a middleware class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't know how to use typing.Protocol, please consider using mypy to give it a try. 😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know how Protocols work, but you haven't explained why you are using these parameters for the __call__
method (see my answers above). As I understand it currently, you are using the __call__
method of your protocol as a way to define the __init__
method of a middleware class, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will continue discussion in main thread for clarity. Sorry for the confusion, it seems your suggestion is working, I couldn't get it to work since I was still on mypy==0.991
.
MiddewareType
for middlewaresMiddlewareType
for middlewares
@abersheeran sorry for the delay, this is how it looks now: class MiddlewareType(Protocol):
__name__: str
def __call__(self, *args: typing.Any, **kwargs: typing.Any) -> ASGIApp: # pragma: no cover
... Unfortunately, I can't enforce the |
45cd92d
to
b67412d
Compare
class MiddlewareType(Protocol):
def __call__(
self,
app: ASGIApp,
**options: Any
) -> ASGIApp:
... Be more precise. |
This doesn't work, as I said, more details in the linked starlette/starlette/middleware/errors.py Lines 125 to 142 in 01aa49a
as it would expect |
starlette/starlette/applications.py Lines 95 to 107 in 01aa49a
We can modify its definition a bit. Because it is like this when it is used. |
Apart from moving My opinion on this is while the |
My meaning is to modify the code to def __init__(
self,
app: ASGIApp,
*,
handler: typing.Optional[typing.Callable] = None,
debug: bool = False,
) -> None: |
I'm not following the discussion, but I guess is @Viicos turn. 👀 |
Yes I'll probably take a look this afternoon 👌 |
Unfortunately this doesn't seem to work either: main.py:20: note: Revealed type is "def (builtins.str)"
main.py:22: note: Revealed type is "None"
main.py:24: error: Argument 1 to "test" has incompatible type "type[ExampleMiddleware]"; expected "MiddlewareType" [arg-type]
main.py:24: note: Following member(s) of "ExampleMiddleware" have conflicts:
main.py:24: note: Expected:
main.py:24: note: def __call__(app: int, **options: Any) -> Callable[[str], None]
main.py:24: note: Got:
main.py:24: note: def __init__(app: int, *, other_arg: int | None = ...) -> ExampleMiddleware But even if it did, I'm not sure having people defining their own middlewares to explicitly use the |
😯 The results of mypy are a bit beyond my imagination. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Limited by the inference ability of mypy, I temporarily agree with the solution given by this PR. 🙏 Thank you all for your patient communication.
Thanks for the reviews @abersheeran and sorry for taking some time to respond. Typing callables can sometimes be a pain, but hopefully with |
ASGIApp
as a type hint for middlewaresMiddlewareType
as a type hint for middlewares
Pyright doesn't seem to like the proposed changes from this PR. 🤔 |
Are there plans to add pyright to CI? |
Pyright doesn't seem to support |
No, but since |
The other alternative would be to switch back to what I did at first: class MiddlewareType(Protocol):
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
... This is equivalent to We can also keep things as is if we don't want to support |
Let's first do what httpx did, and use mypy on strict mode. Then we can see what errors we have with pyright. For the time being, I'll close this PR. Thanks for the patience @Viicos |
This might brake things in other repositories (fastapi for instance), but this improves type safety in my opinion.