mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 18:58:04 +00:00
[red-knot
] Allow subclasses of Any to be assignable to Callable types (#17717)
## Summary Fixes #17701. ## Test plan New Markdown test. --------- Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
parent
b7e69ecbfc
commit
76ec64d535
3 changed files with 60 additions and 21 deletions
|
@ -46,30 +46,27 @@ def f():
|
|||
y: Any = "not an Any" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Subclass
|
||||
## Subclasses of `Any`
|
||||
|
||||
The spec allows you to define subclasses of `Any`.
|
||||
|
||||
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
|
||||
`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be
|
||||
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
|
||||
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Subclass(Any): ...
|
||||
class SubclassOfAny(Any): ...
|
||||
|
||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
|
||||
|
||||
x: Subclass = 1 # error: [invalid-assignment]
|
||||
y: int = Subclass()
|
||||
|
||||
def _(s: Subclass):
|
||||
reveal_type(s) # revealed: Subclass
|
||||
x: SubclassOfAny = 1 # error: [invalid-assignment]
|
||||
y: int = SubclassOfAny()
|
||||
```
|
||||
|
||||
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
|
||||
be a subclass of `FinalClass`:
|
||||
`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not
|
||||
possibly be a subclass of `FinalClass`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
@ -77,11 +74,43 @@ from typing import final
|
|||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
f: FinalClass = Subclass() # error: [invalid-assignment]
|
||||
f: FinalClass = SubclassOfAny() # error: [invalid-assignment]
|
||||
|
||||
@final
|
||||
class OtherFinalClass: ...
|
||||
|
||||
f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
A use case where this comes up is with mocking libraries, where the mock object should be assignable
|
||||
to any type:
|
||||
A subclass of `Any` can also be assigned to arbitrary `Callable` types:
|
||||
|
||||
```py
|
||||
from typing import Callable, Any
|
||||
|
||||
def takes_callable1(f: Callable):
|
||||
f()
|
||||
|
||||
takes_callable1(SubclassOfAny())
|
||||
|
||||
def takes_callable2(f: Callable[[int], None]):
|
||||
f(1)
|
||||
|
||||
takes_callable2(SubclassOfAny())
|
||||
```
|
||||
|
||||
A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:
|
||||
|
||||
```py
|
||||
from typing import Any, Literal
|
||||
|
||||
class MockAny(Any):
|
||||
pass
|
||||
|
||||
x: Literal[1] = MockAny() # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should
|
||||
be assignable to (almost) any type:
|
||||
|
||||
```py
|
||||
from unittest.mock import MagicMock
|
||||
|
|
|
@ -1477,6 +1477,12 @@ impl<'db> Type<'db> {
|
|||
self_callable.is_assignable_to(db, target_callable)
|
||||
}
|
||||
|
||||
(Type::NominalInstance(instance), Type::Callable(_))
|
||||
if instance.class().is_subclass_of_any_or_unknown(db) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
|
||||
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
|
||||
let call_symbol = self.member(db, "__call__").symbol;
|
||||
match call_symbol {
|
||||
|
|
|
@ -231,6 +231,16 @@ impl<'db> ClassType<'db> {
|
|||
class_literal.is_final(db)
|
||||
}
|
||||
|
||||
/// Is this class a subclass of `Any` or `Unknown`?
|
||||
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
|
||||
self.iter_mro(db).any(|base| {
|
||||
matches!(
|
||||
base,
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// If `self` and `other` are generic aliases of the same generic class, returns their
|
||||
/// corresponding specializations.
|
||||
fn compatible_specializations(
|
||||
|
@ -310,13 +320,7 @@ impl<'db> ClassType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
if self.iter_mro(db).any(|base| {
|
||||
matches!(
|
||||
base,
|
||||
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
|
||||
)
|
||||
}) && !other.is_final(db)
|
||||
{
|
||||
if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue