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, )) };