Mypy / Pep-484 对 ORM 映射的支持¶
支持 PEP 484 键入注释以及
使用 SQLAlchemy 时的 MyPy 类型检查工具
直接引用 Column
对象的声明性映射,而不是 SQLAlchemy 2.0 中引入的 mapped_column()
结构。
2.0 版后已废弃: SQLAlchemy Mypy 插件已废弃,可能会被删除
早在 SQLAlchemy 2.1 版本中。 我们敦促用户请
尽快离开它。 mypy 插件也只能工作到
mypy 版本 1.10.1。 版本 1.11.0 及更高版本可能无法正常工作。
此插件无法在不断变化的 mypy 版本中进行维护,并且无法保证其未来的稳定性。
现代 SQLAlchemy 现在提供
完全符合 PEP-484 的映射语法;有关迁移详细信息,请参阅链接部分。
安装¶
仅适用于 SQLAlchemy 2.0:不应安装存根,并且应完全卸载 sqlalchemy-stubs 和 sqlalchemy2-stubs 等软件包。
Mypy 包本身就是一个依赖项。
Mypy 可以使用 pip 的 “mypy” extras 钩子进行安装:
pip install sqlalchemy[mypy]
插件本身的配置如
使用 sqlalchemy.ext.mypy.plugin
模块名称(例如在
setup.cfg
中:
[mypy]
plugins = sqlalchemy.ext.mypy.plugin
插件的作用¶
Mypy 插件的主要目的是拦截和更改静态
SQLAlchemy 的定义
声明式映射,以便
它们与它们之后的结构相匹配
由其 Mapper
对象进行检测。这允许两者
类结构本身以及使用该类的代码对
Mypy 工具,否则根据声明性
mappings 当前有效。 该插件与类似的插件没有什么不同
这些库是必需的,例如
data类,它们在运行时动态地改变类。
要涵盖发生这种情况的主要区域,请考虑以下 ORM 映射,使用 User
类的典型示例:
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import declarative_base
# "Base" is a class that is created dynamically from the
# declarative_base() function
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
# "some_user" is an instance of the User class, which
# accepts "id" and "name" kwargs based on the mapping
some_user = User(id=5, name="user")
# it has an attribute called .name that's a string
print(f"Username: {some_user.name}")
# a select() construct makes use of SQL expressions derived from the
# User class itself
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))
以上,Mypy 扩展可以采取的步骤包括:
生成的Base
dynamic 类的解释declarative_base(),
以便已知从它继承的类被映射。它还可以适应使用 Decorator 的声明式映射中描述的类修饰器方法(无声明性基)。
以声明性“内联”样式定义的 ORM 映射属性的类型推断,在上面的示例中,是 User
类的 id 和name
属性。这包括
User
的实例将使用int
表示id,str
表示
name
。它还包括,当 访问User.id
和User.name
类级属性,如上面的select()
语句中所示,它们与 SQL 兼容 expression 行为,该行为派生自InstrumentedAttribute
属性描述符类。
将__init__()
方法应用于尚未包含显式构造函数的映射类,该构造函数接受检测到的所有映射属性的特定类型的关键字参数。
当 Mypy 插件处理上述文件时,传递给 Mypy 工具的结果静态类定义和 Python 代码等效于以下内容:
from sqlalchemy import Column, Integer, String, select
from sqlalchemy.orm import Mapped
from sqlalchemy.orm.decl_api import DeclarativeMeta
class Base(metaclass=DeclarativeMeta):
__abstract__ = True
class User(Base):
__tablename__ = "user"
id: Mapped[Optional[int]] = Mapped._special_method(
Column(Integer, primary_key=True)
)
name: Mapped[Optional[str]] = Mapped._special_method(Column(String))
def __init__(self, id: Optional[int] = ..., name: Optional[str] = ...) -> None: ...
some_user = User(id=5, name="user")
print(f"Username: {some_user.name}")
select_stmt = select(User).where(User.id.in_([3, 4, 5])).where(User.name.contains("s"))
上面采取的关键步骤包括:
Base
类现在根据DeclarativeMeta
定义 class 显式地表示,而不是动态类。id
和name
属性是根据Mapped
类,它表示一个 Python 描述符,该 在类级别和实例级别表现出不同的行为。 这Mapped
类现在是InstrumentedAttribute
的基类 类。Mapped
被定义为针对任意 Python 类型的泛型类,这意味着Mapped
的特定实例与特定的 Python 类型相关联,例如Mapped[Optional[int]]
和Mapped[Optional[str]]
的
声明性映射属性分配的右侧是 removed 的 API作,因为这类似于Mapper
class 通常会执行,也就是说,它将替换这些 具有特定InstrumentedAttribute
实例的 attributes 来获取。原始表达式被移动到函数调用中,这将允许它仍然进行类型检查,而不会与表达式的左侧冲突。对于 Mypy 目的,左侧键入的 Comments 足以理解 attribute 的行为。
添加了User.__init__()
方法的类型存根,其中包括正确的关键字和数据类型。
用法¶
以下小节将介绍迄今为止已考虑符合 pep-484 要求的各个用例。
基于 TypeEngine 的列内省¶
对于包含显式数据类型的映射列,当它们被映射为内联属性时,将自动内省映射类型:
class MyClass(Base):
# ...
id = Column(Integer, primary_key=True)
name = Column("employee_name", String(50), nullable=False)
other_name = Column(String(50))
上面是 id
、name
和
other_name
将被内省为 Mapped[Optional[int]]
,
Mapped[Optional[str]]
和 Mapped[Optional[str]]
来获取。默认情况下,这些类型始终被视为 Optional
,即使对于主键和不可为 null 的列也是如此。原因是虽然数据库列 “id” 和 “name” 不能为 NULL,但 Python 属性 id
和 name
肯定可以是 None
,而无需显式构造函数:
>>> m1 = MyClass()
>>> m1.id
None
上述列的类型可以明确说明,提供了两个优点,即更清晰的自我文档以及能够控制哪些类型是可选的:
class MyClass(Base):
# ...
id: int = Column(Integer, primary_key=True)
name: str = Column("employee_name", String(50), nullable=False)
other_name: Optional[str] = Column(String(50))
Mypy 插件将接受上述 int
、str
和 Optional[str]
并将它们转换为包含它们周围的 Mapped[]
类型。 这
Mapped[]
结构也可以显式使用:
from sqlalchemy.orm import Mapped
class MyClass(Base):
# ...
id: Mapped[int] = Column(Integer, primary_key=True)
name: Mapped[str] = Column("employee_name", String(50), nullable=False)
other_name: Mapped[Optional[str]] = Column(String(50))
当类型为非可选类型时,它仅表示从 MyClass
实例访问的属性将被视为非 None:
mc = MyClass(...)
# will pass mypy --strict
name: str = mc.name
对于 optional 属性,Mypy 认为类型必须包含 None 或为 Optional
:
mc = MyClass(...)
# will pass mypy --strict
other_name: Optional[str] = mc.name
无论 mapped 属性是否键入为 Optional
,__init__()
方法的生成仍将考虑所有关键字
设置为可选。这再次与 SQLAlchemy ORM 在创建构造函数时实际执行的作相匹配,并且不应与验证系统的行为(例如 Python 数据类
)混淆,后者将生成一个在可选属性与必需属性方面与 annotation 匹配的构造函数。
没有 Explicit 类型的列¶
包含 ForeignKey
修饰符的列不需要在 SQLAlchemy 声明性映射中指定数据类型。对于这种类型的 attribute,Mypy 插件会通知用户它需要一个显式的类型来发送:
# .. other imports
from sqlalchemy.sql.schema import ForeignKey
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id = Column(ForeignKey("user.id"))
该插件将按如下方式发送消息:
$ mypy test3.py --strict
test3.py:20: error: [SQLAlchemy Mypy plugin] Can't infer type from
ORM mapped expression assigned to attribute 'user_id'; please specify a
Python type or Mapped[<python type>] on the left hand side.
Found 1 error in 1 file (checked 1 source file)
要解决此问题,请将显式类型注释应用于 Address.user_id
列:
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
使用 Imperative Table 映射列¶
在命令式表格样式中,
列
定义在 Table
中给出
构造,它与映射的属性本身是分开的。 Mypy 餐厅
plugin 不考虑这个 Table
,而是支持
这些属性可以通过一个完整的注解来明确声明,该注解
必须使用 Mapped
类将它们标识为 Map 属性:
class MyClass(Base):
__table__ = Table(
"mytable",
Base.metadata,
Column(Integer, primary_key=True),
Column("employee_name", String(50), nullable=False),
Column(String(50)),
)
id: Mapped[int]
name: Mapped[str]
other_name: Mapped[Optional[str]]
上述 Mapped
注解被视为映射列,并将包含在默认构造函数中,并在类级别和实例级别为 MyClass
提供正确的类型配置文件。
映射关系¶
该插件对使用类型推理来检测关系类型的支持有限。对于无法检测到类型的所有情况,它将发出一条信息性错误消息,并且在所有情况下都可以显式提供适当的类型,或者使用 Mapped
类,也可以为内联声明省略它。 插件
还需要确定关系是否引用集合
或标量,为此它依赖于
relationship.uselist
和/或 relationship.collection_class
参数。 如果这些参数都不是
存在,以及 relationship()
的目标类型
是字符串或可调用对象,而不是类:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user = relationship(User)
上述映射将产生以下错误:
test3.py:22: error: [SQLAlchemy Mypy plugin] Can't infer scalar or
collection for ORM mapped expression assigned to attribute 'user'
if both 'uselist' and 'collection_class' arguments are absent from the
relationship(); please specify a type annotation on the left hand side.
Found 1 error in 1 file (checked 1 source file)
可以使用 relationship(User, uselist=False)
或者通过提供类型(在本例中为标量 User
对象):
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: User = relationship(User)
对于集合,类似的模式适用,其中在没有
uselist=True
或relationship.collection_class
,则可以使用集合注释,例如 List
。在 pep-484 支持的注解中使用类的字符串名称也是完全合适的,确保在 TYPE_CHECKING 块中导入类
视情况而定:
from typing import TYPE_CHECKING, List
from .mymodel import Base
if TYPE_CHECKING:
# if the target of the relationship is in another module
# that cannot normally be imported at runtime
from .myaddressmodel import Address
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
addresses: List["Address"] = relationship("Address")
与列一样,也可以显式应用 Mapped
类:
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
name = Column(String)
addresses: Mapped[List["Address"]] = relationship("Address", back_populates="user")
class Address(Base):
__tablename__ = "address"
id = Column(Integer, primary_key=True)
user_id: int = Column(ForeignKey("user.id"))
user: Mapped[User] = relationship(User, back_populates="addresses")
使用 @declared_attr 和声明式 mixin¶
declared_attr
类允许 Declarative 映射属性
在类级函数中声明,并且在使用
声明式 mixin 中。对于这些函数,函数的返回类型应该使用 Mapped[]
construct 或通过指示函数返回的对象的确切类型来构建。
此外,未以其他方式映射的 “mixin” 类(即不扩展
从 declarative_base()
类中,它们也没有使用 registry.mapped()
) 等方法进行映射,则应使用
declarative_mixin()
装饰器,它向 Mypy 插件提供一个提示,表明特定类打算用作声明性 mixin:
from sqlalchemy.orm import declarative_mixin, declared_attr
@declarative_mixin
class HasUpdatedAt:
@declared_attr
def updated_at(cls) -> Column[DateTime]: # uses Column
return Column(DateTime)
@declarative_mixin
class HasCompany:
@declared_attr
def company_id(cls) -> Mapped[int]: # uses Mapped
return mapped_column(ForeignKey("company.id"))
@declared_attr
def company(cls) -> Mapped["Company"]:
return relationship("Company")
class Employee(HasUpdatedAt, HasCompany, Base):
__tablename__ = "employee"
id = Column(Integer, primary_key=True)
name = Column(String)
请注意,像
HasCompany.company
与注释的内容。Mypy 插件将所有 @declared_attr
函数转换为简单的带注释的属性,以避免这种复杂性:
# what Mypy sees
class HasCompany:
company_id: Mapped[int]
company: Mapped["Company"]
与数据类或其他类型敏感的属性系统相结合¶
将 ORM 映射应用于现有数据类(使用旧数据类)中的 Python 数据类集成示例
提出了一个问题;Python 数据类需要一个显式类型,它将
use 构建类,每个赋值语句中给出的值
非常重要。 也就是说,必须准确说明以下类
因为它是为了被 Dataclasses 接受:
mapper_registry: registry = registry()
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str] = None
nickname: Optional[str] = None
addresses: List[Address] = field(default_factory=list)
__mapper_args__ = { # type: ignore
"properties": {"addresses": relationship("Address")}
}
我们不能将 Mapped[]
类型应用于属性 id
、name
等,因为它们会被 @dataclass
装饰器拒绝。此外,Mypy 还有另一个显式的数据类插件,它也可能妨碍我们正在做的事情。
上面的类实际上会毫无问题地通过 Mypy 的类型检查;我们唯一缺少的是 User
上的属性能够在 SQL 表达式中使用,例如:
stmt = select(User.name).where(User.id.in_([1, 2, 3]))
为了提供解决方法,Mypy 插件有一个额外的功能,我们可以指定一个额外的属性_mypy_mapped_attrs
,即一个包含类级对象或其字符串名称的列表。此属性可以是 TYPE_CHECKING
变量中的条件:
@mapper_registry.mapped
@dataclass
class User:
__table__ = Table(
"user",
mapper_registry.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(50)),
Column("fullname", String(50)),
Column("nickname", String(12)),
)
id: int = field(init=False)
name: Optional[str] = None
fullname: Optional[str]
nickname: Optional[str]
addresses: List[Address] = field(default_factory=list)
if TYPE_CHECKING:
_mypy_mapped_attrs = [id, name, "fullname", "nickname", addresses]
__mapper_args__ = { # type: ignore
"properties": {"addresses": relationship("Address")}
}
使用上述配方,_mypy_mapped_attrs
将与 Mapped
键入信息一起应用,以便
在
类绑定上下文中使用时,User 类的行为将类似于 SQLAlchemy 映射类。