Skip to content

Commit e8d4f6d

Browse files
authored
[ty] Ensure that a function-literal type is always equivalent to itself (astral-sh#18227)
1 parent 60b486a commit e8d4f6d

File tree

3 files changed

+38
-10
lines changed

3 files changed

+38
-10
lines changed

crates/ty_python_semantic/resources/mdtest/type_properties/is_equivalent_to.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,30 @@ static_assert(is_equivalent_to(CallableTypeOf[pg], CallableTypeOf[cpg]))
334334
static_assert(is_equivalent_to(CallableTypeOf[cpg], CallableTypeOf[pg]))
335335
```
336336

337+
## Function-literal types and bound-method types
338+
339+
Function-literal types and bound-method types are always considered self-equivalent, even if they
340+
have unannotated parameters, or parameters with not-fully-static annotations.
341+
342+
```toml
343+
[environment]
344+
python-version = "3.12"
345+
```
346+
347+
```py
348+
from ty_extensions import is_equivalent_to, TypeOf, static_assert
349+
350+
def f(): ...
351+
352+
static_assert(is_equivalent_to(TypeOf[f], TypeOf[f]))
353+
354+
class A:
355+
def method(self) -> int:
356+
return 42
357+
358+
static_assert(is_equivalent_to(TypeOf[A.method], TypeOf[A.method]))
359+
type X = TypeOf[A.method]
360+
static_assert(is_equivalent_to(X, X))
361+
```
362+
337363
[the equivalence relation]: https://typing.python.org/en/latest/spec/glossary.html#term-equivalent

crates/ty_python_semantic/src/types.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7146,10 +7146,11 @@ impl<'db> FunctionType<'db> {
71467146
// However, our representation of a function literal includes any specialization that
71477147
// should be applied to the signature. Different specializations of the same function
71487148
// literal are only subtypes of each other if they result in subtype signatures.
7149-
self.body_scope(db) == other.body_scope(db)
7150-
&& self
7151-
.into_callable_type(db)
7152-
.is_subtype_of(db, other.into_callable_type(db))
7149+
self.normalized(db) == other.normalized(db)
7150+
|| (self.body_scope(db) == other.body_scope(db)
7151+
&& self
7152+
.into_callable_type(db)
7153+
.is_subtype_of(db, other.into_callable_type(db)))
71537154
}
71547155

71557156
fn is_assignable_to(self, db: &'db dyn Db, other: Self) -> bool {
@@ -7164,10 +7165,11 @@ impl<'db> FunctionType<'db> {
71647165
}
71657166

71667167
fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
7167-
self.body_scope(db) == other.body_scope(db)
7168-
&& self
7169-
.into_callable_type(db)
7170-
.is_equivalent_to(db, other.into_callable_type(db))
7168+
self.normalized(db) == other.normalized(db)
7169+
|| (self.body_scope(db) == other.body_scope(db)
7170+
&& self
7171+
.into_callable_type(db)
7172+
.is_equivalent_to(db, other.into_callable_type(db)))
71717173
}
71727174

71737175
fn is_gradual_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {

crates/ty_python_semantic/src/types/signatures.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,8 @@ impl<'db> Signature<'db> {
302302

303303
pub(crate) fn normalized(&self, db: &'db dyn Db) -> Self {
304304
Self {
305-
generic_context: self.generic_context,
306-
inherited_generic_context: self.inherited_generic_context,
305+
generic_context: self.generic_context.map(|ctx| ctx.normalized(db)),
306+
inherited_generic_context: self.inherited_generic_context.map(|ctx| ctx.normalized(db)),
307307
parameters: self
308308
.parameters
309309
.iter()

0 commit comments

Comments
 (0)