过滤方法表现异常

Filter method is behaving unexpectedly

我正在尝试将类型提示引入现有代码库,但是当我尝试输入查询时 运行 遇到了问题。

from sqlalchemy.orm.query import Query

class DbContext:
    def __init__(self, db_host, db_port, db_name, db_user, db_password):

        engine = create_engine(...)

        session = sessionmaker(bind=engine)
        self.Session: Session = session(bind=engine)

...

def fetch(context: DbContext, filters: ...):
    sub_query: Query = context.Session.query(...)

在我添加类型提示之前,动态过滤只是一个问题:

if filters.name is not None:
    sub_query = sub_query.filter(
        Person.name.ilike(f"%{filters.name}%"))

但是,现在提示我收到此错误:

Expression of type "None" cannot be assigned to declared type "Query"

果然filter出现在return None:

(method) filter: (*criterion: Unknown) -> None

我导航到 the source,看来该方法确实没有 return 任何东西。

def filter(self, *criterion):
    for criterion in list(criterion):
        criterion = expression._expression_literal_as_text(criterion)

        criterion = self._adapt_clause(criterion, True, True)

        if self._criterion is not None:
            self._criterion = self._criterion & criterion
        else:
            self._criterion = criterion

显然某处断开连接,因为将 None 分配给 sub_query 会导致提示警告的错误,但我需要执行分配才能使过滤真正起作用:

# Does NOT work, filtering is not applied
if filters.name is not None:
  sub_query.filter(
               Person.name.ilike(f"%{filters.name}%"))

# Works but Pylance complains
if filters.name is not None:
  sub_query = sub_query.filter(
               Person.name.ilike(f"%{filters.name}%"))

这是我第一次涉足 Python,希望得到一些关于这里发生的事情的指导!

您遗漏了两件事:

  • 您需要为 SQLAlchemy 安装 typing stubs
  • Query.filter() 方法有一个装饰器,用于定义什么是 returned。

SQLAlchemy 的输入存根

您想安装 sqlalchemy-stubs project,它为 SQLAlchemy API 提供存根。

请注意,即使安装了这个存根,您仍然会看到 Pyright(支持 Pylance 扩展的检查工具)的问题,因为静态存根不能完全代表某些组件的动态特性SQLAlchemy API 的部分,例如模型列定义(例如,如果您的 Person 模型有一个名为 name 的列,用 name = Column(String) 定义,那么存根无法分辨Pyright name 将是一个字符串)。 sqlalchemy-stubs 项目包含一个用于 mypy 类型检查器的插件,以更好地处理动态部分,但此类插件不能与其他类型检查器一起使用。

安装存根后,Pylance 可以告诉您有关 filter:

Query.filter() 装饰器详细信息

Query.filter()方法实现实际上并不是对原始实例对象进行操作;它已被注释为 decorator:

    @_generative(_no_statement_condition, _no_limit_offset)
    def filter(self, *criterion):
        ...

@_generative(...) 部分在这里很重要; definition of the decorator factory 显示 filter() 方法基本上被此 包装方法 :

取代
    def generate(fn, *args, **kw):
        self = args[0]._clone()
        for assertion in assertions:
            assertion(self, fn.__name__)
        fn(self, *args[1:], **kw)
        return self

这里,fn是原始的filter()方法定义,args[0]是对self的引用,是初始的Query实例。所以 self 被调用 self._clone() 取代(基本上,创建一个新实例并复制属性),它运行声明的断言(这里, _no_statement_condition_no_limit_offset 是此类断言),在 运行 原始函数 之前

因此,filter() 函数的作用是 就地更改克隆的实例 ,因此不必 return 任何东西;这是由 generate() 包装器处理的。正是这种用实用程序包装器交换方法的技巧让 Pyright 误以为 None 是 returned,但安装存根后它知道另一个 Query 实例是 returned相反。