From 6e89e0abff2ebd677734690abf2e05067d011013 Mon Sep 17 00:00:00 2001 From: Enric Calabuig Date: Tue, 13 Jan 2026 15:29:58 +0100 Subject: [PATCH] [ty] Fix classmethod + contextmanager + Self (#22407) ## Summary The test I've added illustrates the fix. Copying it here too: ```python from contextlib import contextmanager from typing import Iterator from typing_extensions import Self class Base: @classmethod @contextmanager def create(cls) -> Iterator[Self]: yield cls() class Child(Base): ... with Base.create() as base: reveal_type(base) # revealed: Base (after the fix, None before) with Child.create() as child: reveal_type(child) # revealed: Child (after the fix, None before) ``` Full disclosure: I've used LLMs for this PR, but the result is thoroughly reviewed by me before submitting. I'm excited about my first Rust contribution to Astral tools and will address feedback quickly. Related to https://github.com/astral-sh/ty/issues/2030, I am working on a fix for the TypeVar case also reported in that issue (by me) ## Test Plan Updated mdtests --------- Co-authored-by: Douglas Creager --- .../resources/mdtest/call/methods.md | 36 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 9 ++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index c15e14a615..c6adcbbbdc 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -464,6 +464,42 @@ reveal_type(C.f2(1)) # revealed: str reveal_type(C().f2(1)) # revealed: str ``` +### Classmethods with `Self` and callable-returning decorators + +When a classmethod is decorated with a decorator that returns a callable type (like +`@contextmanager`), `Self` in the return type should correctly resolve to the subclass when accessed +on a derived class. + +```py +from contextlib import contextmanager +from typing import Iterator +from typing_extensions import Self + +class Base: + @classmethod + @contextmanager + def create(cls) -> Iterator[Self]: + yield cls() + +class Child(Base): ... + +reveal_type(Base.create()) # revealed: _GeneratorContextManager[Base, None, None] +with Base.create() as base: + reveal_type(base) # revealed: Base + +reveal_type(Base().create()) # revealed: _GeneratorContextManager[Base, None, None] +with Base().create() as base: + reveal_type(base) # revealed: Base + +reveal_type(Child.create()) # revealed: _GeneratorContextManager[Child, None, None] +with Child.create() as child: + reveal_type(child) # revealed: Child + +reveal_type(Child().create()) # revealed: _GeneratorContextManager[Child, None, None] +with Child().create() as child: + reveal_type(child) # revealed: Child +``` + ### `__init_subclass__` The [`__init_subclass__`] method is implicitly a classmethod: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e923c4df48..4c37f17ba1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2645,8 +2645,15 @@ impl<'db> Type<'db> { return if instance.is_none(db) && callable.is_function_like(db) { Some((self, AttributeKind::NormalOrNonDataDescriptor)) } else { + // For classmethod-like callables, bind to the owner class. For function-like callables, bind to the instance. + let self_type = if callable.is_classmethod_like(db) && instance.is_none(db) { + owner.to_instance(db).unwrap_or(owner) + } else { + instance + }; + Some(( - Type::Callable(callable.bind_self(db, Some(instance))), + Type::Callable(callable.bind_self(db, Some(self_type))), AttributeKind::NormalOrNonDataDescriptor, )) };