Mypy/Pep-484 对 ORM 映射的支持
支持 PEP 484 键入批注以及 Mypy 类型检查工具。
注解
Mypy插件和键入注释应该被视为 Alpha级别 用于SQLAlChemy的早期1.4版本。该插件尚未在真实场景中进行测试,可能存在许多未处理的情况和错误情况。新类型存根的详细信息还包括 如有更改,可随时更改 在1.4系列期间。
安装
Mypy插件依赖于打包在 sqlalchemy2-stubs 。这些存根必须完全替换以前的 sqlalchemy-stubs
键入Dropbox发布的批注,因为它们占用相同的 sqlalchemy-stubs
指定的命名空间 PEP 561 。这个 Mypy 包本身也是一个依赖项。
这两个软件包都可以通过使用pip::的“mypy”附加挂接进行安装。
pip install sqlalchemy[mypy]
插件本身的配置如中所述 Configuring mypy to use Plugins ,使用 sqlalchemy.ext.mypy.plugin
模块名称,如内 setup.cfg
::
[mypy] plugins = sqlalchemy.ext.mypy.plugin
插件的作用
Mypy插件的主要目的是拦截和更改SQLAlChemy的静电定义 declarative mappings 从而使它们与它们在被构造之后的结构相匹配 instrumented 由他们的 Mapper
对象。这允许类结构本身以及使用类的代码对Mypy工具有意义,否则,根据声明性映射当前的工作方式,就不会出现这种情况。该插件与库所需的类似插件没有什么不同,例如 dataclasses 它在运行时动态更改类。
要涵盖发生这种情况的主要区域,请考虑下面的ORM映射,使用 User
班级::
from sqlalchemy import Column from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import 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
由生成的动态类declarative_base()
,因此从它继承的类是已知映射的。中描述的类装饰器方法。 使用修饰符的声明性映射(无声明基) 。以声明性“内联”样式定义的ORM映射属性的类型推断,在上面的示例中
id
和name
对象的属性User
班级。这包括User
将使用int
为id
和str
为name
。它还包括当User.id
和User.name
访问类级属性,因为它们位于select()
语句,它们与SQL表达式行为兼容,而SQL表达式行为派生自InstrumentedAttribute
属性描述符类。AN的应用
__init__()
方法应用于尚未包含显式构造函数的映射类,显式构造函数接受检测到的所有映射属性的特定类型的关键字参数。
当Mypy插件处理上述文件时,传递给Mypy工具的静电类定义和Python代码等同于以下内容:
from sqlalchemy import Column from sqlalchemy import Integer from sqlalchemy import String from sqlalchemy import select from sqlalchemy.orm import declarative_base from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm import Mapped 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
类,而不是动态类。这个
id
和name
属性是根据Mapped
类,它表示在类与实例级别上表现出不同行为的Python描述符。这个Mapped
类现在是InstrumentedAttribute
用于所有ORM映射属性的类。在……里面
sqlalchemy2-stubs
,Mapped
定义为针对任意Python类型的泛型类,这意味着Mapped
与特定的Python类型相关联,例如Mapped[Optional[int]]
和Mapped[Optional[str]]
上面。声明性映射属性赋值的右侧为 已删除 ,因为这类似于
Mapper
类通常执行的操作,即它将用特定的InstrumentedAttribute
。原始表达式被移到函数调用中,这样仍然可以对其进行类型检查,而不会与表达式的左侧冲突。对于Mypy而言,左侧的键入注释就足以理解属性的行为。类的类型存根。
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
,即使对于主键和不可为空的列也是如此。原因是虽然数据库列“id”和“name”不能为空,但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[]
CONTACTION也可以明确使用::
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
将被视为非无::
mc = MyClass(...) # will pass mypy --strict name: str = mc.name
对于可选属性,Mypy认为类型必须包括None,否则 Optional
::
mc = MyClass(...) # will pass mypy --strict other_name: Optional[str] = mc.name
映射属性的类型是否为 Optional
,新一代的 __init__()
方法将 仍然认为所有关键字都是可选的 。这再次与SQLAlChemy ORM在创建构造函数时实际执行的操作相匹配,不应与验证系统(如Python)的行为混淆 dataclasses
它将生成一个构造函数,该构造函数在可选属性与必需属性方面与注释匹配。
小技巧
在上面的示例中, Integer
和 String
数据类型都是 TypeEngine
子类。在……里面 sqlalchemy2-stubs
,即 Column
对象是一个 generic 其订阅该类型,例如,在列类型之上是 Column[Integer]
, Column[String]
,以及 Column[String]
。这个 Integer
和 String
类又一般订阅它们所对应的Python类型,即 Integer(TypeEngine[int])
, String(TypeEngine[str])
。
没有显式类型的列
包含 ForeignKey
修饰符不需要在SQLAlChemy声明性映射中指定数据类型。对于这种类型的属性,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 style ,即 Column
定义在 Table
构造,该构造独立于映射属性本身。Mypy插件不考虑这一点 Table
,而是支持使用完整的批注显式声明属性,该批注 must 使用 Mapped
类将它们标识为映射属性::
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 block 视乎情况而定:
from typing import List, TYPE_CHECKING 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")
使用@DECLARATED_ATTR和声明性混合
这个 declared_attr
类允许在类级别函数中声明声明性映射属性,在使用 declarative mixins 。对于这些函数,函数的返回类型应使用 Mapped[]
构造或通过指示函数返回的确切对象类型。此外,没有以其他方式映射的“混合”类(即不是从 declarative_base()
类,它们也不是用如下方法映射的 registry.mapped()
)应该用 declarative_mixin()
修饰符,它向Mypy插件提供提示,表明特定类打算用作声明性混合::
from sqlalchemy.orm import declared_attr from sqlalchemy.orm import declarative_mixin @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 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"]
与数据类或其他类型敏感属性系统组合
上的Python数据类集成示例 具有数据类和属性的声明性映射 提出了一个问题;Python数据类需要一个它将用来构建类的显式类型,并且每个赋值语句中给出的值都很重要。也就是说,如下所示的类必须如实声明,才能被数据类接受:
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映射类。