Just relax, take it easy..

0%

SQLAlchemy 与 Pyright 相爱相杀的故事

Pyright 作为一款功能强大的静态类型检查器,深得我心。但近日使用 SQLAlchemy 时,Pyright 提示的类型检查报错,却让我陷入困扰。

本篇文章将简单分享解决思路以及背后的原理。

背景

假设有如下 python 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String

Base = declarative_base()


class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)


def print_username(name: str):
print(name)


if __name__ == "__main__":
user = User(id=42, name=42)
print_username(user.name)

Pyright 认真负责给出如下报错提示:[Pyright reportGeneralTypeIssues] Argument of type "Column[str]" cannot be assigned to parameter "name" of type "str" in function "print_username" [E]

解决办法

很简单,本地安装 sqlalchemy-stubs 后,报错消失。

Pyright 可正确识别 user 实例的 name 属性为 str 类型,而不是代码定义的 Column 类型。

小技巧:无需引用,直接使用 reveal_type 方法调试类型:

WHY???

为什么安装 stub 包后,无需任何配置,即可直接对 pyright 生效?

Try to resolve using stubs or inlined types found within installed packages.

For a given package, try to resolve first using a stub package. Stub packages, as defined in PEP 561, are named the same as the original package but with “-stubs” appended.

参考 pyright 文档,默认根据包名 -stubs 后缀自动识别。

为什么安装 stub 包后,可正确识别 name 的类型?

首先安装前,pyright 根据 sqlalchemy 的源代码,解析对应的类型,自然将 user.name 当作 Column 类型。安装 sqlalchemy-stubs 后,优先通过 stub 中定义的接口类型解析。

具体实现参考 sqlalchemy-stubs/sql/schema.pyi::Column,关键代码如下。

简而言之类似 java 中的泛型(generics),当 Column 的类型定义为 Type[TypeEngine[_T]] 时,强制约束返回的类型为 T

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
_T = TypeVar('_T')

class Column(SchemaItem, ColumnClause[_T]):
__visit_name__: str = ...
key: str = ...
primary_key: bool = ...
nullable: bool = ...
default: Optional[Any] = ...
server_default: Optional[Any] = ...
server_onupdate: Optional[FetchedValue] = ...
index: Optional[bool] = ...
unique: Optional[bool] = ...
system: bool = ...
doc: Optional[str] = ...
onupdate: Optional[Any] = ...
autoincrement: Union[bool, str] = ...
constraints: Set[Constraint] = ...
foreign_keys: Set[ForeignKey] = ... # type: ignore # incompatible with ColumnElement.foreign_keys
info: Optional[Mapping[str, Any]] = ...
comment: Optional[str] = ...
table: Table = ... # TODO: double-check this.

# Now without a name argument.
@overload
def __init__(self, type_: Type[TypeEngine[_T]], *args: Any, autoincrement: Union[bool, str] = ...,
default: Any = ..., doc: str = ..., key: str = ..., index: bool = ..., info: Mapping[str, Any] = ...,
nullable: bool = ..., onupdate: Any = ..., primary_key: bool = ..., server_default: Any = ...,
server_onupdate: Union[FetchedValue, FunctionElement] = ..., quote: Optional[bool] = ..., unique: bool = ...,
system: bool = ..., comment: str = ...) -> None: ...

@overload
def __get__(self, instance: None, owner: Any) -> Column[_T]: ...
@overload
def __get__(self, instance: object, owner: Any) -> _T: ...

参考

  1. https://microsoft.github.io/pyright/#/type-concepts?id=debugging-types
  2. https://github.com/dropbox/sqlalchemy-stubs/issues/140
  3. https://github.com/microsoft/pyright/blob/main/docs/import-resolution.md