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 e0de156eb0bdd..731b4de2fd942 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/any.md @@ -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 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 this comes up is with mocking libraries, where the mock object should be assignable -to any type: +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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index f4d12a06f540a..f60b03bdb9a78 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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 { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 912d3946ac2c6..e79ea4f9b24c9 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -195,6 +195,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( @@ -274,13 +284,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; }