Flask 路由装饰器类型提示

Flask route decorator type hinting

我有一个简单的 Flask 应用程序,我在其中添加了一个装饰器以确保特定的 header 出现在每个请求中。

import functools
from http import HTTPStatus

import flask
from flask.typing import ResponseReturnType

app = flask.Flask(__name__)


# What type hints should be added to this?
def requires_header(func):
    @functools.wraps(func)
    def check_headers(*args, **kwargs):
        if not flask.request.headers.get("X-Foo"):
            flask.abort(HTTPStatus.NOT_FOUND)

        return func(*args, **kwargs)

    return check_headers


@app.route("/", methods=["GET"])
@requires_header
def root() -> ResponseReturnType:
    return flask.jsonify(success=True)


if __name__ == "__main__":
    flask.run(host=0.0.0.0, port=3000)

我应该向 requires_header 装饰器添加什么类型的提示?

您的装饰器接受一个可调用参数(任何类型),并且 returns 一个完全相同类型的可调用参数。因此:

from typing import Any, Callable, TypeVar

_C = TypeVar("_C", bound=Callable[..., Any])

def requires_header(func: _C) -> _C:
    ...

请注意,这与执行以下操作不同:

def requires_header(func: Callable) -> Callable:
    ...

因为这不能保证装饰函数与原始函数具有相同的类型(它实际上变成了两个 不同的 Callable[..., Any] 类型,这会破坏类型检查任何调用该函数的东西)。使用 TypeVar 允许原始类型不变地通过装饰器。

如果您是运行 Mypy 并设置了--strict,以下是目前 的最佳输入方式。这与 , but note that the TypeVar is bound to Callable[..., Any] rather than a bare Callable (Mypy --strict will not allow any unparameterised generics, even as arguments to a TypeVar). We also have to help Mypy out when it comes to the return type of requires_header — it can't verify that we're actually returning the type we say we should be returning, so we help Mypy out with a call to typing.cast.

非常相似
import functools
from http import HTTPS
from typing import Callable, TypeVar, cast, Any 

import flask

C = TypeVar('C', bound=Callable[..., Any])

def requires_header(func: C) -> C:
    @functools.wraps(func)
    def check_headers(*args: Any, **kwargs: Any) -> Any:
        if not flask.request.headers.get("X-Foo"):
            flask.abort(HTTPStatus.NOT_FOUND)

        return func(*args, **kwargs)

    return cast(C, check_headers)

上面的解决方案显然有它的缺陷,但是,主要是我们不得不使用的次数Any,一般应该尽量少用。 Python 3.10 有 introduced a new feature 专门用于帮助键入装饰器,ParamSpec。不幸的是,Mypy 还不支持这个功能,但是当它支持时,我们将能够像这样输入你的装饰器:

import functools
from http import HTTPS
from typing import Callable, TypeVar, cast, ParamSpec 

import flask

P = ParamSpec('P')
R = TypeVar('R')

def requires_header(func: Callable[P, R]) -> Callable[P, R]:
    @functools.wraps(func)
    def check_headers(*args: P.args, **kwargs: P.kwargs) -> R:
        if not flask.request.headers.get("X-Foo"):
            flask.abort(HTTPStatus.NOT_FOUND)

        return func(*args, **kwargs)

    return cast(Callable[P, R], check_headers)

可能使用 ParamSpec 也将消除装饰器最后一行中对 cast 的需要——但这很难说,因为 Mypy 尚不支持这个功能,所以遗憾的是它还不能用!