[red-knot] GenericAlias instances as a base class (#17575)

## 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.
This commit is contained in:
David Peter 2025-04-23 10:39:10 +02:00 committed by GitHub
parent 3fae176345
commit b1b8ca3bcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 8 additions and 11 deletions

View file

@ -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__)
```

View file

@ -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]]): ...
```

View file

@ -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`

View file

@ -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

View file

@ -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__`?