FastAPI 完整CBV实现

作者: Gascognya | 来源:发表于2020-10-17 18:01 被阅读0次
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
能够正确发现依赖

相关文章

  • FastAPI 完整CBV实现

    10.23更新:装饰器做了增强 就FastAPI的CBV实现,之前出了篇文章。做了个简单的实现方法。今天逛gith...

  • FastAPI CBV的实现

    FastAPI天生不支持CBV 在Starlette层面上,还提供有CBV的支持,但是在FastAPI的实现,都是...

  • FastAPI WebSocket CBV实现

    FastAPI的WebSocket也未提供CBV方案。造个轮子WebSocketCBV是对starlette的We...

  • 2019-01-02

    CBV实现登录验证 <1>FBV示例代码 views.py urls.py login.html <2> CBV示...

  • FastAPI 官方文档解读 (一)

    安装方式 标准安装 pip install fastapi & pip install uvicorn 完整安装 ...

  • Django-视图CBV执行流程详解

    FBV和CBV其实就是要么写函数要么写类,那么内部原理是怎么实现? FBV写法 CBV写法 原理 FBV可以直接通...

  • Django中的CBV

    django中请求处理方式有2种:FBV 和 CBV CBV CBV(class base views) 就是在视...

  • 第三天

    drf开发商品列表页 django的view实现商品列表页&自带的serializer cbv => class ...

  • Starlette 源码阅读 (七) endpoint

    endpoints.py 关于cbv的源码,分http的cbv和ws的cbv两种 HTTPEndpoint类 We...

  • 1.FastAPI介绍

    一、 Fastapi是什么 FastAPI 框架,高性能,易于学习,高效编码,生产可用 FastAPI 是一个用于...

网友评论

    本文标题:FastAPI 完整CBV实现

    本文链接:https://www.haomeiwen.com/subject/ekrfmktx.html