From b1b8ca3bcd9fde92cd2e2ab553b557d4e73d2154 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 23 Apr 2025 10:39:10 +0200 Subject: [PATCH] [red-knot] GenericAlias instances as a base class (#17575) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary We currently emit a diagnostic for code like the following: ```py from typing import Any # error: Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`) class C(tuple[Any, ...]): ... ``` The changeset here silences this diagnostic by recognizing instances of `GenericAlias` in `ClassBase::try_from_type`, and inferring a `@Todo` type for them. This is a change in preparation for #17557, because `C` previously had `Unknown` in its MRO … ```py reveal_type(C.__mro__) # tuple[Literal[C], Unknown, Literal[object]] ``` … which would cause us to think that `C` is assignable to everything. The changeset also removes some false positive `invalid-base` diagnostics across the ecosystem. ## Test Plan Updated Markdown tests. --- .../resources/mdtest/annotations/stdlib_typing_aliases.md | 6 +++--- .../resources/mdtest/generics/classes.md | 2 -- .../resources/mdtest/subscript/tuple.md | 4 +--- .../resources/mdtest/type_of/basic.md | 4 +--- crates/red_knot_python_semantic/src/types/class_base.rs | 3 +++ 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 7426e4c887..4c54139cc4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -106,13 +106,13 @@ reveal_type(ChainMapSubclass.__mro__) class CounterSubclass(typing.Counter): ... # TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[CounterSubclass], Literal[Counter], Unknown, Literal[object]] +# revealed: tuple[Literal[CounterSubclass], Literal[Counter], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]] reveal_type(CounterSubclass.__mro__) class DefaultDictSubclass(typing.DefaultDict): ... # TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], Unknown, Literal[object]] +# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict], @Todo(GenericAlias instance), Literal[object]] reveal_type(DefaultDictSubclass.__mro__) class DequeSubclass(typing.Deque): ... @@ -124,6 +124,6 @@ reveal_type(DequeSubclass.__mro__) class OrderedDictSubclass(typing.OrderedDict): ... # TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object) -# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], Unknown, Literal[object]] +# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict], @Todo(GenericAlias instance), Literal[object]] reveal_type(OrderedDictSubclass.__mro__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md index 6359978660..295b44827d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md @@ -326,8 +326,6 @@ class Sub(Base[Sub]): ... ## Another cyclic case ```pyi -# TODO no error (generics) -# error: [invalid-base] class Derived[T](list[Derived[T]]): ... ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 005e3da1ea..04b39d1f50 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -81,13 +81,11 @@ python-version = "3.9" ``` ```py -# TODO: `tuple[int, str]` is a valid base (generics) -# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)" class A(tuple[int, str]): ... # Runtime value: `(A, tuple, object)` # TODO: Generics -reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]] +reveal_type(A.__mro__) # revealed: tuple[Literal[A], @Todo(GenericAlias instance), Literal[object]] ``` ## `typing.Tuple` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md index 1e5f4bdc5b..fc7f9f450b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_of/basic.md @@ -145,12 +145,10 @@ _: type[A, B] ## As a base class ```py -# TODO: this is a false positive -# error: [invalid-base] "Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)" class Foo(type[int]): ... # TODO: should be `tuple[Literal[Foo], Literal[type], Literal[object]] -reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]] +reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], @Todo(GenericAlias instance), Literal[object]] ``` ## `@final` classes diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 6142cd322f..fa5175a0ab 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -78,6 +78,9 @@ impl<'db> ClassBase<'db> { Self::Class(literal.default_specialization(db)) }), Type::GenericAlias(generic) => Some(Self::Class(ClassType::Generic(generic))), + Type::Instance(instance) if instance.class().is_known(db, KnownClass::GenericAlias) => { + Self::try_from_type(db, todo_type!("GenericAlias instance")) + } Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs? Type::Intersection(_) => None, // TODO -- probably incorrect? Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?