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()
|
||||
```
|
||||
|
||||
## `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
|
||||
|
|
|
|||
|
|
@ -11018,11 +11018,19 @@ impl<'db> CallableType<'db> {
|
|||
db: &'db dyn Db,
|
||||
self_type: Option<Type<'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> {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@ impl<'db> ProtocolInterface<'db> {
|
|||
.and(db, || {
|
||||
our_type.has_relation_to_impl(
|
||||
db,
|
||||
Type::Callable(other_type.bind_self(db, None)),
|
||||
Type::Callable(protocol_bind_self(db, other_type, None)),
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
|
|
@ -313,7 +313,7 @@ impl<'db> ProtocolInterface<'db> {
|
|||
ProtocolMemberKind::Method(other_method),
|
||||
) => our_method.bind_self(db, None).has_relation_to_impl(
|
||||
db,
|
||||
other_method.bind_self(db, None),
|
||||
protocol_bind_self(db, other_method, None),
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
|
|
@ -712,7 +712,7 @@ impl<'a, 'db> ProtocolMember<'a, 'db> {
|
|||
.map(|callable| callable.apply_self(db, fallback_other))
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
method.bind_self(db, Some(fallback_other)),
|
||||
protocol_bind_self(db, *method, Some(fallback_other)),
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
|
|
@ -912,3 +912,16 @@ fn proto_interface_cycle_initial<'db>(
|
|||
) -> ProtocolInterface<'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