为 SQLAlchemy Model 添加 type hint 和 type check
TL;DR
Use from typing import dataclass_transform
Motivation
其实动机很简单,众所周知对于 SQLAlchemy 1.4 想要 typing 可以安装 sqlalchemy2-stub
,对于 < 1.4 也可以有 sqlalchemy-stub
。然而对于最新的 SQLAlchemy >= 2.0,因为它自己有类型注释,但是又很少,所以还没有一个很好的解决方案。本文就是为了介绍一种我摸索出的解决方案,基本上可以完美解决 SQLAlchemy Model / ORM 的 typing 问题。
当然,如果你在手写 raw sql,那肯定是没办法自动弄好类型的,不要做梦了🚫。本文只针对使用了 SQLAlchemy ORM Model 的用户。
Implementation
基本思路是,Python 在 PEP 681 (>= Python 3.11) 当中为 typing
模块提供了一个 dataclass_transform
decorator,可以将第三方的 class 标注为和原生的 dataclass 提供类似的功能:
Most type checkers, linters and language servers have full support for dataclasses. This proposal aims to generalize this functionality and provide a way for third-party libraries to indicate that certain decorator functions, classes, and metaclasses provide behaviors similar to dataclasses.
These behaviors include:
- Synthesizing an
__init__
method based on declared data fields. <-- good for us- Optionally synthesizing
__eq__
,__ne__
,__lt__
,__le__
,__gt__
and__ge__
methods.- Supporting “frozen” classes, a way to enforce immutability during static type checking.
- Supporting “field specifiers”, which describe attributes of individual fields that a static type checker must be aware of, such as whether a default value is provided for the field. <-- mostly good for us
总体来讲这个 decorator 提供了所有我们想要的功能,唯一的问题是 field: type = mapped_column(...)
会导致 type checker 认为 field
是 Optional
的。但总归基本的类型检查是能用的,甚至能兼容外键和 relationship
,只是在初始化类的时候会有提供参数没写全的风险。如果感觉这样不够好,可以通过给每个 Model 编写一个 dummy __init__
的方式来把 optional 干掉:
1 | class A(Base): |
实践上,我们为了方便,可以直接在 SQLAlchemy 的 Base
class 上面使用这个 decorator,这样就不用每次定义一个 Model class 都要写一遍了。
1 | from sqlalchemy import Integer, String, Text, text, ForeignKey |
这里 class Base(DeclarativeBase):
写法得到的 Base
class 和 Base = declarative_base()
得到的 Base
在功能上是一样的,但是给了我们使用 dataclass_transform
的空间。