Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>`, `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<Self>)`) 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<Self>`, `PyResult<Self>`, `Vec<Self>`, `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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-14
Original file line number Diff line number Diff line change
@@ -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<Self>` 返回、裸参数、`Py<Self>` 参数——加一个 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<Self>`、`PyResult<Self>`、`Vec<Self>`、`Option<Self>` 等嵌套)始终渲染为 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<Self>` / `PyResult<Self>` / `Vec<Self>` / `Option<Self>` 等嵌套。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 无变化。
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

rylai 在 `python_version ≥ 3.11`(`native_self` 渲染策略)时,会把 `#[staticmethod]` 中的 `Self`——无论返回类型还是参数类型,无论裸 `Self` 还是 `Py<Self>` / `PyResult<Self>` / `Vec<Self>` 等嵌套——渲染成 `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<Self>`、`PyResult<Self>`、`Vec<Self>` 等嵌套形态自动正确)。
- 修改 collector/parser 层(收集层原样保留 `Self`,渲染决策在 generator)。
Original file line number Diff line number Diff line change
@@ -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`
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
## MODIFIED Requirements

### Requirement: 依赖 Self 的 receiver 类型仅在 receiver 位置被排除

当 `#[pymethods]` 方法的参数类型为依赖 `Self` 的 receiver 类型(`Py<Self>`、`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<Self>` / `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<Self> 在普通参数位置必须保留
- **WHEN** `#[pymethods]` 方法声明为 `fn by_normal(&self, other: Py<Self>) -> i64`
- **THEN** 生成的 stub MUST 为 `def by_normal(self, other: Foo) -> int`,`other` 作为普通参数保留,MUST NOT 删除

#### Scenario: receiver 位置之后再次出现的 Py<Self> 必须保留
- **WHEN** `#[pymethods]` 方法声明为 `fn m(slf: Py<Self>, other: Py<Self>) -> i64`(首个 `Py<Self>` 作 receiver,第二个作普通参数)
- **THEN** 生成的 stub MUST 为 `def m(self, other: Foo) -> int`,仅首个 `slf` 被排除,`other` 作为普通参数保留

#### Scenario: staticmethod 中的 Py<Self> 必须作为普通参数保留
- **WHEN** `#[pymethods]` 中带 `#[staticmethod]` 的方法声明为 `fn make(other: Py<Self>) -> i64`
- **THEN** 生成的 stub MUST 为 `def make(other: Foo) -> int`(无 `self`),`other` 作为普通参数保留,因为静态方法没有 receiver

#### Scenario: Py<Self> 在 receiver 位置仍被正确排除(不回归)
- **WHEN** `#[pymethods]` 实例方法声明为 `fn by_receiver(slf: Py<Self>, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## ADDED Requirements

### Requirement: 非 staticmethod 方法的 Self 渲染遵循版本感知策略

当 `#[pymethods]` 方法**不是** `#[staticmethod]`(即 instance method / classmethod / `#[new]`),且其返回类型或参数类型涉及 Rust `Self`(含裸 `Self` 及 `Py<Self>` / `PyRef<'_, Self>` / `Bound<'_, Self>` / `PyResult<Self>` / `Vec<Self>` / `Option<Self>` 等嵌套)时,该 `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** 该 classmethod 的 `Self` 返回类型 MUST 渲染为 `t.Self:`(PEP 673 接受 classmethod 中的 `Self`)

#### Scenario: Initializer 模式下 `__new__` 返回 Self 在 py3.11 渲染为 t.Self
- **WHEN** Initializer 模式下 `#[new]` 方法返回 `Self`(或 `PyResult<Self>`),`python_version = 3.11`
- **THEN** 生成的 `__new__` 返回类型 MUST 为 `t.Self`

### Requirement: staticmethod 中的 Self 必须始终渲染为类名

当 `#[staticmethod]` 方法的返回类型或参数类型涉及 Rust `Self`(含裸 `Self` 及 `Py<Self>` / `PyResult<Self>` / `Vec<Self>` / `Option<Self>` 等嵌套)时,该 `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<Self>` 返回在 py3.11 渲染为类名
- **WHEN** `#[staticmethod] fn from_int(x: i64) -> PyResult<Self>`,`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<Self>` 参数在 py3.11 渲染为类名(嵌套递归)
- **WHEN** `#[staticmethod] fn take_py(other: Py<Self>) -> 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)
Loading
Loading