FastAPI 完整CBV实现

10.23更新:装饰器做了增强

FastAPI的CBV实现,之前出了篇文章。做了个简单的实现方法。
今天逛github发现有牛人造了个FastAPI的tools包。

https://github.com/dmontagu/fastapi-utils

里面有关于CBV的实现,这位对于反射的理解让我非常佩服。抽空在其代码上做了点简化。因为并非自己原创,不好意思贴回github。在此贴出。

首先我加了个CBV专用的Router类,可以被APIRouter所include。
class CBVRouter(Router):
    def __init__(
            self,
            app: FastAPI,
            path: str,
            group_name: str,
            tags: Optional[List[str]] = None,
            description: Optional[str] = None,
            summary: Optional[str] = None,
            routes: Optional[List[routing.BaseRoute]] = None,
            redirect_slashes: bool = True,
            default: Optional[ASGIApp] = None,
            dependency_overrides_provider: Optional[Any] = None,
            route_class: Type[APIRoute] = APIRoute,
            default_response_class: Optional[Type[Response]] = None,
            on_startup: Optional[Sequence[Callable]] = None,
            on_shutdown: Optional[Sequence[Callable]] = None,
    ) -> None:
        """

        :param app: FastAPI的APP
        :param group_name: 配置一个CBV的方法们独有的名字,方便标识。

        :param path: 整合参数,只能在此输入,必填
        :param tags: 整合参数,默认值是group_name
        :param description: 整合参数,只能在此输入
        :param summary: 整合参数,只能在此输入,默认值是group_name_方法名


        """
        super().__init__(
            routes=routes,
            redirect_slashes=redirect_slashes,
            default=default,
            on_startup=on_startup,
            on_shutdown=on_shutdown,
        )
        self.dependency_overrides_provider = dependency_overrides_provider
        self.route_class = route_class
        self.default_response_class = default_response_class

        self.app = app
        self.path = path
        self.name = group_name
        self.tags = tags or [group_name]
        self.description = description
        self.summary = summary

    def method(
            self,
            response_model: Optional[Type[Any]] = None,
            status_code: int = 200,
            summary: Optional[str] = None,
            tags: Optional[List[str]] = [],
            response_description: str = "Successful Response",
            dependencies: Optional[Sequence[params.Depends]] = None,
            responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
            deprecated: Optional[bool] = None,
            response_model_include: Optional[Union[SetIntStr, DictIntStrAny]] = None,
            response_model_exclude: Optional[Union[SetIntStr, DictIntStrAny]] = None,
            response_model_by_alias: bool = True,
            response_model_exclude_unset: bool = False,
            response_model_exclude_defaults: bool = False,
            response_model_exclude_none: bool = False,
            include_in_schema: bool = True,
            response_class: Optional[Type[Response]] = None,
            name: Optional[str] = None,
            callbacks: Optional[List[APIRoute]] = None,
    ) -> Callable:
        def decorator(func: Callable) -> Callable:
            method = getattr(func, "__name__", None)
            assert method, "装饰器使用方式错误"

            assert method in ['get', 'post', 'put', 'delete', 'options', 'head', 'patch', 'trace'], 
                "请将方法名配置为' HTTP METHOD '中的一个"

            tags.extend(self.tags)

            route_class = self.route_class
            route = route_class(
                self.path,
                endpoint=func,
                response_model=response_model,
                status_code=status_code,

                tags=tags,
                description=self.description,
                methods=[method],
                operation_id=f'{self.name}_{self.path[1:]}_{method}',
                summary=summary or f'{self.name} _ {method}',

                dependencies=dependencies,
                deprecated=deprecated,
                response_description=response_description,
                responses=responses or {},
                response_model_include=response_model_include,
                response_model_exclude=response_model_exclude,
                response_model_by_alias=response_model_by_alias,
                response_model_exclude_unset=response_model_exclude_unset,
                response_model_exclude_defaults=response_model_exclude_defaults,
                response_model_exclude_none=response_model_exclude_none,
                include_in_schema=include_in_schema,
                response_class=response_class or self.default_response_class,
                name=name,
                dependency_overrides_provider=self.dependency_overrides_provider,
                callbacks=callbacks,
            )
            self.routes.append(route)

            return func

        return decorator
  • method是替代@app.get等八个方法的装饰器,具体是什么方法取决于函数名。
@router.method()
def get(self):
-----------------------------
@app.get(path)
def xxx():

即上述两者是等价的

  • __init__对部分属性进行了整合,不必在装饰器里输入。因为cbv的方法之间是具有部分共性的。例如tags之类的。
接下来的部分是对原代码的小修改
T = TypeVar("T")

