[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:
Hans 2025-05-01 16:18:12 +08:00 committed by GitHub
parent b7e69ecbfc
commit 76ec64d535
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 60 additions and 21 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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;
}