starlette icon indicating copy to clipboard operation
starlette copied to clipboard

incompatible type and expected "type[_MiddlewareClass[[]]]" [arg-type]

Open RaviMintel07 opened this issue 9 months ago • 8 comments

Hi ,

am using asgi-logger and extended AccessLoggerMiddleware to customise the logging in the application and when I upgrade fastapi to 0.109.x version getting issue on mypy, any suggestions ?

error: Argument 1 to "Middleware" has incompatible type "type[AccessLoggerMiddleware]"; expected "type[_MiddlewareClass[[]]]" [arg-type]

middleware = [
    Middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["PUT", "GET", "DELETE"],
        allow_headers=["Authorization"],
    ),
    Middleware(access_logger.AccessLoggerMiddleware),
]

and this is my customise version of logging.

import structlog
from asgi_logger import middleware as asl
from asgiref.typing import ASGI3Application, HTTPScope


class AccessLoggerMiddleware(asl.AccessLoggerMiddleware):
   //code

RaviMintel07 avatar Mar 20 '25 10:03 RaviMintel07

what version of mypy are you using? have you tried updating to the latest version, or using pyright which generally in my experience is more reliable?

adriangb avatar Mar 20 '25 15:03 adriangb

I tried with mypy 1.15.0 as well but still same issue.

RaviMintel07 avatar Mar 20 '25 15:03 RaviMintel07

Can you offer a self-contained MRE?

adriangb avatar Mar 20 '25 18:03 adriangb

I'm seeing the same issue here in the Nominatim project after updating to starlette 0.47. Example of failing CI: here.

The problem appears when creating a custom middleware with initialisation parameters.

Minimal code example to reproduce:

from starlette.applications import Starlette
from starlette.responses import Response, JSONResponse
from starlette.routing import Route
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request


class FileLoggingMiddleware(BaseHTTPMiddleware):

    def __init__(self, app: Starlette, file_name: str = '') -> None:
        super().__init__(app)

    async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
        return await call_next(request)


async def homepage(request):
    return JSONResponse({'hello': 'world'})


app = Starlette(debug=True, routes=[Route('/', homepage)],
                middleware=[Middleware(FileLoggingMiddleware, file_name='/tmp')])

Running mypy results in:

test.py:23: error: Argument 1 to "Middleware" has incompatible type "type[FileLoggingMiddleware]"; expected "_MiddlewareFactory[[]]"  [arg-type]
test.py:23: note: Following member(s) of "FileLoggingMiddleware" have conflicts:
test.py:23: note:     Expected:
test.py:23: note:         def __call__(self, Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]], /) -> Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]
test.py:23: note:     Got:
test.py:23: note:         def __init__(app: Starlette, file_name: str = ...) -> FileLoggingMiddleware
test.py:23: note: "_MiddlewareFactory[[]].__call__" has type "Callable[[Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]], Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]]"

lonvia avatar May 30 '25 13:05 lonvia

Hit this as well while implementing a pure ASGI middleware using asgiref types.

MRE

# asgi_mre.py

from asgiref.typing import (
    ASGI3Application,
    ASGIReceiveCallable,
    ASGISendCallable,
    Scope,
)
from starlette.applications import Starlette
from starlette.middleware import Middleware


class MyMiddleware:
    def __init__(self, app: ASGI3Application) -> None:
        self.app = app

    async def __call__(
        self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable
    ) -> None: ...


app = Starlette(
    middleware=[Middleware(MyMiddleware)],
)

mypy

asgi_mre.py:21: error: Argument 1 to "Middleware" has incompatible type "type[MyMiddleware]"; expected "_MiddlewareFactory[[]]"  [arg-type]
asgi_mre.py:21: note: Following member(s) of "MyMiddleware" have conflicts:
asgi_mre.py:21: note:     Expected:
asgi_mre.py:21: note:         def __call__(self, Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]], /) -> Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]
asgi_mre.py:21: note:     Got:
asgi_mre.py:21: note:         def __init__(app: Callable[[HTTPScope | WebSocketScope | LifespanScope, Callable[[], Awaitable[HTTPRequestEvent | HTTPDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent | WebSocketDisconnectEvent | LifespanStartupEvent | LifespanShutdownEvent]], Callable[[HTTPResponseStartEvent | HTTPResponseBodyEvent | HTTPResponseTrailersEvent | HTTPServerPushEvent | HTTPDisconnectEvent | <9 more items>], Awaitable[None]]], Awaitable[None]]) -> MyMiddleware
asgi_mre.py:21: note: "_MiddlewareFactory[[]].__call__" has type "Callable[[Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]], Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]]"
Found 1 error in 1 file (checked 1 source file)

