mirror of https://github.com/astral-sh/ruff
[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:
parent
a241321735
commit
99fa850e53
|
|
@ -50,10 +50,9 @@ y: Any = "not an Any" # error: [invalid-assignment]
|
||||||
|
|
||||||
The spec allows you to define subclasses of `Any`.
|
The spec allows you to define subclasses of `Any`.
|
||||||
|
|
||||||
TODO: Handle assignments correctly. `Subclass` has an unknown superclass, which might be `int`. The
|
`Subclass` has an unknown superclass, which might be `int`. The assignment to `x` should not be
|
||||||
assignment to `x` should not be allowed, even when the unknown superclass is `int`. The assignment
|
allowed, even when the unknown superclass is `int`. The assignment to `y` should be allowed, since
|
||||||
to `y` should be allowed, since `Subclass` might have `int` as a superclass, and is therefore
|
`Subclass` might have `int` as a superclass, and is therefore assignable to `int`.
|
||||||
assignable to `int`.
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -63,13 +62,33 @@ class Subclass(Any): ...
|
||||||
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
reveal_type(Subclass.__mro__) # revealed: tuple[Literal[Subclass], Any, Literal[object]]
|
||||||
|
|
||||||
x: Subclass = 1 # error: [invalid-assignment]
|
x: Subclass = 1 # error: [invalid-assignment]
|
||||||
# TODO: no diagnostic
|
y: int = Subclass()
|
||||||
y: int = Subclass() # error: [invalid-assignment]
|
|
||||||
|
|
||||||
def _(s: Subclass):
|
def _(s: Subclass):
|
||||||
reveal_type(s) # revealed: 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
|
## Invalid
|
||||||
|
|
||||||
`Any` cannot be parameterized:
|
`Any` cannot be parameterized:
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ We can then place custom stub files in `/typeshed/stdlib`, for example:
|
||||||
`/typeshed/stdlib/builtins.pyi`:
|
`/typeshed/stdlib/builtins.pyi`:
|
||||||
|
|
||||||
```pyi
|
```pyi
|
||||||
|
class object: ...
|
||||||
class BuiltinClass: ...
|
class BuiltinClass: ...
|
||||||
|
|
||||||
builtin_symbol: BuiltinClass
|
builtin_symbol: BuiltinClass
|
||||||
|
|
|
||||||
|
|
@ -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(Any, Literal[1]))
|
||||||
static_assert(is_assignable_to(Literal[1], Unknown))
|
static_assert(is_assignable_to(Literal[1], Unknown))
|
||||||
static_assert(is_assignable_to(Literal[1], Any))
|
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
|
## Literal types
|
||||||
|
|
|
||||||
|
|
@ -324,8 +324,6 @@ impl<'db> ClassType<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_assignable_to(self, db: &'db dyn Db, other: ClassType<'db>) -> bool {
|
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) {
|
if self.is_subclass_of(db, other) {
|
||||||
return true;
|
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
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue