functools.wraps新增session参数时适配Strawberry报错求解
我希望实现数据库会话自动注入的装饰器逻辑,最初的实现代码如下:
def need_session(func): @wraps(func) def wrapper(*args, **kwargs): session = SessionLocal() try: func(session, *args, **kwargs) finally: session.close()
我使用wraps函数是因为项目采用Strawberry框架开发GraphQL服务,该框架强依赖函数的参数类型注解。
按照上述逻辑,被装饰的原函数会被额外传入一个session参数,但实际运行时出现异常:使用该装饰器修饰Strawberry的Mutation字段函数启动服务时,抛出如下错误:
TypeError: Mutation fields cannot be resolved. Unexpected type '<class 'sqlalchemy.orm.session.Session'>'
之后我调整装饰器实现,改为将session通过关键字参数传入:
def need_session(func): @wraps(func) def wrapper(*args, **kwargs): session = SessionLocal() kwargs['session'] = session try: func(*args, **kwargs) finally: session.close()
同时将被装饰函数的参数列表修改为如下形式:
def some_func(some_arg: SomeClass, **kwargs): ...
此时运行服务又抛出新的错误:
strawberry.exceptions.MissingArgumentsAnnotationsError: Missing annotation for argument "kwargs" in field "login_user", did you forget to add it?
出现这两个错误的核心原因是Strawberry在解析字段解析函数时,会完整读取函数签名和所有参数的类型注解,将所有公开参数识别为GraphQL字段的入参,两种写法分别触发了框架的校验规则:
- 第一种写法中,
session参数直接暴露在函数签名里,类型为SQLAlchemy的Session,Strawberry会尝试将其映射为GraphQL支持的输入类型,但Session不属于合法的GraphQL输入类型,因此抛出类型不识别错误。 - 第二种写法中,使用了无类型注解的
**kwargs参数,Strawberry要求解析函数的所有公开参数都必须有明确的类型注解,因此抛出缺少注解的错误。
核心思路是在装饰器中主动修改包装函数的对外签名,将内部注入的session参数从公开签名中移除,让Strawberry无法感知到这个内部参数,就不会将其作为GraphQL入参做校验。可以借助Python标准库inspect模块实现:
from functools import wraps from inspect import signature # 从项目自身的数据库配置模块导入SessionLocal from db import SessionLocal def need_session(func): # 获取原函数的签名 original_sig = signature(func) # 过滤掉名为session的参数,生成对外暴露的新签名 public_params = [ param for param_name, param in original_sig.parameters.items() if param_name != "session" ] public_sig = original_sig.replace(parameters=public_params) @wraps(func) def wrapper(*args, **kwargs): session = SessionLocal() try: # 执行原函数时注入session,必须return返回值否则GraphQL响应会为空 return func(session=session, *args, **kwargs) finally: session.close() # 将包装函数的签名替换为过滤后的公开签名 wrapper.__signature__ = public_sig return wrapper
使用时不需要写**kwargs,直接在被装饰的解析函数里正常声明session: Session参数即可:
import strawberry from sqlalchemy.orm import Session # 导入自定义的响应类型、数据模型 from schema.types import LoginResponse from models import User @strawberry.type class Mutation: @strawberry.mutation @need_session def login_user(self, username: str, password: str, session: Session) -> LoginResponse: # 直接使用注入的session操作数据库即可 user = session.query(User).filter(User.username == username).first() # 业务逻辑处理... return LoginResponse(success=True)
这种实现下Strawberry读取到的解析函数签名只包含username、password两个公开参数,不会感知到session的存在,既不会报类型映射错误,也不会报参数注解缺失错误。
内容的提问来源于stack exchange,提问作者pooooky

