[ty] Ensure that a function-literal type is always equivalent to itself (#18227)

This commit is contained in:
Alex Waygood 2025-05-20 14:11:03 -04:00 committed by GitHub
parent 60b486abce
commit e8d4f6d891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 38 additions and 10 deletions

View file

@ -334,4 +334,30 @@ static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg]))
static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg]))
```
## Function-literal types and bound-method types
Function-literal types and bound-method types are always considered self-equivalent, even if they
have unannotated parameters, or parameters with not-fully-static annotations.
```toml
[environment]
python-version = "3.12"
```
```py
from ty_extensions import is_equivalent_to, TypeOf, static_assert
def f(): ...
static_assert(is_equivalent_to(TypeOf[f], TypeOf[f]))
class A:
def method(self) -> int:
return 42
static_assert(is_equivalent_to(TypeOf[A.method], TypeOf[A.method]))
type X = TypeOf[A.method]
static_assert(is_equivalent_to(X, X))
```
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent

View file

@ -7146,10 +7146,11 @@ impl<'db> FunctionType<'db> {
// However, our representation of a function literal includes any specialization that
// should be applied to the signature. Different specializations of the same function
// literal are only subtypes of each other if they result in subtype signatures.
self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_subtype_of(db, other.into_callable_type(db))
self.normalized(db) == other.normalized(db)
|| (self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_subtype_of(db, other.into_callable_type(db)))
}
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
@ -7164,10 +7165,11 @@ impl<'db> FunctionType<'db> {
}
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_equivalent_to(db, other.into_callable_type(db))
self.normalized(db) == other.normalized(db)
|| (self.body_scope(db) == other.body_scope(db)
&& self
.into_callable_type(db)
.is_equivalent_to(db, other.into_callable_type(db)))
}
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {

View file

@ -302,8 +302,8 @@ impl<'db> Signature<'db> {
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
Self {
generic_context: self.generic_context,
inherited_generic_context: self.inherited_generic_context,
generic_context: self.generic_context.map(|ctx| ctx.normalized(db)),
inherited_generic_context: self.inherited_generic_context.map(|ctx| ctx.normalized(db)),
parameters: self
.parameters
.iter()