mirror of https://github.com/astral-sh/ruff
[ty] Retain the function-like-ness of `Callable` types when binding `self` (#21614)
## Summary
For something like this:
```py
from typing import Callable
def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
return fn
class MyClass:
@my_lossy_decorator
def method(self) -> int:
return 42
```
we will currently infer the type of `MyClass.method` as a function-like
`Callable`, but we will infer the type of `MyClass().method` as a
`Callable` that is _not_ function-like. That's because a `CallableType`
currently "forgets" whether it was function-like or not during the
`bound_self` transformation:
a57e291311/crates/ty_python_semantic/src/types.rs (L10985-L10987)
This seems incorrect, and it's quite different to what we do when
binding the `self` parameter of `FunctionLiteral` types: `BoundMethod`
types are all seen as subtypes of function-like `Callable` supertypes --
here's `BoundMethodType::into_callable_type`:
a57e291311/crates/ty_python_semantic/src/types.rs (L10844-L10860)
The bug here is also causing lots of false positives in the ecosystem
report on https://github.com/astral-sh/ruff/pull/21611: a decorated
method on a subclass is currently not seen as validly overriding an
undecorated method with the same signature on a superclass, because the
undecorated superclass method is seen as function-like after binding
`self` whereas the decorated subclass method is not.
Fixing the bug required adding a new API in `protocol_class.rs`, because
it turns out that for our purposes in protocol subtyping/assignability,
we really do want a callable type to forget its function-like-ness when
binding `self`.
I initially tried out this change without changing anything in
`protocol_class.rs`. However, it resulted in many ecosystem false
positives and new false positives on the typing conformance test suite.
This is because it would mean that no protocol with a `__call__` method
would ever be seen as a subtype of a `Callable` type, since the
`__call__` method on the protocol would be seen as being function-like
whereas the `Callable` type would not be seen as function-like.
## Test Plan
Added an mdtest that fails on `main`
This commit is contained in:
parent
7e277667d1
commit
bab688b76c
|
|
@ -237,4 +237,26 @@ class Matrix:
|
||||||
Matrix() < Matrix()
|
Matrix() < Matrix()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `self`-binding behaviour of function-like `Callable`s
|
||||||
|
|
||||||
|
Binding the `self` parameter of a function-like `Callable` creates a new `Callable` that is also
|
||||||
|
function-like:
|
||||||
|
|
||||||
|
`main.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
def my_lossy_decorator(fn: Callable[..., int]) -> Callable[..., int]:
|
||||||
|
return fn
|
||||||
|
|
||||||
|
class MyClass:
|
||||||
|
@my_lossy_decorator
|
||||||
|
def method(self) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
reveal_type(MyClass().method) # revealed: (...) -> int
|
||||||
|
reveal_type(MyClass().method.__name__) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
[`tensorbase`]: https://github.com/pytorch/pytorch/blob/f3913ea641d871f04fa2b6588a77f63efeeb9f10/torch/_tensor.py#L1084-L1092
|
||||||
|
|
|
||||||
|
|
@ -11018,11 +11018,19 @@ impl<'db> CallableType<'db> {
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
self_type: Option<Type<'db>>,
|
self_type: Option<Type<'db>>,
|
||||||
) -> CallableType<'db> {
|
) -> CallableType<'db> {
|
||||||
CallableType::new(db, self.signatures(db).bind_self(db, self_type), false)
|
CallableType::new(
|
||||||
|
db,
|
||||||
|
self.signatures(db).bind_self(db, self_type),
|
||||||
|
self.is_function_like(db),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> {
|
pub(crate) fn apply_self(self, db: &'db dyn Db, self_type: Type<'db>) -> CallableType<'db> {
|
||||||
CallableType::new(db, self.signatures(db).apply_self(db, self_type), false)
|
CallableType::new(
|
||||||
|
db,
|
||||||
|
self.signatures(db).apply_self(db, self_type),
|
||||||
|
self.is_function_like(db),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a callable type which represents a fully-static "bottom" callable.
|
/// Create a callable type which represents a fully-static "bottom" callable.
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,7 @@ impl<'db> ProtocolInterface<'db> {
|
||||||
.and(db, || {
|
.and(db, || {
|
||||||
our_type.has_relation_to_impl(
|
our_type.has_relation_to_impl(
|
||||||
db,
|
db,
|
||||||
Type::Callable(other_type.bind_self(db, None)),
|
Type::Callable(protocol_bind_self(db, other_type, None)),
|
||||||
inferable,
|
inferable,
|
||||||
relation,
|
relation,
|
||||||
relation_visitor,
|
relation_visitor,
|
||||||
|
|
@ -313,7 +313,7 @@ impl<'db> ProtocolInterface<'db> {
|
||||||
ProtocolMemberKind::Method(other_method),
|
ProtocolMemberKind::Method(other_method),
|
||||||
) => our_method.bind_self(db, None).has_relation_to_impl(
|
) => our_method.bind_self(db, None).has_relation_to_impl(
|
||||||
db,
|
db,
|
||||||
other_method.bind_self(db, None),
|
protocol_bind_self(db, other_method, None),
|
||||||
inferable,
|
inferable,
|
||||||
relation,
|
relation,
|
||||||
relation_visitor,
|
relation_visitor,
|
||||||
|
|
@ -712,7 +712,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
|
||||||
.map(|callable| callable.apply_self(db, fallback_other))
|
.map(|callable| callable.apply_self(db, fallback_other))
|
||||||
.has_relation_to_impl(
|
.has_relation_to_impl(
|
||||||
db,
|
db,
|
||||||
method.bind_self(db, Some(fallback_other)),
|
protocol_bind_self(db, *method, Some(fallback_other)),
|
||||||
inferable,
|
inferable,
|
||||||
relation,
|
relation,
|
||||||
relation_visitor,
|
relation_visitor,
|
||||||
|
|
@ -912,3 +912,16 @@ fn proto_interface_cycle_initial<'db>(
|
||||||
) -> ProtocolInterface<'db> {
|
) -> ProtocolInterface<'db> {
|
||||||
ProtocolInterface::empty(db)
|
ProtocolInterface::empty(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bind `self`, and *also* discard the functionlike-ness of the callable.
|
||||||
|
///
|
||||||
|
/// This additional upcasting is required in order for protocols with `__call__` method
|
||||||
|
/// members to be considered assignable to `Callable` types, since the `Callable` supertype
|
||||||
|
/// of the `__call__` method will be function-like but a `Callable` type is not.
|
||||||
|
fn protocol_bind_self<'db>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
callable: CallableType<'db>,
|
||||||
|
self_type: Option<Type<'db>>,
|
||||||
|
) -> CallableType<'db> {
|
||||||
|
CallableType::new(db, callable.signatures(db).bind_self(db, self_type), false)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue