Odoo 的模型是运行时拼装出来的:一个模型由多个 addon 通过 _inherit 不断扩展,最后在 registry 里合成。这带来一个副作用——静态类型检查器看不见它。
问题在哪
Python 的类型检查器(Pyright、mypy)靠继承关系推断类型。但 Odoo 扩展模型并不靠 Python 继承:
class CrmLead(models.Model):
_inherit = "crm.lead"
@api.model
def attach_source(self, partner_id: int, source: str) -> None:
...
在检查器眼里,env["crm.lead"] 的类型永远是 BaseModel:它不知道这个模型上还有什么方法。
于是你想调用别处加进来的方法时,编辑器只能这样报错:
class ResPartner(models.Model):
_inherit = "res.partner"
def action_mark_referral(self) -> None:
leads = self.env["crm.lead"]
leads.attach_source(partner_id=self.id, source="referral")
# Pyright: Cannot access member "attach_source" for type "BaseModel"
⚠️ 关键在这条报错的含义:它不是说方法不存在,而是检查器不知道这个模型上有什么。
于是你既得不到补全,也得不到校验——写错参数名、传错类型,它都不会提醒你,只能整段 # type: ignore 掉。
如果你想再进一步,用 Protocol 去约束"服务层需要的模型接口",也同样做不到:检查器无法确认运行期拿到的模型实例真的满足那个接口。
设计思路
odoo-typegen 的做法是:读源码、生成类型存根(stub),让检查器"知道"模型最终长什么样。三个关键设计:
- 不 import,只解析:用 Astroid 读 AST,而不是把 addon 导入解释器运行。 这样生成过程不需要跑起来的 Odoo、不需要数据库,也不会执行任何 addon 代码——干净且安全。
- 按 manifest 依赖图排序:拓扑排序,被依赖的 addon 先解析,后加的覆盖在后。顺序对了,成员合并才对。
- 每个模型生成一个
.pyi,再生成一个带overload的Environment存根,把模型名映射到具体类型。
用法
# 指向你的 addon 目录(会按 manifest 依赖自动排序)
odoo-typegen ./addons
生成 typings/ 目录后,让 Pyright 把它包含进去(stubPath / extraPaths),编辑器立刻就能推断 env["model"] 的具体类型。
一个例子
① 某个 addon 给 crm.lead 加了方法(同上文的 attach_source)
② 跑一次 odoo-typegen,得到:
# typings/crm/lead.pyi
class CrmLead(Model):
def attach_source(self, partner_id: int, source: str) -> None: ...
# typings/odoo/orm/environments.pyi
class Environment:
@typing.overload
def __getitem__(self, model_name: typing.Literal["crm.lead"]) -> CrmLead: ...
@typing.overload
def __getitem__(self, model_name: str) -> typing.Any: ...
③ 调用处就被检查了:
leads = self.env["crm.lead"] # 推断为 CrmLead
leads.attach_source(partner_id=p.id, source="referral") # ✅
leads.attach_source(partner_id=p.id, source=42) # ❌ 类型错误
不需要 import CrmLead,也不需要手写任何注解。
目前的局限(作者自述)
- Odoo 核心模块的模型覆盖还没解析——架构上能做,先要解决性能。
- 模型内部的
self没有类型——要给self类型就得 import 生成的存根,作者暂时不想引入这个依赖。 - ORM 基础方法(
search/browse)的返回值还是Any——在路线图上。 - “这个方法是哪个 addon 加的” 还在构想——数据其实已经有了(
explain --model --method)。
一点评价
它解决的是编辑器与 CI 里的类型安全,不影响运行期,对现有代码零侵入、可逐步引入。
在 Odoo 这种"运行时拼装"的框架里,它走了一条不碰运行时的折中路线:不改造元类、不引入 monkey patch,代价是每个项目要生成一份存根,并随依赖变化重新生成。相比"让 _inherit 变成真正的 Python 继承"(如 typodoo 的思路),它更保守,也更容易随 Odoo 版本升级。
原文:Odoo-Typegen: Bringing Modern Static Typing to Odoo (作者 jbrocher) 仓库:jbrocher/odoo-typegen