[red-knot] Assignability for subclasses of Any and Unknown (#17557)

## Summary

Allow (instances of) subclasses of `Any` and `Unknown` to be assignable
to (instances of) other classes, unless they are final. This allows us
to get rid of ~1000 false positives, mostly when mock-objects like
`unittest.mock.MagicMock` are assigned to various targets.

## Test Plan

Adapted and new Markdown tests.
This commit is contained in:
David Peter 2025-04-23 11:37:30 +02:00 committed by GitHub
parent a241321735
commit 99fa850e53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 43 additions and 8 deletions

View file

@ -50,10 +50,9 @@ y: Any = "not an Any" # error: [invalid-assignment]
The spec allows you to define subclasses of `Any`.
TODO: Handle assignments correctly. `Subclass` 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`.
`Subclass` 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
@ -63,13 +62,33 @@ class Subclass(Any): ...
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
x: Subclass = 1 # error: [invalid-assignment]
# TODO: no diagnostic
y: int = Subclass() # error: [invalid-assignment]
y: int = Subclass()
def _(s: Subclass):
reveal_type(s) # revealed: Subclass
```
`Subclass` should not be assignable to a final class though, because `Subclass` could not possibly
be a subclass of `FinalClass`:
```py
from typing import final
@final
class FinalClass: ...
f: FinalClass = Subclass() # error: [invalid-assignment]
```
A use case where this comes up is with mocking libraries, where the mock object should be assignable
to any type:
```py
from unittest.mock import MagicMock
x: int = MagicMock()
```
## Invalid
`Any` cannot be parameterized:

View file

@ -22,6 +22,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example:
`/typeshed/stdlib/builtins.pyi`:
```pyi
class object: ...
class BuiltinClass: ...
builtin_symbol: BuiltinClass

View file

@ -47,6 +47,13 @@ static_assert(is_assignable_to(Unknown, Literal[1]))
static_assert(is_assignable_to(Any, Literal[1]))
static_assert(is_assignable_to(Literal[1], Unknown))
static_assert(is_assignable_to(Literal[1], Any))
class SubtypeOfAny(Any): ...
static_assert(is_assignable_to(SubtypeOfAny, Any))
static_assert(is_assignable_to(SubtypeOfAny, int))
static_assert(is_assignable_to(Any, SubtypeOfAny))
static_assert(not is_assignable_to(int, SubtypeOfAny))
```
## Literal types

View file

@ -324,8 +324,6 @@ impl<'db> ClassType<'db> {
}
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
// `is_subclass_of` is checking the subtype relation, in which gradual types do not
// participate, so we should not return `True` if we find `Any/Unknown` in the MRO.
if self.is_subclass_of(db, other) {
return true;
}
@ -341,6 +339,16 @@ impl<'db> ClassType<'db> {
}
}
if self.iter_mro(db).any(|base| {
matches!(
base,
ClassBase::Dynamic(DynamicType::Any | DynamicType::Unknown)
)
}) && !other.is_final(db)
{
return true;
}
false
}