-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Use MiddlewareType as a type hint for middlewares
#1987
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
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 = ASGIAppThis 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.
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 _asgiBut 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
MiddlewareTypeyou 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 as it would expect |
|
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 = ...) -> ExampleMiddlewareBut 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. |
abersheeran
left a comment
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.