From 2214a46139e7ea110505280591a709404524d098 Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Tue, 16 Dec 2025 13:37:11 -0500 Subject: [PATCH] [ty] Don't use implicit superclass annotation when converting a class constructor into a `Callable` (#22011) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes a bug @zsol found running ty against pyx. His original repro is: ```py class Base: def __init__(self) -> None: pass class A(Base): pass def foo[T](callable: Callable[..., T]) -> T: return callable() a: A = foo(A) ``` The call at the bottom would fail, since we would infer `() -> Base` as the callable type of `A`, when it should be `() -> A`. The issue was how we add implicit annotations to `self` parameters. Typically, we turn it into `self: Self`. But in cases where we don't need to introduce a full typevar, we turn it into `self: [the class itself]` — in this case, `self: Base`. Then, when turning the class constructor into a callable, we would see this non-`Self` annotation and think that it was important and load-bearing. The fix is that we skip all implicit annotations when determining whether the `self` annotation should take precedence in the callable's return type. --- .../resources/mdtest/annotations/callable.md | 18 ++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 1 + .../ty_python_semantic/src/types/signatures.rs | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 728566d30e..34dd6e7d5d 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -407,4 +407,22 @@ def f_okay(c: Callable[[], None]): c.__qualname__ = "my_callable" # okay ``` +## From a class + +### Subclasses should return themselves, not superclass + +```py +from ty_extensions import into_callable + +class Base: + def __init__(self) -> None: + pass + +class A(Base): + pass + +# revealed: () -> A +reveal_type(into_callable(A)) +``` + [gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d5fd42ace7..a503421713 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1257,6 +1257,7 @@ impl<'db> ClassType<'db> { let self_annotation = signature .parameters() .get_positional(0) + .filter(|parameter| !parameter.inferred_annotation) .and_then(Parameter::annotated_type) .filter(|ty| { ty.as_typevar() diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 0a70337930..33266f8dde 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -2167,7 +2167,7 @@ pub(crate) struct Parameter<'db> { /// Does the type of this parameter come from an explicit annotation, or was it inferred from /// the context, like `Self` for the `self` parameter of instance methods. - inferred_annotation: bool, + pub(crate) inferred_annotation: bool, kind: ParameterKind<'db>, pub(crate) form: ParameterForm,