pyright

asgi_mre.py:21:28 - error: Argument of type "type[MyMiddleware]" cannot be assigned to parameter "cls" of type "_MiddlewareFactory[P@__init__]" in function "__init__"
    Type "type[MyMiddleware]" is not assignable to type "(app: ASGIApp, /) -> ASGIApp"
      Parameter 1: type "ASGIApp" is incompatible with type "ASGI3Application"
        Type "ASGIApp" is not assignable to type "ASGI3Application"
          Parameter 1: type "Scope" is incompatible with type "Scope"
            Type "asgiref.typing.Scope" is not assignable to type "starlette.types.Scope"
          Parameter 2: type "ASGIReceiveCallable" is incompatible with type "Receive"
            Type "ASGIReceiveCallable" is not assignable to type "Receive"
          Parameter 3: type "ASGISendCallable" is incompatible with type "Send"
    ... (reportArgumentType)
1 error, 0 warnings, 0 informations

Versions

  • Python: 3.10
  • Starlette: 0.47.0
  • mypy: 1.16.0
  • pyright: 1.1.401

frankie567 avatar Jun 08 '25 07:06 frankie567

Do we need to revert something?

Kludex avatar Jun 08 '25 15:06 Kludex

Do we need to revert something?

Actually, I'm not sure it even worked at some point actually. The _MiddlewareClass protocol was introduced in #2381 (released in 0.35.0) almost two years ago. The error in this version is slightly different:

mre.py:23: error: Argument 1 to "Middleware" has incompatible type "type[MyMiddleware]"; expected "type[_MiddlewareClass[[]]]"  [arg-type]

It passes in version 0.34.0, but that's expected, since there was no typing (i.e. any type).


The root problem (from what I understand) is that in Starlette, Scope and Message are just mappings:

https://github.com/encode/starlette/blob/78da9b9e218ab289117df7d62aee200ed4c59617/starlette/types.py#L12-L13

In asgiref, they are actually unions of TypedDict with strict definitions of each scope/message:

https://github.com/django/asgiref/blob/5eff04dce46b74172a1d6d1c5b02409f0afe33b6/asgiref/typing.py#L106-L107 https://github.com/django/asgiref/blob/5eff04dce46b74172a1d6d1c5b02409f0afe33b6/asgiref/typing.py#L227-L235

Surprisingly, a MutableMapping is apparently not compatible with a TypedDict: https://github.com/python/mypy/issues/4976

So, not really sure how to solve this, apart from using asgiref types in Starlette instead of the homemade types; but I guess it comes with a lot of problems too.

frankie567 avatar Jun 09 '25 11:06 frankie567

Another data point here: this only shows up after updating to mypy 1.16.0. So, either downgrading to mypy 1.15.0 or to starlette 0.46.2 makes the issue go away.

lonvia avatar Jun 09 '25 16:06 lonvia

Getting the same error when upgrading mypy:

care/main.py:54: error: Argument 1 to "add_middleware" of "Starlette" has incompatible type "Callable[[Request, Callable[[Request], Awaitable[Response]]], Coroutine[Any, Any, Response]]"; expected "_MiddlewareFactory[[]]"  [arg-type]
care/main.py:54: note: "_MiddlewareFactory[[]].__call__" has type "Callable[[Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]], Callable[[MutableMapping[str, Any], Callable[[], Awaitable[MutableMapping[str, Any]]], Callable[[MutableMapping[str, Any]], Awaitable[None]]], Awaitable[None]]]"
Found 1 error in 1 file (checked 1 source file)

In my case I'm defining a logging function with FastAPI according to their docs:

async def MyMiddleware(
    request: Request,
    call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
    pass

Besides a valid fix to the library, is there any way to patch this from my app code?

galah92 avatar Sep 22 '25 08:09 galah92