Skip to content

Commit 76ec64d

Browse files
Kalmaegisharkdp
andauthored
[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 <[email protected]>
1 parent b7e69ec commit 76ec64d

File tree

3 files changed

+60
-21
lines changed

3 files changed

+60
-21
lines changed

crates/red_knot_python_semantic/resources/mdtest/annotations/any.md

+43-14
Original file line numberDiff line numberDiff line change
@@ -46,42 +46,71 @@ def f():
4646
y: Any = "not an Any" # error: [invalid-assignment]
4747
```
4848

49-
## Subclass
49+
## Subclasses of `Any`
5050

5151
The spec allows you to define subclasses of `Any`.
5252

53-
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
53+
`SubclassOfAny` has an unknown superclass, which might be `int`. The assignment to `x` should not be
5454
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
5555
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
5656

5757
```py
5858
from typing import Any
5959

60-
class Subclass(Any): ...
60+
class SubclassOfAny(Any): ...
6161

62-
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
62+
reveal_type(SubclassOfAny.__mro__) # revealed: tuple[Literal[SubclassOfAny], Any, Literal[object]]
6363

64-
x: Subclass = 1 # error: [invalid-assignment]
65-
y: int = Subclass()
66-
67-
def _(s: Subclass):
68-
reveal_type(s) # revealed: Subclass
64+
x: SubclassOfAny = 1 # error: [invalid-assignment]
65+
y: int = SubclassOfAny()
6966
```
7067

71-
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
72-
be a subclass of `FinalClass`:
68+
`SubclassOfAny` should not be assignable to a final class though, because `SubclassOfAny` could not
69+
possibly be a subclass of `FinalClass`:
7370

7471
```py
7572
from typing import final
7673

7774
@final
7875
class FinalClass: ...
7976

80-
f: FinalClass = Subclass() # error: [invalid-assignment]
77+
f: FinalClass = SubclassOfAny() # error: [invalid-assignment]
78+
79+
@final
80+
class OtherFinalClass: ...
81+
82+
f: FinalClass | OtherFinalClass = SubclassOfAny() # error: [invalid-assignment]
83+
```
84+
85+
A subclass of `Any` can also be assigned to arbitrary `Callable` types:
86+
87+
```py
88+
from typing import Callable, Any
89+
90+
def takes_callable1(f: Callable):
91+
f()
92+
93+
takes_callable1(SubclassOfAny())
94+
95+
def takes_callable2(f: Callable[[int], None]):
96+
f(1)
97+
98+
takes_callable2(SubclassOfAny())
99+
```
100+
101+
A subclass of `Any` cannot be assigned to literal types, since those can not be subclassed:
102+
103+
```py
104+
from typing import Any, Literal
105+
106+
class MockAny(Any):
107+
pass
108+
109+
x: Literal[1] = MockAny() # error: [invalid-assignment]
81110
```
82111

83-
A use case where this comes up is with mocking libraries, where the mock object should be assignable
84-
to any type:
112+
A use case where subclasses of `Any` come up is in mocking libraries, where the mock object should
113+
be assignable to (almost) any type:
85114

86115
```py
87116
from unittest.mock import MagicMock

crates/red_knot_python_semantic/src/types.rs

+6
Original file line numberDiff line numberDiff line change
@@ -1477,6 +1477,12 @@ impl<'db> Type<'db> {
14771477
self_callable.is_assignable_to(db, target_callable)
14781478
}
14791479

1480+
(Type::NominalInstance(instance), Type::Callable(_))
1481+
if instance.class().is_subclass_of_any_or_unknown(db) =>
1482+
{
1483+
true
1484+
}
1485+
14801486
(Type::NominalInstance(_) | Type::ProtocolInstance(_), Type::Callable(_)) => {
14811487
let call_symbol = self.member(db, "__call__").symbol;
14821488
match call_symbol {

crates/red_knot_python_semantic/src/types/class.rs

+11-7
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ impl<'db> ClassType<'db> {
231231
class_literal.is_final(db)
232232
}
233233

234+
/// Is this class a subclass of `Any` or `Unknown`?
235+
pub(crate) fn is_subclass_of_any_or_unknown(self, db: &'db dyn Db) -> bool {
236+
self.iter_mro(db).any(|base| {
237+
matches!(
238+
base,
239+
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
240+
)
241+
})
242+
}
243+
234244
/// If `self` and `other` are generic aliases of the same generic class, returns their
235245
/// corresponding specializations.
236246
fn compatible_specializations(
@@ -310,13 +320,7 @@ impl<'db> ClassType<'db> {
310320
}
311321
}
312322

313-
if self.iter_mro(db).any(|base| {
314-
matches!(
315-
base,
316-
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
317-
)
318-
}) && !other.is_final(db)
319-
{
323+
if self.is_subclass_of_any_or_unknown(db) && !other.is_final(db) {
320324
return true;
321325
}
322326

0 commit comments

Comments
 (0)