From 99fa850e5362d957f2249224dcecccf984faabee Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 11:37:30 +0200 Subject: [PATCH] [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. --- .../resources/mdtest/annotations/any.md | 31 +++++++++++++++---- .../mdtest/mdtest_custom_typeshed.md | 1 + .../type_properties/is_assignable_to.md | 7 +++++ .../src/types/class.rs | 12 +++++-- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md index b5b43a8412..e0de156eb0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md b/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md index e4255708fe..7c4a0abc3b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md +++ b/crates/red_knot_python_semantic/resources/mdtest/mdtest_custom_typeshed.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index fdd7ddbe7c..9c33827733 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 8eaa0c7988..f35c6a3eb2 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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 }