From e42cdf84957b178dc7e37aed0f2b1f4d5f0c272a Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 5 Dec 2025 08:57:21 -0500 Subject: [PATCH] [ty] Carry generic context through when converting class into `Callable` (#21798) When converting a class (whether specialized or not) into a `Callable` type, we should carry through any generic context that the constructor has. This includes both the generic context of the class itself (if it's generic) and of the constructor methods (if they are separately generic). To help test this, this also updates the `generic_context` extension function to work on `Callable` types and unions; and adds a new `into_callable` extension function that works just like `CallableTypeOf`, but on value forms instead of type forms. Pulled this out of #21551 for separate review. --- .../mdtest/generics/legacy/classes.md | 87 +++++++++++++++++ .../mdtest/generics/pep695/classes.md | 97 +++++++++++++++++++ .../resources/mdtest/liskov.md | 2 +- crates/ty_python_semantic/src/types.rs | 9 +- .../ty_python_semantic/src/types/call/bind.rs | 84 +++++++++------- crates/ty_python_semantic/src/types/class.rs | 94 ++++++++++++------ .../ty_python_semantic/src/types/function.rs | 4 + .../ty_python_semantic/src/types/generics.rs | 39 +++++++- .../src/types/signatures.rs | 7 +- .../ty_extensions/ty_extensions.pyi | 4 + 10 files changed, 350 insertions(+), 77 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 9e0696a5c2..bc6ccdb7c1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -301,6 +301,7 @@ consistent with each other. ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") @@ -308,6 +309,11 @@ class C(Generic[T]): def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -318,12 +324,18 @@ wrong_innards: C[int] = C("five") ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") class C(Generic[T]): def __init__(self, x: T) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -334,6 +346,7 @@ wrong_innards: C[int] = C("five") ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") @@ -343,6 +356,11 @@ class C(Generic[T]): def __init__(self, x: T) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -353,6 +371,7 @@ wrong_innards: C[int] = C("five") ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") @@ -362,6 +381,11 @@ class C(Generic[T]): def __init__(self, x: T) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -373,6 +397,11 @@ class D(Generic[T]): def __init__(self, *args, **kwargs) -> None: ... +# revealed: ty_extensions.GenericContext[T@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D(1)) # revealed: D[int] # error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`" @@ -386,6 +415,7 @@ to specialize the class. ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") @@ -398,6 +428,11 @@ class C(Generic[T, U]): class D(C[V, int]): def __init__(self, x: V) -> None: ... +# revealed: ty_extensions.GenericContext[V@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[V@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D(1)) # revealed: D[int] ``` @@ -405,6 +440,7 @@ reveal_type(D(1)) # revealed: D[int] ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") @@ -415,6 +451,11 @@ class C(Generic[T, U]): class D(C[T, U]): pass +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(C(1, "str")) # revealed: C[int, str] reveal_type(D(1, "str")) # revealed: D[int, str] ``` @@ -425,6 +466,7 @@ This is a specific example of the above, since it was reported specifically by a ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") @@ -432,6 +474,11 @@ U = TypeVar("U") class D(dict[T, U]): pass +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D(key=1)) # revealed: D[str, int] ``` @@ -443,12 +490,18 @@ context. But from the user's point of view, this is another example of the above ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") class C(tuple[T, U]): ... +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C((1, 2))) # revealed: C[int, int] ``` @@ -480,6 +533,7 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable S = TypeVar("S") T = TypeVar("T") @@ -487,6 +541,11 @@ T = TypeVar("T") class C(Generic[T]): def __init__(self, x: T, y: S) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C, S@__init__] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1, 1)) # revealed: C[int] reveal_type(C(1, "string")) # revealed: C[int] reveal_type(C(1, True)) # revealed: C[int] @@ -499,6 +558,7 @@ wrong_innards: C[int] = C("five", 1) ```py from typing_extensions import overload, Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U") @@ -514,6 +574,11 @@ class C(Generic[T]): def __init__(self, x: int) -> None: ... def __init__(self, x: str | bytes | int) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C("string")) # revealed: C[str] reveal_type(C(b"bytes")) # revealed: C[bytes] reveal_type(C(12)) # revealed: C[Unknown] @@ -541,6 +606,11 @@ class D(Generic[T, U]): def __init__(self, t: T, u: U) -> None: ... def __init__(self, *args) -> None: ... +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D("string")) # revealed: D[str, str] reveal_type(D(1)) # revealed: D[str, int] reveal_type(D(1, "string")) # revealed: D[int, str] @@ -551,6 +621,7 @@ reveal_type(D(1, "string")) # revealed: D[int, str] ```py from dataclasses import dataclass from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") @@ -558,6 +629,11 @@ T = TypeVar("T") class A(Generic[T]): x: T +# revealed: ty_extensions.GenericContext[T@A] +reveal_type(generic_context(A)) +# revealed: ty_extensions.GenericContext[T@A] +reveal_type(generic_context(into_callable(A))) + reveal_type(A(x=1)) # revealed: A[int] ``` @@ -565,17 +641,28 @@ reveal_type(A(x=1)) # revealed: A[int] ```py from typing_extensions import Generic, TypeVar +from ty_extensions import generic_context, into_callable T = TypeVar("T") U = TypeVar("U", default=T) class C(Generic[T, U]): ... +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C()) # revealed: C[Unknown, Unknown] class D(Generic[T, U]): def __init__(self) -> None: ... +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D()) # revealed: D[Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index a110783ed2..dbb249b45e 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -264,12 +264,19 @@ signatures don't count towards variance). ### `__new__` only ```py +from ty_extensions import generic_context, into_callable + class C[T]: x: T def __new__(cls, x: T) -> "C[T]": return object.__new__(cls) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -279,11 +286,18 @@ wrong_innards: C[int] = C("five") ### `__init__` only ```py +from ty_extensions import generic_context, into_callable + class C[T]: x: T def __init__(self, x: T) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -293,6 +307,8 @@ wrong_innards: C[int] = C("five") ### Identical `__new__` and `__init__` signatures ```py +from ty_extensions import generic_context, into_callable + class C[T]: x: T @@ -301,6 +317,11 @@ class C[T]: def __init__(self, x: T) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -310,6 +331,8 @@ wrong_innards: C[int] = C("five") ### Compatible `__new__` and `__init__` signatures ```py +from ty_extensions import generic_context, into_callable + class C[T]: x: T @@ -318,6 +341,11 @@ class C[T]: def __init__(self, x: T) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1)) # revealed: C[int] # error: [invalid-assignment] "Object of type `C[int | str]` is not assignable to `C[int]`" @@ -331,6 +359,11 @@ class D[T]: def __init__(self, *args, **kwargs) -> None: ... +# revealed: ty_extensions.GenericContext[T@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D(1)) # revealed: D[int] # error: [invalid-assignment] "Object of type `D[int | str]` is not assignable to `D[int]`" @@ -343,6 +376,8 @@ If either method comes from a generic base class, we don't currently use its inf to specialize the class. ```py +from ty_extensions import generic_context, into_callable + class C[T, U]: def __new__(cls, *args, **kwargs) -> "C[T, U]": return object.__new__(cls) @@ -350,18 +385,30 @@ class C[T, U]: class D[V](C[V, int]): def __init__(self, x: V) -> None: ... +# revealed: ty_extensions.GenericContext[V@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[V@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D(1)) # revealed: D[Literal[1]] ``` ### Generic class inherits `__init__` from generic base class ```py +from ty_extensions import generic_context, into_callable + class C[T, U]: def __init__(self, t: T, u: U) -> None: ... class D[T, U](C[T, U]): pass +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(C(1, "str")) # revealed: C[Literal[1], Literal["str"]] reveal_type(D(1, "str")) # revealed: D[Literal[1], Literal["str"]] ``` @@ -371,9 +418,16 @@ reveal_type(D(1, "str")) # revealed: D[Literal[1], Literal["str"]] This is a specific example of the above, since it was reported specifically by a user. ```py +from ty_extensions import generic_context, into_callable + class D[T, U](dict[T, U]): pass +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D(key=1)) # revealed: D[str, int] ``` @@ -384,8 +438,15 @@ for `tuple`, so we use a different mechanism to make sure it has the right inher context. But from the user's point of view, this is another example of the above.) ```py +from ty_extensions import generic_context, into_callable + class C[T, U](tuple[T, U]): ... +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C((1, 2))) # revealed: C[Literal[1], Literal[2]] ``` @@ -409,11 +470,18 @@ def func8(t1: tuple[complex, list[int]], t2: tuple[int, *tuple[str, ...]], t3: t ### `__init__` is itself generic ```py +from ty_extensions import generic_context, into_callable + class C[T]: x: T def __init__[S](self, x: T, y: S) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C, S@__init__] +reveal_type(generic_context(into_callable(C))) + reveal_type(C(1, 1)) # revealed: C[int] reveal_type(C(1, "string")) # revealed: C[int] reveal_type(C(1, True)) # revealed: C[int] @@ -427,6 +495,7 @@ wrong_innards: C[int] = C("five", 1) ```py from __future__ import annotations from typing import overload +from ty_extensions import generic_context, into_callable class C[T]: # we need to use the type variable or else the class is bivariant in T, and @@ -443,6 +512,11 @@ class C[T]: def __init__(self, x: int) -> None: ... def __init__(self, x: str | bytes | int) -> None: ... +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C("string")) # revealed: C[str] reveal_type(C(b"bytes")) # revealed: C[bytes] reveal_type(C(12)) # revealed: C[Unknown] @@ -470,6 +544,11 @@ class D[T, U]: def __init__(self, t: T, u: U) -> None: ... def __init__(self, *args) -> None: ... +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D("string")) # revealed: D[str, Literal["string"]] reveal_type(D(1)) # revealed: D[str, Literal[1]] reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]] @@ -479,24 +558,42 @@ reveal_type(D(1, "string")) # revealed: D[Literal[1], Literal["string"]] ```py from dataclasses import dataclass +from ty_extensions import generic_context, into_callable @dataclass class A[T]: x: T +# revealed: ty_extensions.GenericContext[T@A] +reveal_type(generic_context(A)) +# revealed: ty_extensions.GenericContext[T@A] +reveal_type(generic_context(into_callable(A))) + reveal_type(A(x=1)) # revealed: A[int] ``` ### Class typevar has another typevar as a default ```py +from ty_extensions import generic_context, into_callable + class C[T, U = T]: ... +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(C)) +# revealed: ty_extensions.GenericContext[T@C, U@C] +reveal_type(generic_context(into_callable(C))) + reveal_type(C()) # revealed: C[Unknown, Unknown] class D[T, U = T]: def __init__(self) -> None: ... +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(D)) +# revealed: ty_extensions.GenericContext[T@D, U@D] +reveal_type(generic_context(into_callable(D))) + reveal_type(D()) # revealed: D[Unknown, Unknown] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/liskov.md b/crates/ty_python_semantic/resources/mdtest/liskov.md index ad16b99f08..2dca2dd558 100644 --- a/crates/ty_python_semantic/resources/mdtest/liskov.md +++ b/crates/ty_python_semantic/resources/mdtest/liskov.md @@ -218,8 +218,8 @@ class E(A[int]): def method(self, x: object) -> None: ... # fine class F[T](A[T]): - # TODO: we should emit `invalid-method-override` on this: # `str` is not necessarily a supertype of `T`! + # error: [invalid-method-override] def method(self, x: str) -> None: ... class G(A[int]): diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 61cc160769..6022aad53e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8535,12 +8535,9 @@ impl<'db> TypeMapping<'_, 'db> { | TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults | TypeMapping::EagerExpansion => context, - TypeMapping::BindSelf { .. } => GenericContext::from_typevar_instances( - db, - context - .variables(db) - .filter(|var| !var.typevar(db).is_self(db)), - ), + TypeMapping::BindSelf { + binding_context, .. + } => context.remove_self(db, *binding_context), TypeMapping::ReplaceSelf { new_upper_bound } => GenericContext::from_typevar_instances( db, context.variables(db).map(|typevar| { diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 2093f2f377..da63e45208 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -32,7 +32,9 @@ use crate::types::function::{ use crate::types::generics::{ InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, }; -use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; +use crate::types::signatures::{ + CallableSignature, Parameter, ParameterForm, ParameterKind, Parameters, +}; use crate::types::tuple::{TupleLength, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, @@ -788,51 +790,67 @@ impl<'db> Bindings<'db> { )) }; - let function_generic_context = |function: FunctionType<'db>| { - let union = UnionType::from_elements( - db, - function - .signature(db) - .overloads - .iter() - .filter_map(|signature| signature.generic_context) - .map(wrap_generic_context), - ); - if union.is_never() { - Type::none(db) - } else { - union - } - }; + let signature_generic_context = + |signature: &CallableSignature<'db>| { + UnionType::try_from_elements( + db, + signature.overloads.iter().map(|signature| { + signature.generic_context.map(wrap_generic_context) + }), + ) + }; - // TODO: Handle generic functions, and unions/intersections of - // generic types - overload.set_return_type(match ty { - Type::ClassLiteral(class) => class - .generic_context(db) - .map(wrap_generic_context) - .unwrap_or_else(|| Type::none(db)), + let generic_context_for_simple_type = |ty: Type<'db>| match ty { + Type::ClassLiteral(class) => { + class.generic_context(db).map(wrap_generic_context) + } Type::FunctionLiteral(function) => { - function_generic_context(*function) + signature_generic_context(function.signature(db)) } - Type::BoundMethod(bound_method) => { - function_generic_context(bound_method.function(db)) + Type::BoundMethod(bound_method) => signature_generic_context( + bound_method.function(db).signature(db), + ), + + Type::Callable(callable) => { + signature_generic_context(callable.signatures(db)) } Type::KnownInstance(KnownInstanceType::TypeAliasType( TypeAliasType::PEP695(alias), - )) => alias - .generic_context(db) - .map(wrap_generic_context) - .unwrap_or_else(|| Type::none(db)), + )) => alias.generic_context(db).map(wrap_generic_context), - _ => Type::none(db), - }); + _ => None, + }; + + let generic_context = match ty { + Type::Union(union_type) => UnionType::try_from_elements( + db, + union_type + .elements(db) + .iter() + .map(|ty| generic_context_for_simple_type(*ty)), + ), + _ => generic_context_for_simple_type(*ty), + }; + + overload.set_return_type( + generic_context.unwrap_or_else(|| Type::none(db)), + ); } } + Some(KnownFunction::IntoCallable) => { + let [Some(ty)] = overload.parameter_types() else { + continue; + }; + let Some(callables) = ty.try_upcast_to_callable(db) else { + continue; + }; + overload.set_return_type(callables.into_type(db)); + } + Some(KnownFunction::DunderAllNames) => { if let [Some(ty)] = overload.parameter_types() { overload.set_return_type(match ty { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 06f91502fc..00b5bed3ec 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1133,6 +1133,13 @@ impl<'db> ClassType<'db> { /// constructor signature of this class. #[salsa::tracked(cycle_initial=into_callable_cycle_initial, heap_size=ruff_memory_usage::heap_size)] pub(super) fn into_callable(self, db: &'db dyn Db) -> CallableTypes<'db> { + // TODO: This mimics a lot of the logic in Type::try_call_from_constructor. Can we + // consolidate the two? Can we invoke a class by upcasting the class into a Callable, and + // then relying on the call binding machinery to Just Work™? + + let (class_literal, _) = self.class_literal(db); + let class_generic_context = class_literal.generic_context(db); + let self_ty = Type::from(self); let metaclass_dunder_call_function_symbol = self_ty .member_lookup_with_policy( @@ -1206,39 +1213,58 @@ impl<'db> ClassType<'db> { // If the class defines an `__init__` method, then we synthesize a callable type with the // same parameters as the `__init__` method after it is bound, and with the return type of // the concrete type of `Self`. - let synthesized_dunder_init_callable = - if let Place::Defined(ty, _, _) = dunder_init_function_symbol { - let signature = match ty { - Type::FunctionLiteral(dunder_init_function) => { - Some(dunder_init_function.signature(db)) - } - Type::Callable(callable) => Some(callable.signatures(db)), - _ => None, + let synthesized_dunder_init_callable = if let Place::Defined(ty, _, _) = + dunder_init_function_symbol + { + let signature = match ty { + Type::FunctionLiteral(dunder_init_function) => { + Some(dunder_init_function.signature(db)) + } + Type::Callable(callable) => Some(callable.signatures(db)), + _ => None, + }; + + if let Some(signature) = signature { + let synthesized_signature = |signature: &Signature<'db>| { + let self_annotation = signature + .parameters() + .get_positional(0) + .and_then(Parameter::annotated_type) + .filter(|ty| { + ty.as_typevar() + .is_none_or(|bound_typevar| !bound_typevar.typevar(db).is_self(db)) + }); + let return_type = self_annotation.unwrap_or(correct_return_type); + let instance_ty = self_annotation.unwrap_or_else(|| Type::instance(db, self)); + let generic_context = GenericContext::merge_optional( + db, + class_generic_context, + signature.generic_context, + ); + Signature::new_generic( + generic_context, + signature.parameters().clone(), + Some(return_type), + ) + .with_definition(signature.definition()) + .bind_self(db, Some(instance_ty)) }; - if let Some(signature) = signature { - let synthesized_signature = |signature: &Signature<'db>| { - let instance_ty = Type::instance(db, self); - Signature::new(signature.parameters().clone(), Some(correct_return_type)) - .with_definition(signature.definition()) - .bind_self(db, Some(instance_ty)) - }; + let synthesized_dunder_init_signature = CallableSignature::from_overloads( + signature.overloads.iter().map(synthesized_signature), + ); - let synthesized_dunder_init_signature = CallableSignature::from_overloads( - signature.overloads.iter().map(synthesized_signature), - ); - - Some(CallableType::new( - db, - synthesized_dunder_init_signature, - true, - )) - } else { - None - } + Some(CallableType::new( + db, + synthesized_dunder_init_signature, + true, + )) } else { None - }; + } + } else { + None + }; match (dunder_new_function, synthesized_dunder_init_callable) { (Some(dunder_new_function), Some(synthesized_dunder_init_callable)) => { @@ -1261,9 +1287,13 @@ impl<'db> ClassType<'db> { ) .place; - if let Place::Defined(Type::FunctionLiteral(new_function), _, _) = + if let Place::Defined(Type::FunctionLiteral(mut new_function), _, _) = new_function_symbol { + if let Some(class_generic_context) = class_generic_context { + new_function = + new_function.with_inherited_generic_context(db, class_generic_context); + } CallableTypes::one( new_function .into_bound_method_type(db, correct_return_type) @@ -1273,7 +1303,11 @@ impl<'db> ClassType<'db> { // Fallback if no `object.__new__` is found. CallableTypes::one(CallableType::single( db, - Signature::new(Parameters::empty(), Some(correct_return_type)), + Signature::new_generic( + class_generic_context, + Parameters::empty(), + Some(correct_return_type), + ), )) } } diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 0fba3aecd8..7380ec86f3 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1339,6 +1339,8 @@ pub enum KnownFunction { IsSingleValued, /// `ty_extensions.generic_context` GenericContext, + /// `ty_extensions.into_callable` + IntoCallable, /// `ty_extensions.dunder_all_names` DunderAllNames, /// `ty_extensions.enum_members` @@ -1411,6 +1413,7 @@ impl KnownFunction { | Self::IsSingleton | Self::IsSubtypeOf | Self::GenericContext + | Self::IntoCallable | Self::DunderAllNames | Self::EnumMembers | Self::StaticAssert @@ -1946,6 +1949,7 @@ pub(crate) mod tests { KnownFunction::IsSingleton | KnownFunction::IsSubtypeOf | KnownFunction::GenericContext + | KnownFunction::IntoCallable | KnownFunction::DunderAllNames | KnownFunction::EnumMembers | KnownFunction::StaticAssert diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index de357dfbce..432785b778 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -17,11 +17,12 @@ use crate::types::signatures::Parameters; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral, - FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, - KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext, - TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarIdentity, BoundTypeVarInstance, + ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, + IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, + Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, + TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type, + walk_bound_type_var_type, }; use crate::{Db, FxOrderMap, FxOrderSet}; @@ -261,6 +262,34 @@ impl<'db> GenericContext<'db> { ) } + pub(crate) fn merge_optional( + db: &'db dyn Db, + left: Option, + right: Option, + ) -> Option { + match (left, right) { + (None, None) => None, + (Some(one), None) | (None, Some(one)) => Some(one), + (Some(left), Some(right)) => Some(left.merge(db, right)), + } + } + + pub(crate) fn remove_self( + self, + db: &'db dyn Db, + binding_context: Option>, + ) -> Self { + Self::from_typevar_instances( + db, + self.variables(db).filter(|bound_typevar| { + !(bound_typevar.typevar(db).is_self(db) + && binding_context.is_none_or(|binding_context| { + bound_typevar.binding_context(db) == binding_context + })) + }), + ) + } + pub(crate) fn inferable_typevars(self, db: &'db dyn Db) -> InferableTypeVars<'db, 'db> { #[derive(Default)] struct CollectTypeVars<'db> { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index d091b8a53f..c76a086cfa 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -667,10 +667,11 @@ impl<'db> Signature<'db> { let mut parameters = Parameters::new(db, parameters); let mut return_ty = self.return_ty; + let binding_context = self.definition.map(BindingContext::Definition); if let Some(self_type) = self_type { let self_mapping = TypeMapping::BindSelf { self_type, - binding_context: self.definition.map(BindingContext::Definition), + binding_context, }; parameters = parameters.apply_type_mapping_impl( db, @@ -682,7 +683,9 @@ impl<'db> Signature<'db> { .map(|ty| ty.apply_type_mapping(db, &self_mapping, TypeContext::default())); } Self { - generic_context: self.generic_context, + generic_context: self + .generic_context + .map(|generic_context| generic_context.remove_self(db, binding_context)), definition: self.definition, parameters, return_ty, diff --git a/crates/ty_vendored/ty_extensions/ty_extensions.pyi b/crates/ty_vendored/ty_extensions/ty_extensions.pyi index 8f45b29238..347b6b4b34 100644 --- a/crates/ty_vendored/ty_extensions/ty_extensions.pyi +++ b/crates/ty_vendored/ty_extensions/ty_extensions.pyi @@ -147,6 +147,10 @@ def is_single_valued(ty: Any) -> bool: # type is not generic. def generic_context(ty: Any) -> GenericContext | None: ... +# Converts a value into a `Callable`, if possible. This is the value equivalent +# of `CallableTypeOf`, which operates on types. +def into_callable(ty: Any) -> Any: ... + # Returns the `__all__` names of a module as a tuple of sorted strings, or `None` if # either the module does not have `__all__` or it has invalid elements. def dunder_all_names(module: Any) -> Any: ...