From 04d8a28a6d117e850e4a010ab9a62f0b411cc2eb Mon Sep 17 00:00:00 2001 From: monchin Date: Sun, 14 Jun 2026 14:47:45 +0800 Subject: [PATCH 1/3] docs(openspec): propose fix for Self rendering in staticmethod --- .../fix-self-in-staticmethod/.openspec.yaml | 2 + .../fix-self-in-staticmethod/design.md | 60 +++++++++++++++++++ .../fix-self-in-staticmethod/proposal.md | 31 ++++++++++ .../specs/constructor-emission/spec.md | 15 +++++ .../specs/self-receiver-detection/spec.md | 33 ++++++++++ .../specs/self-type-rendering/spec.md | 51 ++++++++++++++++ .../changes/fix-self-in-staticmethod/tasks.md | 29 +++++++++ 7 files changed, 221 insertions(+) create mode 100644 openspec/changes/fix-self-in-staticmethod/.openspec.yaml create mode 100644 openspec/changes/fix-self-in-staticmethod/design.md create mode 100644 openspec/changes/fix-self-in-staticmethod/proposal.md create mode 100644 openspec/changes/fix-self-in-staticmethod/specs/constructor-emission/spec.md create mode 100644 openspec/changes/fix-self-in-staticmethod/specs/self-receiver-detection/spec.md create mode 100644 openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md create mode 100644 openspec/changes/fix-self-in-staticmethod/tasks.md diff --git a/openspec/changes/fix-self-in-staticmethod/.openspec.yaml b/openspec/changes/fix-self-in-staticmethod/.openspec.yaml new file mode 100644 index 0000000..c86b1d7 --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-14 diff --git a/openspec/changes/fix-self-in-staticmethod/design.md b/openspec/changes/fix-self-in-staticmethod/design.md new file mode 100644 index 0000000..d2b87d6 --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/design.md @@ -0,0 +1,60 @@ +## Context + +当前 generator 渲染 Rust `Self` 的链路横跨 `src/generator.rs` 与 `src/type_map.rs`: + +- `gen_method`(`src/generator.rs:819`)进入任一方法时设置 `current_self_type = Some(class.name)`,**不区分方法种类**;用 RAII guard `RestoreCurrentSelfTypeGuard`(`src/generator.rs:164`)在返回(含 `?` early-return)时恢复为 `None`。 +- `resolve_type`(`src/generator.rs:236`)是 generator 调用 `map_type` 的唯一入口,传入 `&self.policy` + 类名上下文。 +- `map_type` 的 `"Self"` 分支(`src/type_map.rs:330`)只看 `policy.native_self`:`true` → `t.Self`,`false` → 类名。 +- `native_self` 由 `python_version >= 3.11` 决定(`src/config.rs:387`)。 + +由于 `map_type` 不感知方法种类,`#[staticmethod]` 的 `Self` 在 py≥3.11 也被渲染成 `t.Self`。但 [PEP 673](https://peps.python.org/pep-0673/#valid-locations-for-self) 拒绝 staticmethod 中的 `Self`(没有 `self`/`cls` 可绑定)。 + +已验证:用最小 PyO3 crate(含 `#[staticmethod]` 的 4 种 `Self` 形态——裸返回、`PyResult` 返回、裸参数、`Py` 参数——加一个 instance method `-> Self` 对照)跑 rylai,py3.11 stub 的 4 个 staticmethod 全部错误 emit `t.Self`,`ty --python-version 3.11` 报 4 个 `Self cannot be used in a static method`;py3.9 用类名正确。手写"修复后期望输出"(staticmethod 用类名、instance 用 `t.Self`)经 `ty --python-version 3.11` 全 pass——证明 py3.11 下类体内 forward reference(类名)在 `.pyi` 合法,无需额外动 `from __future__ import annotations` header。 + +## Goals / Non-Goals + +**Goals:** +- `#[staticmethod]` 中的 `Self`(返回 / 参数,裸及 `Py`、`PyResult`、`Vec`、`Option` 等嵌套)始终渲染为 Python 类名,符合 PEP 673。 +- instance method / classmethod / `__new__` 的 `Self` 渲染**不变**(PEP 673 接受这些位置)。 +- 不改 `map_type` 签名。 +- 不回归 py<3.11 行为(本就用类名)。 + +**Non-Goals:** +- metaclass 中的 `Self`(PEP 673 同样拒绝,但 rylai 不建模 Python metaclass)。 +- 修改 collector/parser(收集层原样保留 raw `Self`,渲染决策在 generator)。 +- 修改 `map_type` 递归分支。 + +## Decisions + +### 决策 1:generator 层引入 `in_static_method` 上下文 + 临时 policy + +**方案:** `GenCtx` 加 `in_static_method: bool`;`gen_method` 对 `MethodKind::Static` 置 `true`(用 RAII guard 恢复);`resolve_type` 在该上下文且 `native_self` 时,构造临时 policy(`native_self=false`)调 `map_type`。 + +**理由:** `map_type` 递归处理 `Py` / `PyResult` / `Vec` / `Option` 等嵌套。generator 层透传 `native_self=false` 后,所有嵌套 `Self` 自动走类名路径,无需逐个改 `map_type` 递归分支。`map_type` 签名不变,其大量单元测试(type_map.rs)不受影响。 + +**考虑过的替代方案:** _给 `map_type` 加 `in_static_method` 参数透传所有递归点_——改动面大(递归调用点多 + 测试多),且把"方法上下文"这个 generator 层关注点下沉到类型映射层,职责错位。已否决。 + +### 决策 2:扩展现有 RAII guard 同时管理两个字段 + +**方案:** 现有 `RestoreCurrentSelfTypeGuard`(`src/generator.rs:164`)管理 `current_self_type`;扩展它同时管理 `in_static_method`(持有两个 raw 指针,`Drop` 时恢复两者)。重命名为 `RestoreMethodContextGuard`。 + +**理由:** `current_self_type` 与 `in_static_method` 生命周期完全一致(都在 `gen_method` 设置、`gen_method` 返回恢复),合并到一个 guard 比两个独立 guard 更内聚、`unsafe` 块更少(1 vs 2)。 + +**考虑过的替代方案:** _新增独立 `RestoreInStaticMethodGuard`_——多一个 `unsafe` 块,且两个字段语义同属"方法上下文",拆开无收益。已否决。 + +### 决策 3:`resolve_type` 用临时 policy,不修改 `self.policy` + +**方案:** `resolve_type` 内 `let effective_policy = if self.in_static_method && self.policy.native_self { clone + native_self=false } else { self.policy.clone() }`,传 `&effective_policy` 给 `map_type`。不修改 `self.policy` 本身。 + +**理由:** `self.policy` 是 `GenCtx` 全局字段,代表整个文件的版本策略(还驱动 header 的 `future_annotations`);原地修改需额外 guard 且影响面大。临时 policy 每次 resolve clone 一个 4-bool `RenderPolicy`,开销可忽略,作用域局限于单次 `map_type` 调用,无副作用。 + +## Risks / Trade-offs + +- **[guard 泄漏]** `in_static_method` 必须在 `gen_method` 返回(含 early-return)恢复 `false`。RAII guard 保证;新增"同类内 instance/classmethod/static 共存"测试覆盖防回归。 +- **[`needs_self_import` dead state]** staticmethod 不再 emit `t.Self`,但 `needs_self_import` 是 dead state(`import typing as t` 无条件 emit,`src/generator.rs:80-82`),无副作用。 +- **[现有测试]** `render_policy_py312_emits_native_self_and_no_future_annotations`(`src/generator.rs:1933`)用 staticmethod 断言 `t.Self`,修复后失败,需同步改为类名(本质上把 bug 的回归测试就地转为正确行为的回归测试)。 +- **[spec 一致性]** `constructor-emission`、`self-receiver-detection` 现有 spec 中与本次修复冲突的表述,由本 change 的 spec delta 同步修正。 + +## Migration Plan + +纯 generator 层,无配置/数据迁移。回滚 = revert 该 commit。生成的 staticmethod stub 从(违反 PEP 673 的)`t.Self` 变为类名,用户侧无需动作;py<3.11 无变化。 diff --git a/openspec/changes/fix-self-in-staticmethod/proposal.md b/openspec/changes/fix-self-in-staticmethod/proposal.md new file mode 100644 index 0000000..e6ab332 --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/proposal.md @@ -0,0 +1,31 @@ +## Why + +rylai 在 `python_version ≥ 3.11`(`native_self` 渲染策略)时,会把 `#[staticmethod]` 中的 `Self`——无论返回类型还是参数类型,无论裸 `Self` 还是 `Py` / `PyResult` / `Vec` 等嵌套——渲染成 `t.Self`。但 [PEP 673](https://peps.python.org/pep-0673/#valid-locations-for-self) 明确**拒绝** staticmethod 中的 `Self`(没有 `self`/`cls` 可绑定),类型检查器会报错(实测 `ty`:`Self cannot be used in a static method`)。已用最小 PyO3 crate 复现并经 `ty --python-version 3.11` 验证(4 个 staticmethod 全报错);py < 3.11 用类名反而正确——这是启用 native Self 后引入的回归。 + +## What Changes + +- generator 层引入 staticmethod 上下文标志:`#[staticmethod]` 中的 `Self` 始终解析为 Python 类名,绕过 `native_self`(对齐 py<3.11 行为)。 +- instance method / classmethod / `__new__` 的 `Self` 渲染不变(PEP 673 接受这些位置)。 +- 修正现有 generator 测试 `render_policy_py312_emits_native_self_and_no_future_annotations`(它用 staticmethod 断言 `t.Self`,固化了 bug)。 + +## Capabilities + +### New Capabilities +- `self-type-rendering`:规格化 Rust `Self` → Python 的渲染策略——instance/classmethod/`__new__` 方法在 py≥3.11 渲染为 `t.Self`、py<3.11 渲染为类名;`#[staticmethod]` 无论版本均渲染为类名(PEP 673)。 + +### Modified Capabilities +- `constructor-emission`:其"`__new__` 返回类型遵循版本感知的 `Self` 渲染" requirement 中"该渲染 MUST 与返回 `Self` 的 staticmethod 行为完全一致"不再成立(`__new__` 用 `t.Self`、staticmethod 用类名),需修正为指向 `self-type-rendering`。 +- `self-receiver-detection`:其"依赖 Self 的 receiver 类型仅在 receiver 位置被排除" requirement 的补充说明里"py ≥ 3.11 映射为 `t.Self`"对 staticmethod 不准,需限定为非 staticmethod 的普通参数。 + +## Impact + +- 仅 **generator 层**(`src/generator.rs`):`GenCtx` 加 `in_static_method` 标志 + 扩展现有 RAII guard;`resolve_type` 在 staticmethod 上下文用临时 policy(`native_self=false`)调 `map_type`。不改 `map_type` 签名。 +- 不影响 CLI / parser / config / output layout。 +- 向后兼容:staticmethod 的 stub 从(违反 PEP 673 的)`t.Self` 变为类名;py<3.11 无变化。 +- PyO3 兼容:不改变解析假设,仅改渲染。 + +## Non-goals + +- metaclass 中的 `Self`(PEP 673 同样拒绝,但 rylai 不建模 Python metaclass)。 +- 修改 `map_type` 签名或其递归分支(generator 层透传临时 policy 即可让 `Py`、`PyResult`、`Vec` 等嵌套形态自动正确)。 +- 修改 collector/parser 层(收集层原样保留 `Self`,渲染决策在 generator)。 diff --git a/openspec/changes/fix-self-in-staticmethod/specs/constructor-emission/spec.md b/openspec/changes/fix-self-in-staticmethod/specs/constructor-emission/spec.md new file mode 100644 index 0000000..a0f9cce --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/specs/constructor-emission/spec.md @@ -0,0 +1,15 @@ +## MODIFIED Requirements + +### Requirement: `__new__` 返回类型遵循版本感知的 `Self` 渲染 + +Initializer 模式下 `#[new]` 生成的 `__new__` 返回类型 MUST 经由 `Self` 渲染机制产出(规则见 `self-type-rendering` capability):当 `python_version` ≥ 3.11(PEP 673 `native_self`)时 MUST 为 `t.Self`;当 `python_version` < 3.11 时 MUST 为当前类的类名。 + +`__new__` 属 PEP 673 接受 `Self` 的位置(类构造器,带 `cls`),故其渲染与 instance method 一致;而 `#[staticmethod]` 因 PEP 673 例外改渲染类名(见 `self-type-rendering`)——**二者不再等价**,旧表述"MUST 与返回 `Self` 的 staticmethod 行为完全一致"据此作废。 + +#### Scenario: `python_version` 3.12 下 `__new__` 返回 `t.Self` +- **WHEN** Initializer 模式下 `python_version` 配置为 `3.12` +- **THEN** 生成的 `__new__` 签名 MUST 形如 `def __new__(cls, ...) -> t.Self:`,且 stub MUST 包含 `import typing as t`、MUST NOT 包含 `from __future__ import annotations` + +#### Scenario: `python_version` 3.9 下 `__new__` 返回类名 +- **WHEN** Initializer 模式下 `python_version` 配置为 `3.9`,类名为 `MyDict` +- **THEN** 生成的 `__new__` 签名 MUST 形如 `def __new__(cls, ...) -> MyDict:`,且 stub MUST 包含 `from __future__ import annotations`、MUST NOT 出现 `t.Self` diff --git a/openspec/changes/fix-self-in-staticmethod/specs/self-receiver-detection/spec.md b/openspec/changes/fix-self-in-staticmethod/specs/self-receiver-detection/spec.md new file mode 100644 index 0000000..87de2ff --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/specs/self-receiver-detection/spec.md @@ -0,0 +1,33 @@ +## MODIFIED Requirements + +### Requirement: 依赖 Self 的 receiver 类型仅在 receiver 位置被排除 + +当 `#[pymethods]` 方法的参数类型为依赖 `Self` 的 receiver 类型(`Py`、`Bound<'_, Self>`、`&Bound<'_, Self>`、`&Borrowed<'_, Self>`)时,该参数 MUST 仅在 **receiver 位置**被视为隐式 `self` 并从生成的 stub 中排除。 + +"receiver 位置"定义为:方法**带有实例 receiver**(`MethodKind::Instance` / `Getter` / `Setter`,即非 `#[staticmethod]` / `#[classmethod]` / `#[new]`),且该参数为方法签名中的首个参数,其前不存在 `&self` / `&mut self` / `self`(即无 `FnArg::Receiver`)。 + +一旦方法的 receiver 被确认(出现了 `FnArg::Receiver`,或首个 typed 参数已被识别为 receiver),后续所有依赖 `Self` 的 receiver 类型参数**MUST 作为普通参数保留**在 stub 中,并映射为对应的 Python 类类型。 + +`Python<'_>`、`&Bound<'_, PyModule>` 等与位置无关的纯注入类型不受此规则约束,继续在任何位置无条件排除。 + +补充:被保留为普通参数的 `Py` / `Bound<'_, Self>` 等类型,其 Python 类型遵循 `self-type-rendering` capability 的规则——对 instance method / classmethod 的普通参数位置,py < 3.11 映射为类名(如 `Foo`)、py ≥ 3.11 映射为 `t.Self`;对 `#[staticmethod]` 中的此类参数,无论版本 MUST 映射为类名(PEP 673)。下方 scenario 以默认策略(类名)示例,均满足"作为普通参数保留"的契约。 + +#### Scenario: Py 在普通参数位置必须保留 +- **WHEN** `#[pymethods]` 方法声明为 `fn by_normal(&self, other: Py) -> i64` +- **THEN** 生成的 stub MUST 为 `def by_normal(self, other: Foo) -> int`,`other` 作为普通参数保留,MUST NOT 删除 + +#### Scenario: receiver 位置之后再次出现的 Py 必须保留 +- **WHEN** `#[pymethods]` 方法声明为 `fn m(slf: Py, other: Py) -> i64`(首个 `Py` 作 receiver,第二个作普通参数) +- **THEN** 生成的 stub MUST 为 `def m(self, other: Foo) -> int`,仅首个 `slf` 被排除,`other` 作为普通参数保留 + +#### Scenario: staticmethod 中的 Py 必须作为普通参数保留 +- **WHEN** `#[pymethods]` 中带 `#[staticmethod]` 的方法声明为 `fn make(other: Py) -> i64` +- **THEN** 生成的 stub MUST 为 `def make(other: Foo) -> int`(无 `self`),`other` 作为普通参数保留,因为静态方法没有 receiver + +#### Scenario: Py 在 receiver 位置仍被正确排除(不回归) +- **WHEN** `#[pymethods]` 实例方法声明为 `fn by_receiver(slf: Py, x: i64) -> i64` +- **THEN** 生成的 stub MUST 为 `def by_receiver(self, x: int) -> int`,`slf` 作为 receiver 被排除(与上一 change 行为一致,不产生回归) + +#### Scenario: getter / setter 的 typed Self receiver 必须被排除 +- **WHEN** `#[pymethods]` 声明 `#[setter] fn set_x(slf: PyRefMut<'_, Self>, v: i32)` 与 `#[getter] fn get_x(slf: PyRef<'_, Self>) -> i32` +- **THEN** getter 生成的 stub MUST 为 `@property def x(self) -> int`;setter MUST 为 `@x.setter def x(self, value: int) -> None`——即 typed receiver 被排除,setter 的 `value` 取到真正的参数 `v` 而非 receiver diff --git a/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md b/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md new file mode 100644 index 0000000..db150be --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md @@ -0,0 +1,51 @@ +## ADDED Requirements + +### Requirement: 非 staticmethod 方法的 Self 渲染遵循版本感知策略 + +当 `#[pymethods]` 方法**不是** `#[staticmethod]`(即 instance method / classmethod / `#[new]`),且其返回类型或参数类型涉及 Rust `Self`(含裸 `Self` 及 `Py` / `PyRef<'_, Self>` / `Bound<'_, Self>` / `PyResult` / `Vec` / `Option` 等嵌套)时,该 `Self` MUST 按 `python_version` 渲染:`python_version` ≥ 3.11(PEP 673 `native_self`)MUST 渲染为 `t.Self`;`python_version` < 3.11 MUST 渲染为当前类的 Python 类名(作为 forward reference,此时 stub MUST 包含 `from __future__ import annotations`)。 + +#### Scenario: instance method 返回 Self 在 py3.11 渲染为 t.Self +- **WHEN** `#[pymethods]` instance method 声明为 `fn echo(&self) -> Self`,`python_version = 3.11` +- **THEN** 生成的 stub MUST 为 `def echo(self) -> t.Self:`,且 stub MUST 包含 `import typing as t` + +#### Scenario: instance method 返回 Self 在 py3.9 渲染为类名 +- **WHEN** 同上方法,`python_version = 3.9`,类名为 `Widget` +- **THEN** 生成的 stub MUST 为 `def echo(self) -> Widget:`,且 stub MUST 包含 `from __future__ import annotations`、MUST NOT 出现 `t.Self` + +#### Scenario: classmethod 返回 Self 在 py3.11 渲染为 t.Self +- **WHEN** `#[pymethods]` classmethod 声明为 `#[classmethod] fn factory(cls) -> Self`,`python_version = 3.11` +- **THEN** 生成的 stub MUST 为 `@classmethod` 修饰的 `def factory(cls) -> t.Self:` + +#### Scenario: Initializer 模式下 `__new__` 返回 Self 在 py3.11 渲染为 t.Self +- **WHEN** Initializer 模式下 `#[new]` 方法返回 `Self`(或 `PyResult`),`python_version = 3.11` +- **THEN** 生成的 `__new__` 返回类型 MUST 为 `t.Self` + +### Requirement: staticmethod 中的 Self 必须始终渲染为类名 + +当 `#[staticmethod]` 方法的返回类型或参数类型涉及 Rust `Self`(含裸 `Self` 及 `Py` / `PyResult` / `Vec` / `Option` 等嵌套)时,该 `Self` MUST **始终**渲染为当前类的 Python 类名,**无论 `python_version`**。因为 [PEP 673](https://peps.python.org/pep-0673/#valid-locations-for-self) 拒绝 staticmethod 中的 `Self`(没有 `self`/`cls` 可绑定),`t.Self` 会被类型检查器拒绝(实测 `ty` 报 `Self cannot be used in a static method`)。 + +py ≥ 3.11 时,类体内 forward reference(类名)在 `.pyi` 中合法,rylai MUST NOT 仅为 staticmethod 而额外引入 `from __future__ import annotations`。 + +#### Scenario: staticmethod 裸 Self 返回在 py3.11 渲染为类名 +- **WHEN** `#[staticmethod] fn make_direct() -> Self`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def make_direct() -> Widget:`,且该方法 MUST NOT 出现 `t.Self` + +#### Scenario: staticmethod `PyResult` 返回在 py3.11 渲染为类名 +- **WHEN** `#[staticmethod] fn from_int(x: i64) -> PyResult`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def from_int(x: int) -> Widget:` + +#### Scenario: staticmethod 裸 Self 参数在 py3.11 渲染为类名 +- **WHEN** `#[staticmethod] fn take_direct(other: Self) -> i64`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def take_direct(other: Widget) -> int:` + +#### Scenario: staticmethod `Py` 参数在 py3.11 渲染为类名(嵌套递归) +- **WHEN** `#[staticmethod] fn take_py(other: Py) -> i64`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def take_py(other: Widget) -> int:` + +#### Scenario: staticmethod 的 Self 在 py3.9 行为不变 +- **WHEN** `#[staticmethod] fn make_direct() -> Self`,`python_version = 3.9`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def make_direct() -> Widget:`(与 py3.11 一致,本就用类名) + +#### Scenario: 同类内 staticmethod 与其他方法共存时渲染互不干扰 +- **WHEN** 同一 `#[pyclass]` 内含 instance method `fn a(&self) -> Self`、classmethod `#[classmethod] fn b(cls) -> Self`、staticmethod `#[staticmethod] fn c() -> Self`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 同时满足:`def a(self) -> t.Self:`、`def b(cls) -> t.Self:`、`def c() -> Widget:`(staticmethod 的类名渲染 MUST NOT 泄漏到相邻的 instance/classmethod) diff --git a/openspec/changes/fix-self-in-staticmethod/tasks.md b/openspec/changes/fix-self-in-staticmethod/tasks.md new file mode 100644 index 0000000..9267801 --- /dev/null +++ b/openspec/changes/fix-self-in-staticmethod/tasks.md @@ -0,0 +1,29 @@ +## 1. generator:引入 staticmethod 上下文标志 + +- [ ] 1.1 在 `src/generator.rs` 的 `GenCtx` 结构(`current_self_type` 字段后,约 211 行)新增 `in_static_method: bool` 字段,注释说明:PEP 673 拒绝 staticmethod 中的 `Self`,该上下文下 `Self` 解析为类名;在 `generate_with_known_classes` 的 ctx 构造(约 49 行 `current_self_type: None,` 后)初始化 `in_static_method: false,`。 +- [ ] 1.2 扩展 RAII guard:把 `RestoreCurrentSelfTypeGuard`(约 164-174 行)重命名为 `RestoreMethodContextGuard`,持有 `self_type: *mut Option` 与 `in_static: *mut bool` 两个字段,`Drop` 时 `*self_type = None; *in_static = false;`(更新 SAFETY 注释)。 +- [ ] 1.3 在 `gen_method`(约 819-821 行)设置 `current_self_type` 后,加 `self.in_static_method = matches!(m.kind, MethodKind::Static);`,并把 guard 构造改为同时传两个字段指针。 +- [ ] 1.4 在 `resolve_type`(约 242-247 行)调 `map_type` 前,计算 `effective_policy`:`if self.in_static_method && self.policy.native_self` 则 clone 并置 `native_self = false`,否则 clone 原 policy;传 `&effective_policy` 给 `map_type`(注释说明 PEP 673 + 嵌套递归自动走类名)。 + +## 2. 修正固化 bug 的现有测试 + +- [ ] 2.1 修改 `src/generator.rs:1933` 的 `render_policy_py312_emits_native_self_and_no_future_annotations`:把 `stub.contains("-> t.Self:")` 断言改为 `stub.contains("-> PdfDocument:")`;测试名改为 `render_policy_py312_staticmethod_self_uses_class_name`,注释说明 PEP 673 拒绝 staticmethod 中的 Self。保留 `!stub.contains("from __future__ import annotations")` 与 `stub.contains(TYPING_IMPORT_LINE)` 断言。 + +## 3. 新增 `self-type-rendering` 的 generator 测试 + +置于 `src/generator.rs` 现有 RenderPolicy 测试组之后(约 1950 行);复用 helper `make_method` / `make_class_with_methods` / `make_param` / `config_with_python_version` / `stub_for_config`,断言失败消息带 `{stub}`。这些测试走完整 `generate` 管线(stub 文本断言),对应 `specs/self-type-rendering/spec.md` 各 scenario。 + +- [ ] 3.1 `static_method_bare_self_return_py311_uses_class_name`:`#[staticmethod] fn make_direct() -> Self` + py3.11,断言 `def make_direct() -> Widget:` 且不含 `t.Self`。 +- [ ] 3.2 `static_method_pyresult_self_return_py311_uses_class_name`:`#[staticmethod] fn from_int(x: i64) -> PyResult` + py3.11,断言 `def from_int(x: int) -> Widget:`。 +- [ ] 3.3 `static_method_bare_self_param_py311_uses_class_name`:参数 `Self` + py3.11,断言 `def take_direct(other: Widget) -> int:`。 +- [ ] 3.4 `static_method_py_self_param_py311_uses_class_name`:参数 `Py` + py3.11,断言 `def take_py(other: Widget) -> int:`(验证 `map_type` 嵌套递归)。 +- [ ] 3.5 `instance_method_self_return_py311_keeps_t_self`(对照):instance `fn echo(&self) -> Self` + py3.11,断言 `def echo(self) -> t.Self:`。 +- [ ] 3.6 `classmethod_self_return_py311_keeps_t_self`(对照):classmethod `#[classmethod] fn factory(cls) -> Self` + py3.11,断言 `def factory(cls) -> t.Self:`。 +- [ ] 3.7 `mixed_methods_in_one_class_py311_static_uses_class_name_others_t_self`(防 guard 泄漏):同类内 instance/classmethod/static 各一,断言三者分别为 `t.Self` / `t.Self` / `Widget`。 +- [ ] 3.8 `static_method_self_return_py39_unchanged_class_name`(回归):staticmethod `-> Self` + py3.9,断言 `def make_direct() -> Widget:` 且含 `from __future__ import annotations`。 + +## 4. 验证 + +- [ ] 4.1 `cargo test` 与 `cargo clippy --all-targets` 全绿、无回归。 +- [ ] 4.2 端到端:在临时目录建最小 PyO3 crate(`#[pymodule] mod` 内一个 `#[pyclass]`,含 `#[staticmethod]` 返回/参数 `Self` 各形态 + 一个 instance method `-> Self` 对照),分别用 `python_version = "3.11"` 与 `"3.9"` 跑 `rylai` 生成 stub;确认 py3.11 stub 中 staticmethod → 类名、instance → `t.Self`,py3.9 全部类名。 +- [ ] 4.3 类型检查器:对 py3.11 stub 跑 `ty check --python-version 3.11`,确认 `All checks passed!`(对照修复前报 `Self cannot be used in a static method`);py3.9 stub 同样 pass。 From bbd9627e39c57ecf2c3805f18d1375842910e660 Mon Sep 17 00:00:00 2001 From: monchin Date: Sun, 14 Jun 2026 16:04:59 +0800 Subject: [PATCH 2/3] fix(generator): render Self as the class name in staticmethods (PEP 673) --- CHANGELOG.md | 1 + .../specs/self-type-rendering/spec.md | 4 +- .../changes/fix-self-in-staticmethod/tasks.md | 32 +- src/generator.rs | 296 +++++++++++++++++- 4 files changed, 299 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f91e52a..a89f1b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Exclude `Self`-dependent PyO3 receiver types (`Py`, `Bound<'_, Self>`, `&Bound<'_, Self>`, `&Borrowed<'_, Self>`, `PyRef<'_, Self>`, `PyRefMut<'_, Self>`) from generated stub parameters **only at the receiver position** (first parameter of an instance method, getter, or setter). The same type used as a later parameter (e.g. `fn m(&self, other: Py)`) or in a `#[staticmethod]` / `#[classmethod]` is now kept as a regular parameter and mapped to the class type. Fixes incorrect stubs where such parameters were either leaked as extra `self`-like bindings (`slf`, `slf_handle`, …) or silently dropped from the signature — both triggered mypy/pyright errors at correct call sites. (#5, #6) +- Render Rust `Self` as the class name (not the `t.Self` keyword) inside `#[staticmethod]` signatures under `native_self` (Python ≥ 3.11). [PEP 673](https://peps.python.org/pep-0673/) rejects `Self` in static methods, so the previous output (e.g. `def make() -> t.Self:`) failed type checking with `Self cannot be used in a static method`. Applies to return types, parameters, and nested wrappers (`Py`, `PyResult`, `Vec`, `PyRef<'_, Self>`, `Bound<'_, Self>`, …); instance methods and classmethods still render `t.Self`. Behavior under `python_version` < 3.11 is unchanged (the class name was already emitted there). ## [0.4.1] - 2026-06-03 diff --git a/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md b/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md index db150be..3b69428 100644 --- a/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md +++ b/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md @@ -14,7 +14,7 @@ #### Scenario: classmethod 返回 Self 在 py3.11 渲染为 t.Self - **WHEN** `#[pymethods]` classmethod 声明为 `#[classmethod] fn factory(cls) -> Self`,`python_version = 3.11` -- **THEN** 生成的 stub MUST 为 `@classmethod` 修饰的 `def factory(cls) -> t.Self:` +- **THEN** 该 classmethod 的 `Self` 返回类型 MUST 渲染为 `t.Self:`(PEP 673 接受 classmethod 中的 `Self`) #### Scenario: Initializer 模式下 `__new__` 返回 Self 在 py3.11 渲染为 t.Self - **WHEN** Initializer 模式下 `#[new]` 方法返回 `Self`(或 `PyResult`),`python_version = 3.11` @@ -48,4 +48,4 @@ py ≥ 3.11 时,类体内 forward reference(类名)在 `.pyi` 中合法,rylai MU #### Scenario: 同类内 staticmethod 与其他方法共存时渲染互不干扰 - **WHEN** 同一 `#[pyclass]` 内含 instance method `fn a(&self) -> Self`、classmethod `#[classmethod] fn b(cls) -> Self`、staticmethod `#[staticmethod] fn c() -> Self`,`python_version = 3.11`,类名 `Widget` -- **THEN** 生成的 stub MUST 同时满足:`def a(self) -> t.Self:`、`def b(cls) -> t.Self:`、`def c() -> Widget:`(staticmethod 的类名渲染 MUST NOT 泄漏到相邻的 instance/classmethod) +- **THEN** instance method `a` 与 classmethod `b` 的 `Self` 返回类型 MUST 渲染为 `t.Self:`,staticmethod `c` 的 MUST 渲染为 `Widget:`(staticmethod 的类名渲染 MUST NOT 泄漏到相邻的 instance/classmethod) diff --git a/openspec/changes/fix-self-in-staticmethod/tasks.md b/openspec/changes/fix-self-in-staticmethod/tasks.md index 9267801..5130ce5 100644 --- a/openspec/changes/fix-self-in-staticmethod/tasks.md +++ b/openspec/changes/fix-self-in-staticmethod/tasks.md @@ -1,29 +1,29 @@ ## 1. generator:引入 staticmethod 上下文标志 -- [ ] 1.1 在 `src/generator.rs` 的 `GenCtx` 结构(`current_self_type` 字段后,约 211 行)新增 `in_static_method: bool` 字段,注释说明:PEP 673 拒绝 staticmethod 中的 `Self`,该上下文下 `Self` 解析为类名;在 `generate_with_known_classes` 的 ctx 构造(约 49 行 `current_self_type: None,` 后)初始化 `in_static_method: false,`。 -- [ ] 1.2 扩展 RAII guard:把 `RestoreCurrentSelfTypeGuard`(约 164-174 行)重命名为 `RestoreMethodContextGuard`,持有 `self_type: *mut Option` 与 `in_static: *mut bool` 两个字段,`Drop` 时 `*self_type = None; *in_static = false;`(更新 SAFETY 注释)。 -- [ ] 1.3 在 `gen_method`(约 819-821 行)设置 `current_self_type` 后,加 `self.in_static_method = matches!(m.kind, MethodKind::Static);`,并把 guard 构造改为同时传两个字段指针。 -- [ ] 1.4 在 `resolve_type`(约 242-247 行)调 `map_type` 前,计算 `effective_policy`:`if self.in_static_method && self.policy.native_self` 则 clone 并置 `native_self = false`,否则 clone 原 policy;传 `&effective_policy` 给 `map_type`(注释说明 PEP 673 + 嵌套递归自动走类名)。 +- [x] 1.1 在 `src/generator.rs` 的 `GenCtx` 结构(`current_self_type` 字段后,约 211 行)新增 `in_static_method: bool` 字段,注释说明:PEP 673 拒绝 staticmethod 中的 `Self`,该上下文下 `Self` 解析为类名;在 `generate_with_known_classes` 的 ctx 构造(约 49 行 `current_self_type: None,` 后)初始化 `in_static_method: false,`。 +- [x] 1.2 扩展 RAII guard:把 `RestoreCurrentSelfTypeGuard`(约 164-174 行)重命名为 `RestoreMethodContextGuard`,持有 `self_type: *mut Option` 与 `in_static: *mut bool` 两个字段,`Drop` 时 `*self_type = None; *in_static = false;`(更新 SAFETY 注释)。 +- [x] 1.3 在 `gen_method`(约 819-821 行)设置 `current_self_type` 后,加 `self.in_static_method = matches!(m.kind, MethodKind::Static);`,并把 guard 构造改为同时传两个字段指针。 +- [x] 1.4 在 `resolve_type`(约 242-247 行)调 `map_type` 前,计算 `effective_policy`:`if self.in_static_method && self.policy.native_self` 则 clone 并置 `native_self = false`,否则 clone 原 policy;传 `&effective_policy` 给 `map_type`(注释说明 PEP 673 + 嵌套递归自动走类名)。 ## 2. 修正固化 bug 的现有测试 -- [ ] 2.1 修改 `src/generator.rs:1933` 的 `render_policy_py312_emits_native_self_and_no_future_annotations`:把 `stub.contains("-> t.Self:")` 断言改为 `stub.contains("-> PdfDocument:")`;测试名改为 `render_policy_py312_staticmethod_self_uses_class_name`,注释说明 PEP 673 拒绝 staticmethod 中的 Self。保留 `!stub.contains("from __future__ import annotations")` 与 `stub.contains(TYPING_IMPORT_LINE)` 断言。 +- [x] 2.1 修改 `src/generator.rs:1933` 的 `render_policy_py312_emits_native_self_and_no_future_annotations`:把 `stub.contains("-> t.Self:")` 断言改为 `stub.contains("-> PdfDocument:")`;测试名改为 `render_policy_py312_staticmethod_self_uses_class_name`,注释说明 PEP 673 拒绝 staticmethod 中的 Self。保留 `!stub.contains("from __future__ import annotations")` 与 `stub.contains(TYPING_IMPORT_LINE)` 断言。 ## 3. 新增 `self-type-rendering` 的 generator 测试 置于 `src/generator.rs` 现有 RenderPolicy 测试组之后(约 1950 行);复用 helper `make_method` / `make_class_with_methods` / `make_param` / `config_with_python_version` / `stub_for_config`,断言失败消息带 `{stub}`。这些测试走完整 `generate` 管线(stub 文本断言),对应 `specs/self-type-rendering/spec.md` 各 scenario。 -- [ ] 3.1 `static_method_bare_self_return_py311_uses_class_name`:`#[staticmethod] fn make_direct() -> Self` + py3.11,断言 `def make_direct() -> Widget:` 且不含 `t.Self`。 -- [ ] 3.2 `static_method_pyresult_self_return_py311_uses_class_name`:`#[staticmethod] fn from_int(x: i64) -> PyResult` + py3.11,断言 `def from_int(x: int) -> Widget:`。 -- [ ] 3.3 `static_method_bare_self_param_py311_uses_class_name`:参数 `Self` + py3.11,断言 `def take_direct(other: Widget) -> int:`。 -- [ ] 3.4 `static_method_py_self_param_py311_uses_class_name`:参数 `Py` + py3.11,断言 `def take_py(other: Widget) -> int:`(验证 `map_type` 嵌套递归)。 -- [ ] 3.5 `instance_method_self_return_py311_keeps_t_self`(对照):instance `fn echo(&self) -> Self` + py3.11,断言 `def echo(self) -> t.Self:`。 -- [ ] 3.6 `classmethod_self_return_py311_keeps_t_self`(对照):classmethod `#[classmethod] fn factory(cls) -> Self` + py3.11,断言 `def factory(cls) -> t.Self:`。 -- [ ] 3.7 `mixed_methods_in_one_class_py311_static_uses_class_name_others_t_self`(防 guard 泄漏):同类内 instance/classmethod/static 各一,断言三者分别为 `t.Self` / `t.Self` / `Widget`。 -- [ ] 3.8 `static_method_self_return_py39_unchanged_class_name`(回归):staticmethod `-> Self` + py3.9,断言 `def make_direct() -> Widget:` 且含 `from __future__ import annotations`。 +- [x] 3.1 `static_method_bare_self_return_py311_uses_class_name`:`#[staticmethod] fn make_direct() -> Self` + py3.11,断言 `def make_direct() -> Widget:` 且不含 `t.Self`。 +- [x] 3.2 `static_method_pyresult_self_return_py311_uses_class_name`:`#[staticmethod] fn from_int(x: i64) -> PyResult` + py3.11,断言 `def from_int(x: int) -> Widget:`。 +- [x] 3.3 `static_method_bare_self_param_py311_uses_class_name`:参数 `Self` + py3.11,断言 `def take_direct(other: Widget) -> int:`。 +- [x] 3.4 `static_method_py_self_param_py311_uses_class_name`:参数 `Py` + py3.11,断言 `def take_py(other: Widget) -> int:`(验证 `map_type` 嵌套递归)。 +- [x] 3.5 `instance_method_self_return_py311_keeps_t_self`(对照):instance `fn echo(&self) -> Self` + py3.11,断言 `def echo(self) -> t.Self:`。 +- [x] 3.6 `classmethod_self_return_py311_keeps_t_self`(对照):classmethod `#[classmethod] fn factory(cls) -> Self` + py3.11,断言 `def factory(cls) -> t.Self:`。 +- [x] 3.7 `mixed_methods_in_one_class_py311_static_uses_class_name_others_t_self`(防 guard 泄漏):同类内 instance/classmethod/static 各一,断言三者分别为 `t.Self` / `t.Self` / `Widget`。 +- [x] 3.8 `static_method_self_return_py39_unchanged_class_name`(回归):staticmethod `-> Self` + py3.9,断言 `def make_direct() -> Widget:` 且含 `from __future__ import annotations`。 ## 4. 验证 -- [ ] 4.1 `cargo test` 与 `cargo clippy --all-targets` 全绿、无回归。 -- [ ] 4.2 端到端:在临时目录建最小 PyO3 crate(`#[pymodule] mod` 内一个 `#[pyclass]`,含 `#[staticmethod]` 返回/参数 `Self` 各形态 + 一个 instance method `-> Self` 对照),分别用 `python_version = "3.11"` 与 `"3.9"` 跑 `rylai` 生成 stub;确认 py3.11 stub 中 staticmethod → 类名、instance → `t.Self`,py3.9 全部类名。 -- [ ] 4.3 类型检查器:对 py3.11 stub 跑 `ty check --python-version 3.11`,确认 `All checks passed!`(对照修复前报 `Self cannot be used in a static method`);py3.9 stub 同样 pass。 +- [x] 4.1 `cargo test` 与 `cargo clippy --all-targets` 全绿、无回归。 +- [x] 4.2 端到端:在临时目录建最小 PyO3 crate(`#[pymodule] mod` 内一个 `#[pyclass]`,含 `#[staticmethod]` 返回/参数 `Self` 各形态 + 一个 instance method `-> Self` 对照),分别用 `python_version = "3.11"` 与 `"3.9"` 跑 `rylai` 生成 stub;确认 py3.11 stub 中 staticmethod → 类名、instance → `t.Self`,py3.9 全部类名。 +- [x] 4.3 类型检查器:对 py3.11 stub 跑 `ty check --python-version 3.11`,确认 `All checks passed!`(对照修复前报 `Self cannot be used in a static method`);py3.9 stub 同样 pass。 diff --git a/src/generator.rs b/src/generator.rs index 241512d..728a8bf 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -47,6 +47,7 @@ pub fn generate_with_known_classes( needs_union: false, warnings, current_self_type: None, + in_static_method: false, known_classes, current_stub_module: cross_import.map(|(s, _)| s.to_string()), class_rust_to_module: cross_import.map(|(_, m)| (*m).clone()), @@ -159,16 +160,22 @@ pub(crate) fn generate(modules: &[PyModule], config: &Config) -> Result // ── Generation context ─────────────────────────────────────────────────────── -/// Restores `GenCtx::current_self_type` to `None` when dropped so that early returns -/// (e.g. via `?`) from `gen_method` do not leave the context in a stale state. -struct RestoreCurrentSelfTypeGuard(*mut Option); +/// Restores `GenCtx::current_self_type` to `None` and `in_static_method` to `false` +/// when dropped so that early returns (e.g. via `?`) from `gen_method` do not leave the +/// context in a stale state. +struct RestoreMethodContextGuard { + self_type: *mut Option, + in_static: *mut bool, +} -impl Drop for RestoreCurrentSelfTypeGuard { +impl Drop for RestoreMethodContextGuard { fn drop(&mut self) { - // SAFETY: the pointer is taken from `&mut self.current_self_type` in `gen_method` - // and is only used while that method is running; the guard is dropped on return. + // SAFETY: the pointers are taken from `&mut self.current_self_type` and + // `&mut self.in_static_method` in `gen_method` and are only used while that + // method is running; the guard is dropped on return. unsafe { - *self.0 = None; + *self.self_type = None; + *self.in_static = false; } } } @@ -209,6 +216,10 @@ struct GenCtx<'a> { /// Set to the Python class name while generating methods for that class, /// so that Rust `Self` return types resolve correctly. current_self_type: Option, + /// True while generating a `#[staticmethod]`. PEP 673 rejects `Self` in static + /// methods, so under `native_self` (py >= 3.11) `Self` still resolves to the class + /// name instead of emitting the `t.Self` keyword. + in_static_method: bool, /// Maps each `#[pyclass]` Rust struct name to its Python-visible class name, collected /// before generation starts. This allows return types like `-> PyResult` /// (where the Rust struct is `PyPageIterator` but the Python name is `PageIterator`) to @@ -239,9 +250,19 @@ impl<'a> GenCtx<'a> { return Ok(ov.clone()); } + // PEP 673 rejects `Self` in staticmethods. Under `native_self` (py >= 3.11), force + // the class-name path by locally disabling `native_self`; recursion inside `map_type` + // (`Py`, `PyResult`, `Vec`, ...) then follows the class-name branch. + let effective_policy = if self.in_static_method && self.policy.native_self { + let mut p = self.policy.clone(); + p.native_self = false; + p + } else { + self.policy.clone() + }; let mapping = type_map::map_type( &py_type.rust_type, - &self.policy, + &effective_policy, self.current_self_type.as_deref(), &self.known_classes, ); @@ -817,8 +838,11 @@ impl<'a> GenCtx<'a> { indent: usize, ) -> Result<()> { self.current_self_type = Some(class.name.clone()); - let _guard = - RestoreCurrentSelfTypeGuard(&mut self.current_self_type as *mut Option); + self.in_static_method = matches!(m.kind, MethodKind::Static); + let _guard = RestoreMethodContextGuard { + self_type: &mut self.current_self_type as *mut Option, + in_static: &mut self.in_static_method as *mut bool, + }; let pad = " ".repeat(indent); // Initializer mode detection (see design decision 2): the class has a method whose literal name is `__init__` and that is not `#[new]`. let has_explicit_init = Self::class_has_explicit_init(class); @@ -1928,10 +1952,11 @@ mod tests { ); } - /// With python_version 3.12 (native_self), stub must NOT add future_annotations, - /// must add `import typing as t`, and return type must be `t.Self`. + /// With python_version 3.12 (native_self), a staticmethod returning Self must still + /// render the class name (PEP 673 rejects `Self` in staticmethods), while NOT adding + /// `from __future__ import annotations` and still importing typing. #[test] - fn render_policy_py312_emits_native_self_and_no_future_annotations() { + fn render_policy_py312_staticmethod_self_uses_class_name() { let config = config_with_python_version("3.12"); let class = make_class_with_self_return("PdfDocument", "from_bytes"); let stub = stub_for_config(vec![PyItem::Class(class)], &config); @@ -1941,11 +1966,250 @@ mod tests { ); assert!( stub.contains(TYPING_IMPORT_LINE), - "py 3.12 must import typing when Self is used, got:\n{stub}" + "py 3.12 must import typing, got:\n{stub}" + ); + assert!( + stub.contains("-> PdfDocument:"), + "py 3.12 staticmethod Self must render the class name, not t.Self (PEP 673), got:\n{stub}" + ); + } + + // ── staticmethod Self rendering (PEP 673) ──────────────────────────────── + // `Self` is rejected in staticmethods by PEP 673, so it must always render the class + // name regardless of `native_self`. Instance methods / classmethods keep `t.Self`. + + /// A `#[staticmethod]` returning bare `Self` under py3.11 (native_self) MUST render the + /// class name, not `t.Self`. + #[test] + fn static_method_bare_self_return_py311_uses_class_name() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "make_direct", + MethodKind::Static, + vec![], + syn::parse_quote! { Self }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def make_direct() -> Widget:"), + "py3.11 staticmethod Self must render class name, got:\n{stub}" + ); + assert!( + !stub.contains("t.Self"), + "py3.11 staticmethod must NOT emit t.Self, got:\n{stub}" + ); + } + + /// A `#[staticmethod]` returning `PyResult` under py3.11 MUST unwrap to the class name. + #[test] + fn static_method_pyresult_self_return_py311_uses_class_name() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "from_int", + MethodKind::Static, + vec![make_param("x", syn::parse_quote! { i64 })], + syn::parse_quote! { pyo3::PyResult }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def from_int(x: int) -> Widget:"), + "py3.11 staticmethod PyResult must unwrap to class name, got:\n{stub}" + ); + } + + /// A `#[staticmethod]` with a bare `Self` parameter under py3.11 MUST render the class name. + #[test] + fn static_method_bare_self_param_py311_uses_class_name() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "take_direct", + MethodKind::Static, + vec![make_param("other", syn::parse_quote! { Self })], + syn::parse_quote! { i64 }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def take_direct(other: Widget) -> int:"), + "py3.11 staticmethod Self param must render class name, got:\n{stub}" + ); + } + + /// A `#[staticmethod]` with a `Py` parameter under py3.11 MUST recurse to the class + /// name (verifies `map_type` recursion follows the class-name path under the forced policy). + #[test] + fn static_method_py_self_param_py311_uses_class_name() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "take_py", + MethodKind::Static, + vec![make_param("other", syn::parse_quote! { pyo3::Py })], + syn::parse_quote! { i64 }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def take_py(other: Widget) -> int:"), + "py3.11 staticmethod Py param must recurse to class name, got:\n{stub}" + ); + } + + /// A `#[staticmethod]` with a `PyRef<'_, Self>` parameter under py3.11 MUST recurse to the + /// class name. `PyRef` is a separate `map_type` branch from `Py` (task 3.4), so this locks + /// the forced-policy path for the PyO3 reference-return wrapper independently. + #[test] + fn static_method_pyref_self_param_py311_uses_class_name() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "take_pyref", + MethodKind::Static, + vec![make_param( + "other", + syn::parse_quote! { pyo3::PyRef<'_, Self> }, + )], + syn::parse_quote! { i64 }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def take_pyref(other: Widget) -> int:"), + "py3.11 staticmethod PyRef<'_, Self> param must recurse to class name, got:\n{stub}" ); + } + + /// A `#[staticmethod]` with a `Bound<'_, Self>` parameter under py3.11 MUST recurse to the + /// class name. `Bound` shares the `Py`/`Bound`/`Borrowed` branch but `generic_args` must skip + /// the `'_` lifetime to reach `Self` — this guards both that skip and the forced policy. + #[test] + fn static_method_bound_self_param_py311_uses_class_name() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "take_bound", + MethodKind::Static, + vec![make_param( + "other", + syn::parse_quote! { pyo3::Bound<'_, Self> }, + )], + syn::parse_quote! { i64 }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def take_bound(other: Widget) -> int:"), + "py3.11 staticmethod Bound<'_, Self> param must recurse to class name, got:\n{stub}" + ); + } + + /// An instance method returning `Self` under py3.11 MUST keep `t.Self` (PEP 673 accepts it). + #[test] + fn instance_method_self_return_py311_keeps_t_self() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "echo", + MethodKind::Instance, + vec![], + syn::parse_quote! { Self }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def echo(self) -> t.Self:"), + "py3.11 instance method must keep t.Self (PEP 673 accepts), got:\n{stub}" + ); + } + + /// A classmethod returning `Self` under py3.11 MUST keep `t.Self` (PEP 673 accepts it). + #[test] + fn classmethod_self_return_py311_keeps_t_self() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "factory", + MethodKind::Class, + vec![], + syn::parse_quote! { Self }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); assert!( - stub.contains("-> t.Self:") || stub.contains("-> t.Self :"), - "py 3.12 must use t.Self as return type, got:\n{stub}" + stub.contains("-> t.Self:"), + "py3.11 classmethod Self return must render as t.Self (PEP 673 accepts), got:\n{stub}" + ); + } + + /// Within one class, instance/classmethod keep `t.Self` while staticmethod uses the class + /// name — verifies `in_static_method` resets between methods (no guard leak). + #[test] + fn mixed_methods_in_one_class_py311_static_uses_class_name_others_t_self() { + let config = config_with_python_version("3.11"); + let class = make_class_with_methods( + "Widget", + vec![ + make_method( + "a", + MethodKind::Instance, + vec![], + syn::parse_quote! { Self }, + ), + make_method("b", MethodKind::Class, vec![], syn::parse_quote! { Self }), + make_method("c", MethodKind::Static, vec![], syn::parse_quote! { Self }), + ], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def a(self) -> t.Self:"), + "instance got:\n{stub}" + ); + assert!( + stub.contains("def c() -> Widget:"), + "staticmethod got:\n{stub}" + ); + assert_eq!( + stub.matches("-> t.Self:").count(), + 2, + "instance + classmethod both keep t.Self (no staticmethod guard leak), got:\n{stub}" + ); + } + + /// A `#[staticmethod]` returning `Self` under py3.9 MUST render the class name with + /// `from __future__ import annotations` (unchanged behavior, no regression). + #[test] + fn static_method_self_return_py39_unchanged_class_name() { + let config = config_with_python_version("3.9"); + let class = make_class_with_methods( + "Widget", + vec![make_method( + "make_direct", + MethodKind::Static, + vec![], + syn::parse_quote! { Self }, + )], + ); + let stub = stub_for_config(vec![PyItem::Class(class)], &config); + assert!( + stub.contains("def make_direct() -> Widget:"), + "py3.9 staticmethod must render class name, got:\n{stub}" + ); + assert!( + stub.contains("from __future__ import annotations"), + "py3.9 must emit future annotations, got:\n{stub}" ); } From 5011727b24eac532ddc55204811f715b30d565cf Mon Sep 17 00:00:00 2001 From: monchin Date: Sun, 14 Jun 2026 16:19:49 +0800 Subject: [PATCH 3/3] chore(opsx): archive --- CHANGELOG.md | 2 +- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/constructor-emission/spec.md | 0 .../specs/self-receiver-detection/spec.md | 0 .../specs/self-type-rendering/spec.md | 0 .../tasks.md | 0 openspec/specs/constructor-emission/spec.md | 4 +- .../specs/self-receiver-detection/spec.md | 4 +- openspec/specs/self-type-rendering/spec.md | 57 +++++++++++++++++++ 11 files changed, 63 insertions(+), 4 deletions(-) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/.openspec.yaml (100%) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/design.md (100%) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/proposal.md (100%) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/specs/constructor-emission/spec.md (100%) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/specs/self-receiver-detection/spec.md (100%) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/specs/self-type-rendering/spec.md (100%) rename openspec/changes/{fix-self-in-staticmethod => archive/2026-06-14-fix-self-in-staticmethod}/tasks.md (100%) create mode 100644 openspec/specs/self-type-rendering/spec.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a89f1b8..7774872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Exclude `Self`-dependent PyO3 receiver types (`Py`, `Bound<'_, Self>`, `&Bound<'_, Self>`, `&Borrowed<'_, Self>`, `PyRef<'_, Self>`, `PyRefMut<'_, Self>`) from generated stub parameters **only at the receiver position** (first parameter of an instance method, getter, or setter). The same type used as a later parameter (e.g. `fn m(&self, other: Py)`) or in a `#[staticmethod]` / `#[classmethod]` is now kept as a regular parameter and mapped to the class type. Fixes incorrect stubs where such parameters were either leaked as extra `self`-like bindings (`slf`, `slf_handle`, …) or silently dropped from the signature — both triggered mypy/pyright errors at correct call sites. (#5, #6) -- Render Rust `Self` as the class name (not the `t.Self` keyword) inside `#[staticmethod]` signatures under `native_self` (Python ≥ 3.11). [PEP 673](https://peps.python.org/pep-0673/) rejects `Self` in static methods, so the previous output (e.g. `def make() -> t.Self:`) failed type checking with `Self cannot be used in a static method`. Applies to return types, parameters, and nested wrappers (`Py`, `PyResult`, `Vec`, `PyRef<'_, Self>`, `Bound<'_, Self>`, …); instance methods and classmethods still render `t.Self`. Behavior under `python_version` < 3.11 is unchanged (the class name was already emitted there). +- Render Rust `Self` as the class name (not the `t.Self` keyword) inside `#[staticmethod]` signatures under `native_self` (Python ≥ 3.11). [PEP 673](https://peps.python.org/pep-0673/) rejects `Self` in static methods, so the previous output (e.g. `def make() -> t.Self:`) failed type checking with `Self cannot be used in a static method`. Applies to return types, parameters, and nested wrappers (`Py`, `PyResult`, `Vec`, `PyRef<'_, Self>`, `Bound<'_, Self>`, …); instance methods and classmethods still render `t.Self`. Behavior under `python_version` < 3.11 is unchanged (the class name was already emitted there). (#8) ## [0.4.1] - 2026-06-03 diff --git a/openspec/changes/fix-self-in-staticmethod/.openspec.yaml b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/.openspec.yaml similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/.openspec.yaml rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/.openspec.yaml diff --git a/openspec/changes/fix-self-in-staticmethod/design.md b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/design.md similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/design.md rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/design.md diff --git a/openspec/changes/fix-self-in-staticmethod/proposal.md b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/proposal.md similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/proposal.md rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/proposal.md diff --git a/openspec/changes/fix-self-in-staticmethod/specs/constructor-emission/spec.md b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/specs/constructor-emission/spec.md similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/specs/constructor-emission/spec.md rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/specs/constructor-emission/spec.md diff --git a/openspec/changes/fix-self-in-staticmethod/specs/self-receiver-detection/spec.md b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/specs/self-receiver-detection/spec.md similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/specs/self-receiver-detection/spec.md rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/specs/self-receiver-detection/spec.md diff --git a/openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/specs/self-type-rendering/spec.md similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/specs/self-type-rendering/spec.md rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/specs/self-type-rendering/spec.md diff --git a/openspec/changes/fix-self-in-staticmethod/tasks.md b/openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/tasks.md similarity index 100% rename from openspec/changes/fix-self-in-staticmethod/tasks.md rename to openspec/changes/archive/2026-06-14-fix-self-in-staticmethod/tasks.md diff --git a/openspec/specs/constructor-emission/spec.md b/openspec/specs/constructor-emission/spec.md index 7700974..38babdf 100644 --- a/openspec/specs/constructor-emission/spec.md +++ b/openspec/specs/constructor-emission/spec.md @@ -40,7 +40,9 @@ ### Requirement: `__new__` 返回类型遵循版本感知的 `Self` 渲染 -Initializer 模式下 `#[new]` 生成的 `__new__` 返回类型 MUST 经由现有 `Self` 渲染机制产出:当 `python_version` ≥ 3.11(PEP 673 `nativeSelf`)时 MUST 为 `t.Self`;当 `python_version` < 3.11 时 MUST 为当前类的类名。该渲染 MUST 与"返回 `Self` 的 staticmethod"行为完全一致。 +Initializer 模式下 `#[new]` 生成的 `__new__` 返回类型 MUST 经由 `Self` 渲染机制产出(规则见 `self-type-rendering` capability):当 `python_version` ≥ 3.11(PEP 673 `nativeSelf`)时 MUST 为 `t.Self`;当 `python_version` < 3.11 时 MUST 为当前类的类名。 + +`__new__` 属 PEP 673 接受 `Self` 的位置(类构造器,带 `cls`),故其渲染与 instance method 一致;而 `#[staticmethod]` 因 PEP 673 例外改渲染类名(见 `self-type-rendering`)——**二者不再等价**,旧表述“MUST 与返回 `Self` 的 staticmethod 行为完全一致”据此作废。 #### Scenario: `python_version` 3.12 下 `__new__` 返回 `t.Self` - **WHEN** Initializer 模式下 `python_version` 配置为 `3.12` diff --git a/openspec/specs/self-receiver-detection/spec.md b/openspec/specs/self-receiver-detection/spec.md index e7b9bad..2cc497d 100644 --- a/openspec/specs/self-receiver-detection/spec.md +++ b/openspec/specs/self-receiver-detection/spec.md @@ -55,11 +55,11 @@ "receiver 位置"定义为:方法**带有实例 receiver**(`MethodKind::Instance` / `Getter` / `Setter`,即非 `#[staticmethod]` / `#[classmethod]` / `#[new]`),且该参数为方法签名中的首个参数,其前不存在 `&self` / `&mut self` / `self`(即无 `FnArg::Receiver`)。 -一旦方法的 receiver 被确认(出现了 `FnArg::Receiver`,或首个 typed 参数已被识别为 receiver),后续所有依赖 `Self` 的 receiver 类型参数**必须作为普通参数保留**在 stub 中,并映射为对应的 Python 类类型。 +一旦方法的 receiver 被确认(出现了 `FnArg::Receiver`,或首个 typed 参数已被识别为 receiver),后续所有依赖 `Self` 的 receiver 类型参数**MUST 作为普通参数保留**在 stub 中,并映射为对应的 Python 类类型。 `Python<'_>`、`&Bound<'_, PyModule>` 等与位置无关的纯注入类型不受此规则约束,继续在任何位置无条件排除。 -补充:被保留为普通参数的 `Py` / `Bound<'_, Self>` 等类型,其 Python 类型取决于渲染策略——py < 3.11 映射为类名(如 `Foo`),py ≥ 3.11(`nativeSelf`)映射为 `t.Self`。下方 scenario 以默认策略(类名)示例,两种映射均满足"作为普通参数保留"的契约。 +补充:被保留为普通参数的 `Py` / `Bound<'_, Self>` 等类型,其 Python 类型遵循 `self-type-rendering` capability 的规则——对 instance method / classmethod 的普通参数位置,py < 3.11 映射为类名(如 `Foo`)、py ≥ 3.11 映射为 `t.Self`;对 `#[staticmethod]` 中的此类参数,无论版本 MUST 映射为类名(PEP 673)。下方 scenario 以默认策略(类名)示例,均满足“作为普通参数保留”的契约。 #### Scenario: Py 在普通参数位置必须保留 - **WHEN** `#[pymethods]` 方法声明为 `fn by_normal(&self, other: Py) -> i64` diff --git a/openspec/specs/self-type-rendering/spec.md b/openspec/specs/self-type-rendering/spec.md new file mode 100644 index 0000000..bba277a --- /dev/null +++ b/openspec/specs/self-type-rendering/spec.md @@ -0,0 +1,57 @@ +# Self Type Rendering + +## Purpose + +规格化 Rust `Self` 在生成的 Python stub(`.pyi`)中的渲染策略:根据方法种类(instance / classmethod / `__new__` / `#[staticmethod]`)与 `python_version`,决定 `Self` 渲染为 `t.Self` 关键字还是当前类的 Python 类名,以符合 [PEP 673](https://peps.python.org/pep-0673/) 对 `Self` 合法位置的规定。 + +## Requirements + +### Requirement: 非 staticmethod 方法的 Self 渲染遵循版本感知策略 + +当 `#[pymethods]` 方法**不是** `#[staticmethod]`(即 instance method / classmethod / `#[new]`),且其返回类型或参数类型涉及 Rust `Self`(含裸 `Self` 及 `Py` / `PyRef<'_, Self>` / `Bound<'_, Self>` / `PyResult` / `Vec` / `Option` 等嵌套)时,该 `Self` MUST 按 `python_version` 渲染:`python_version` ≥ 3.11(PEP 673 `nativeSelf`)MUST 渲染为 `t.Self`;`python_version` < 3.11 MUST 渲染为当前类的 Python 类名(作为 forward reference,此时 stub MUST 包含 `from __future__ import annotations`)。 + +#### Scenario: instance method 返回 Self 在 py3.11 渲染为 t.Self +- **WHEN** `#[pymethods]` instance method 声明为 `fn echo(&self) -> Self`,`python_version = 3.11` +- **THEN** 生成的 stub MUST 为 `def echo(self) -> t.Self:`,且 stub MUST 包含 `import typing as t` + +#### Scenario: instance method 返回 Self 在 py3.9 渲染为类名 +- **WHEN** 同上方法,`python_version = 3.9`,类名为 `Widget` +- **THEN** 生成的 stub MUST 为 `def echo(self) -> Widget:`,且 stub MUST 包含 `from __future__ import annotations`、MUST NOT 出现 `t.Self` + +#### Scenario: classmethod 返回 Self 在 py3.11 渲染为 t.Self +- **WHEN** `#[pymethods]` classmethod 声明为 `#[classmethod] fn factory(cls) -> Self`,`python_version = 3.11` +- **THEN** 该 classmethod 的 `Self` 返回类型 MUST 渲染为 `t.Self:`(PEP 673 接受 classmethod 中的 `Self`) + +#### Scenario: Initializer 模式下 `__new__` 返回 Self 在 py3.11 渲染为 t.Self +- **WHEN** Initializer 模式下 `#[new]` 方法返回 `Self`(或 `PyResult`),`python_version = 3.11` +- **THEN** 生成的 `__new__` 返回类型 MUST 为 `t.Self` + +### Requirement: staticmethod 中的 Self 必须始终渲染为类名 + +当 `#[staticmethod]` 方法的返回类型或参数类型涉及 Rust `Self`(含裸 `Self` 及 `Py` / `PyResult` / `Vec` / `Option` 等嵌套)时,该 `Self` MUST **始终**渲染为当前类的 Python 类名,**无论 `python_version`**。因为 [PEP 673](https://peps.python.org/pep-0673/#valid-locations-for-self) 拒绝 staticmethod 中的 `Self`(没有 `self`/`cls` 可绑定),`t.Self` 会被类型检查器拒绝(实测 `ty` 报 `Self cannot be used in a static method`)。 + +py ≥ 3.11 时,类体内 forward reference(类名)在 `.pyi` 中合法,rylai MUST NOT 仅为 staticmethod 而额外引入 `from __future__ import annotations`。 + +#### Scenario: staticmethod 裸 Self 返回在 py3.11 渲染为类名 +- **WHEN** `#[staticmethod] fn make_direct() -> Self`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def make_direct() -> Widget:`,且该方法 MUST NOT 出现 `t.Self` + +#### Scenario: staticmethod `PyResult` 返回在 py3.11 渲染为类名 +- **WHEN** `#[staticmethod] fn from_int(x: i64) -> PyResult`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def from_int(x: int) -> Widget:` + +#### Scenario: staticmethod 裸 Self 参数在 py3.11 渲染为类名 +- **WHEN** `#[staticmethod] fn take_direct(other: Self) -> i64`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def take_direct(other: Widget) -> int:` + +#### Scenario: staticmethod `Py` 参数在 py3.11 渲染为类名(嵌套递归) +- **WHEN** `#[staticmethod] fn take_py(other: Py) -> i64`,`python_version = 3.11`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def take_py(other: Widget) -> int:` + +#### Scenario: staticmethod 的 Self 在 py3.9 行为不变 +- **WHEN** `#[staticmethod] fn make_direct() -> Self`,`python_version = 3.9`,类名 `Widget` +- **THEN** 生成的 stub MUST 为 `def make_direct() -> Widget:`(与 py3.11 一致,本就用类名) + +#### Scenario: 同类内 staticmethod 与其他方法共存时渲染互不干扰 +- **WHEN** 同一 `#[pyclass]` 内含 instance method `fn a(&self) -> Self`、classmethod `#[classmethod] fn b(cls) -> Self`、staticmethod `#[staticmethod] fn c() -> Self`,`python_version = 3.11`,类名 `Widget` +- **THEN** instance method `a` 与 classmethod `b` 的 `Self` 返回类型 MUST 渲染为 `t.Self:`,staticmethod `c` 的 MUST 渲染为 `Widget:`(staticmethod 的类名渲染 MUST NOT 泄漏到相邻的 instance/classmethod)