def API(item: object):
    """
    我们为了灵活性,设立了主动和被动两种模式。
    主动模式代表,将FastAPI的app传入到router,router自动实现挂载。
    # app = FastAPI()
    # @CBV
    # class TestClass:
    #     router = CBVRouter(app=app, path="/user", group_name="User")

    如果本模块是启动的主模块,这样是一个好的选择。

    被动模式代表,提供router,包含app的主模块import这个router,交由它们去处理!
    这样我们不再期待获得app,并且我们需要暴露一个router
    # router = CBVRouter(path="/user", group_name="User")
    # @CBV(router)
    # class TestClass:

    这样我认为是一个比较好的方式。所以主动与被动的最大区别是写法。
    故这个装饰器,包含两种使用方式: @CBV 与 @CBV(router)

    :param item: 可能代表router或者decorator传入的cls
    :return: 一个decorator,或者一个cls
    """
    if isinstance(item, CBVRouter):
        router = item

        def decorator(cls: Type[T]):
            _get_method(cls, router)
            return cls

        return decorator

    # --------要求配置Router以及指定App--------
    else:
        cls = item
        router = None
        for attr in cls.__dict__.values():
            if isinstance(attr, CBVRouter):
                router = attr
        assert router, "请配置一个Router到类属性router"
        app = getattr(router, 'app')
        assert app, "请指定要挂载的app"

        _get_method(cls, router)
        app.include_router(router)
        return cls
    
def _get_method(cls, router):
    """抽离的公共代码"""
    # ------------修改__init__签名------------
    update_cbv_class_init(cls)

    # ----------------抓取方法----------------
    function_members = inspect.getmembers(cls, inspect.isfunction)
    functions_set = set(func for _, func in function_members)

    def temp(r):
        if isinstance(r, (Route, WebSocketRoute)) and r.endpoint in functions_set:
            _update_endpoint_self_param(cls, r)
            return True
        return False

    router.routes = list(filter(temp, router.routes))


def update_cbv_class_init(cls: Type[Any]) -> None:
    """
    重定义类的__init__(), 更新签名和参数
    """
    CBV_CLASS_KEY = "__cbv_class__"

    if getattr(cls, CBV_CLASS_KEY, False):
        return  # Already initialized

    old_init: Callable[..., Any] = cls.__init__
    old_signature = inspect.signature(old_init)
    old_parameters = list(old_signature.parameters.values())[1:]

    new_parameters = [
        x for x in old_parameters 
        if x.kind not in 
           (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
    ]

    dependency_names: List[str] = []
    for name, hint in get_type_hints(cls).items():
        if is_classvar(hint):
            continue
        parameter_kwargs = {"default": getattr(cls, name, Ellipsis)}
        dependency_names.append(name)
        new_parameters.append(
            inspect.Parameter(
                name=name, 
                kind=inspect.Parameter.KEYWORD_ONLY, 
                annotation=hint, 
                **parameter_kwargs
            )
        )
    new_signature = old_signature.replace(parameters=new_parameters)

    def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
        for dep_name in dependency_names:
            dep_value = kwargs.pop(dep_name)
            setattr(self, dep_name, dep_value)
        old_init(self, *args, **kwargs)

    setattr(cls, "__signature__", new_signature)
    setattr(cls, "__init__", new_init)
    setattr(cls, CBV_CLASS_KEY, True)


def _update_endpoint_self_param(cls: Type[Any], route: Union[Route, WebSocketRoute]) -> None:
    """
    调整endpoint的self参数,使其变为self=Depends(cls)
    这样每次处理依赖时,就可以实例化一个对象
    """
    old_endpoint = route.endpoint
    old_signature = inspect.signature(old_endpoint)
    old_parameters: List[inspect.Parameter] = list(old_signature.parameters.values())
    old_first_parameter = old_parameters[0]
    new_first_parameter = old_first_parameter.replace(default=Depends(cls))
    new_parameters = [new_first_parameter] + [
        parameter.replace(kind=inspect.Parameter.KEYWORD_ONLY) for parameter in old_parameters[1:]
    ]
    new_signature = old_signature.replace(parameters=new_parameters)
    setattr(route.endpoint, "__signature__", new_signature)

使用示例

主动方式:
  • CBV类使用@API装饰器

  • 在类属性中建一个CBVRouter类的实例 (名字随意,会根据类型搜索)

  • app, path, group_name三者是必填参数,因为这次没用metaclass拿不到类名,所以要自己指定个名字,方便api文档来标识。

  • 可以定义__init__,它最终会在endpoint中以(self=Depends(cls), ...)方式提供,这类似于spring的依赖注入。

  • 可以在类属性中添加参数(包括Depends()),他们最终会被整合到__init__的参数中

  • 我们我们可以理解为,从类中的一个方法,变成Depends一个类实例的函数。

def dependency(num: int) -> int:
    return num

app = FastAPI()

@API
class TestClass:
    router = CBVRouter(app, path="/user", group_name="User")

    x: int = Depends(dependency)
    cx: ClassVar[int] = 1
    cy: ClassVar[int]

    def __init__(self, z: int = Depends(dependency)):
        self.y = 1
        self.z = z

    @router.method(response_model=int)
    def get(self) -> int:
        return self.cx + self.x + self.y + self.z

    @router.method(response_model=bool)
    def post(self) -> bool:
        return hasattr(self, "cy")

    @router.method()
    def put(self):
        return {"msg": "put"}
    
    @router.method()
    def delete(self):
        return {"msg": "delete"}
被动方式
  • 方便被其他位置引入挂载
router = CBVRouter(path="/user", group_name="User")

@API(router)
class TestClass:
    x: int = Depends(dependency)
    cx: ClassVar[int] = 1
    cy: ClassVar[int]
    ......
successful

能够正确发现依赖
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,009评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,808评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,891评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,283评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,285评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,409评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,809评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,487评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,680评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,499评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,548评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,268评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,815评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,872评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,102评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,683评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,253评论 2 341