[red-knot] Legacy generic classes (#17721)

This adds support for legacy generic classes, which use a
`typing.Generic` base class, or which inherit from another generic class
that has been specialized with legacy typevars.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-05-02 20:34:20 -04:00 committed by GitHub
parent f7cae4ffb5
commit 96697c98f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1547 additions and 279 deletions

View file

@ -20,7 +20,7 @@ def f(
frozen_set_bare: typing.FrozenSet,
frozen_set_parametrized: typing.FrozenSet[str],
chain_map_bare: typing.ChainMap,
chain_map_parametrized: typing.ChainMap[int],
chain_map_parametrized: typing.ChainMap[str, int],
counter_bare: typing.Counter,
counter_parametrized: typing.Counter[int],
default_dict_bare: typing.DefaultDict,
@ -30,32 +30,45 @@ def f(
ordered_dict_bare: typing.OrderedDict,
ordered_dict_parametrized: typing.OrderedDict[int, str],
):
# TODO: revealed: list[Unknown]
reveal_type(list_bare) # revealed: list
# TODO: revealed: list[int]
reveal_type(list_parametrized) # revealed: list
reveal_type(dict_bare) # revealed: dict
reveal_type(dict_parametrized) # revealed: dict
reveal_type(dict_bare) # revealed: dict[Unknown, Unknown]
# TODO: revealed: dict[int, str]
reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown]
# TODO: revealed: set[Unknown]
reveal_type(set_bare) # revealed: set
# TODO: revealed: set[int]
reveal_type(set_parametrized) # revealed: set
# TODO: revealed: frozenset[Unknown]
reveal_type(frozen_set_bare) # revealed: frozenset
# TODO: revealed: frozenset[str]
reveal_type(frozen_set_parametrized) # revealed: frozenset
reveal_type(chain_map_bare) # revealed: ChainMap
reveal_type(chain_map_parametrized) # revealed: ChainMap
reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown]
# TODO: revealed: ChainMap[str, int]
reveal_type(chain_map_parametrized) # revealed: ChainMap[Unknown, Unknown]
reveal_type(counter_bare) # revealed: Counter
reveal_type(counter_parametrized) # revealed: Counter
reveal_type(counter_bare) # revealed: Counter[Unknown]
# TODO: revealed: Counter[int]
reveal_type(counter_parametrized) # revealed: Counter[Unknown]
reveal_type(default_dict_bare) # revealed: defaultdict
reveal_type(default_dict_parametrized) # revealed: defaultdict
reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown]
# TODO: revealed: defaultdict[str, int]
reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown]
# TODO: revealed: deque[Unknown]
reveal_type(deque_bare) # revealed: deque
# TODO: revealed: deque[str]
reveal_type(deque_parametrized) # revealed: deque
reveal_type(ordered_dict_bare) # revealed: OrderedDict
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict
reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown]
# TODO: revealed: OrderedDict[int, str]
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[Unknown, Unknown]
```
## Inheritance
@ -72,19 +85,19 @@ import typing
class ListSubclass(typing.List): ...
# TODO: generic protocols
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[ListSubclass], Literal[list], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: generic protocols
# revealed: tuple[Literal[DictSubclass], Literal[dict], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
# TODO: generic protocols
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[SetSubclass], Literal[set], Literal[MutableSet], Literal[AbstractSet], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
reveal_type(SetSubclass.__mro__)
class FrozenSetSubclass(typing.FrozenSet): ...
@ -100,30 +113,30 @@ reveal_type(FrozenSetSubclass.__mro__)
class ChainMapSubclass(typing.ChainMap): ...
# TODO: generic protocols
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap], Literal[MutableMapping], Literal[Mapping], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
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], @Todo(GenericAlias instance), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[_T, int]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], 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], @Todo(GenericAlias instance), Literal[object]]
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
# TODO: generic protocols
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[DequeSubclass], Literal[deque], Literal[MutableSequence], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
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], @Todo(GenericAlias instance), Literal[object]]
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View file

@ -25,10 +25,7 @@ def h() -> TypeIs[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
# TODO: should understand the annotation
reveal_type(args) # revealed: tuple
# TODO: should understand the annotation
reveal_type(kwargs) # revealed: dict
reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
return callback(42, *args, **kwargs)
class Foo:

View file

@ -1677,7 +1677,7 @@ functions are instances of that class:
def f(): ...
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
reveal_type(f.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
reveal_type(f.__kwdefaults__) # revealed: dict[str, Any] | None
```
Some attributes are special-cased, however:
@ -1944,7 +1944,8 @@ reveal_type(C.a_float) # revealed: int | float
reveal_type(C.a_complex) # revealed: int | float | complex
reveal_type(C.a_tuple) # revealed: tuple[int]
reveal_type(C.a_range) # revealed: range
reveal_type(C.a_slice) # revealed: slice
# TODO: revealed: slice[Any, Literal[1], Any]
reveal_type(C.a_slice) # revealed: slice[Any, _StartT_co, _StartT_co | _StopT_co]
reveal_type(C.a_type) # revealed: type
reveal_type(C.a_none) # revealed: None
```

View file

@ -94,7 +94,7 @@ function object. We model this explicitly, which means that we can access `__kwd
methods, even though it is not available on `types.MethodType`:
```py
reveal_type(bound_method.__kwdefaults__) # revealed: @Todo(specialized non-generic class) | None
reveal_type(bound_method.__kwdefaults__) # revealed: dict[str, Any] | None
```
## Basic method calls on class objects and instances

View file

@ -145,10 +145,10 @@ def f(x: int) -> int:
return x**2
# TODO: Should be `_lru_cache_wrapper[int]`
reveal_type(f) # revealed: @Todo(specialized non-generic class)
reveal_type(f) # revealed: _lru_cache_wrapper[_T]
# TODO: Should be `int`
reveal_type(f(1)) # revealed: @Todo(specialized non-generic class)
reveal_type(f(1)) # revealed: Unknown
```
## Lambdas as decorators

View file

@ -61,7 +61,7 @@ from knot_extensions import Unknown
def f(x: Any, y: Unknown, z: Any | str | int):
a = cast(dict[str, Any], x)
reveal_type(a) # revealed: @Todo(specialized non-generic class)
reveal_type(a) # revealed: dict[str, Any]
b = cast(Any, y)
reveal_type(b) # revealed: Any

View file

@ -13,8 +13,7 @@ python-version = "3.11"
try:
help()
except* BaseException as e:
# TODO: should be `BaseExceptionGroup[BaseException]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
```
## `except*` with specific exception
@ -25,7 +24,7 @@ try:
except* OSError as e:
# TODO: more precise would be `ExceptionGroup[OSError]` --Alex
# (needs homogeneous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
```
## `except*` with multiple exceptions
@ -36,7 +35,7 @@ try:
except* (TypeError, AttributeError) as e:
# TODO: more precise would be `ExceptionGroup[TypeError | AttributeError]` --Alex
# (needs homogeneous tuples + generics)
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
```
## `except*` with mix of `Exception`s and `BaseException`s
@ -46,7 +45,7 @@ try:
help()
except* (KeyboardInterrupt, AttributeError) as e:
# TODO: more precise would be `BaseExceptionGroup[KeyboardInterrupt | AttributeError]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
```
## Invalid `except*` handlers
@ -55,12 +54,10 @@ except* (KeyboardInterrupt, AttributeError) as e:
try:
help()
except* 3 as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
try:
help()
except* (AttributeError, 42) as e: # error: [invalid-exception-caught]
# TODO: Should be `BaseExceptionGroup[AttributeError | Unknown]` --Alex
reveal_type(e) # revealed: BaseExceptionGroup
reveal_type(e) # revealed: BaseExceptionGroup[BaseException]
```

View file

@ -83,11 +83,10 @@ Using a variadic parameter:
lambda *args: reveal_type(args) # revealed: tuple
```
Using a keyword-varidic parameter:
Using a keyword-variadic parameter:
```py
# TODO: should be `dict[str, Unknown]` (needs generics)
lambda **kwargs: reveal_type(kwargs) # revealed: dict
lambda **kwargs: reveal_type(kwargs) # revealed: dict[str, Unknown]
```
## Nested `lambda` expressions

View file

@ -25,12 +25,9 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5,
reveal_type(f) # revealed: Literal[4]
reveal_type(g) # revealed: Unknown | Literal[5]
reveal_type(h) # revealed: Literal[6]
# TODO: should be `tuple[object, ...]` (needs generics)
reveal_type(args) # revealed: tuple
# TODO: should be `dict[str, str]` (needs generics)
reveal_type(kwargs) # revealed: dict
reveal_type(kwargs) # revealed: dict[str, str]
```
## Unannotated variadic parameters
@ -41,9 +38,7 @@ def f(a, b: int, c=1, d: int = 2, /, e=3, f: Literal[4] = 4, *args: object, g=5,
def g(*args, **kwargs):
# TODO: should be `tuple[Unknown, ...]` (needs generics)
reveal_type(args) # revealed: tuple
# TODO: should be `dict[str, Unknown]` (needs generics)
reveal_type(kwargs) # revealed: dict
reveal_type(kwargs) # revealed: dict[str, Unknown]
```
## Annotation is present but not a fully static type

View file

@ -0,0 +1,35 @@
# Generic builtins
## Variadic keyword arguments with a custom `dict`
When we define `dict` in a custom typeshed, we must take care to define it as a generic class in the
same way as in the real typeshed.
```toml
[environment]
typeshed = "/typeshed"
```
`/typeshed/stdlib/builtins.pyi`:
```pyi
class object: ...
class int: ...
class dict[K, V, Extra]: ...
```
`/typeshed/stdlib/typing_extensions.pyi`:
```pyi
def reveal_type(obj, /): ...
```
If we don't, then we won't be able to infer the types of variadic keyword arguments correctly.
```py
def f(**kwargs):
reveal_type(kwargs) # revealed: Unknown
def g(**kwargs: int):
reveal_type(kwargs) # revealed: Unknown
```

View file

@ -0,0 +1,443 @@
# Generic classes: Legacy syntax
## Defining a generic class
At its simplest, to define a generic class using the legacy syntax, you inherit from the
`typing.Generic` special form, which is "specialized" with the generic class's type variables.
```py
from knot_extensions import generic_context
from typing import Generic, TypeVar
T = TypeVar("T")
S = TypeVar("S")
class SingleTypevar(Generic[T]): ...
class MultipleTypevars(Generic[T, S]): ...
reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
```
You cannot use the same typevar more than once.
```py
# TODO: error
class RepeatedTypevar(Generic[T, T]): ...
```
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).
```py
# error: [invalid-argument-type] "`Literal[int]` is not a valid argument to `typing.Generic`"
class GenericOfType(Generic[int]): ...
```
You can also define a generic class by inheriting from some _other_ generic class, and specializing
it with typevars.
```py
class InheritedGeneric(MultipleTypevars[T, S]): ...
class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ...
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[T, S]
reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[T]
reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None
```
If you don't specialize a generic base class, we use the default specialization, which maps each
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
the inheriting class generic.
```py
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
```
When inheriting from a generic class, you can optionally inherit from `typing.Generic` as well. But
if you do, you have to mention all of the typevars that you use in your other base classes.
```py
class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ...
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ...
class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ...
class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ...
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ...
reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: tuple[T, S]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) # revealed: tuple[T]
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) # revealed: tuple[T, S]
```
## Specializing generic classes explicitly
The type parameter can be specified explicitly:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
x: T
reveal_type(C[int]()) # revealed: C[int]
```
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
reveal_type(C[int, int]()) # revealed: Unknown
```
If the type variable has an upper bound, the specialized type must satisfy that bound:
```py
from typing import Union
BoundedT = TypeVar("BoundedT", bound=int)
BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str])
class Bounded(Generic[BoundedT]): ...
class BoundedByUnion(Generic[BoundedByUnionT]): ...
class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]()) # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]()) # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]()) # revealed: BoundedByUnion[int | str]
```
If the type variable is constrained, the specialized type must satisfy those constraints:
```py
ConstrainedT = TypeVar("ConstrainedT", int, str)
class Constrained(Generic[ConstrainedT]): ...
reveal_type(Constrained[int]()) # revealed: Constrained[int]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass]()) # revealed: Constrained[IntSubclass]
reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
## Inferring generic class parameters
We can infer the type parameter from a type context:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
x: T
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C[Unknown]
```
The typevars of a fully specialized generic class should no longer be visible:
```py
# TODO: revealed: int
reveal_type(c.x) # revealed: Unknown
```
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
specific type, we infer the typevar's default type:
```py
DefaultT = TypeVar("DefaultT", default=int)
class D(Generic[DefaultT]): ...
reveal_type(D()) # revealed: D[int]
```
If a typevar does not provide a default, we use `Unknown`:
```py
reveal_type(C()) # revealed: C[Unknown]
```
## Inferring generic class parameters from constructors
If the type of a constructor parameter is a class typevar, we can use that to infer the type
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
### `__new__` only
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
### `__init__` only
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
### Identical `__new__` and `__init__` signatures
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
### Compatible `__new__` and `__init__` signatures
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]):
def __new__(cls, *args, **kwargs) -> "C[T]":
return object.__new__(cls)
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D(Generic[T]):
def __new__(cls, x: T) -> "D[T]":
return object.__new__(cls)
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
### Both present, `__new__` inherited from a generic base class
If either method comes from a generic base class, we don't currently use its inferred specialization
to specialize the class.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
class C(Generic[T, U]):
def __new__(cls, *args, **kwargs) -> "C[T, U]":
return object.__new__(cls)
class D(C[V, int]):
def __init__(self, x: V) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
```
### `__init__` is itself generic
```py
from typing import Generic, TypeVar
S = TypeVar("S")
T = TypeVar("T")
class C(Generic[T]):
def __init__(self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```
## Generic subclass
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
propagate through:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]):
x: T | None = None
class ExplicitlyGenericSub(Base[T], Generic[T]): ...
class ImplicitlyGenericSub(Base[T]): ...
reveal_type(Base[int].x) # revealed: int | None
reveal_type(ExplicitlyGenericSub[int].x) # revealed: int | None
reveal_type(ImplicitlyGenericSub[int].x) # revealed: int | None
```
## Generic methods
Generic classes can contain methods that are themselves generic. The generic methods can refer to
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
scope for the method.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
class C(Generic[T]):
def method(self, u: U) -> U:
return u
c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
```
## Cyclic class definitions
### F-bounded quantification
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
#### In a stub file
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
```pyi
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]): ...
class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### With string forward references
A similar case can work in a non-stub file, if forward references are stringified:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]): ...
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
```
#### Without string forward references
In a non-stub file, without stringified forward references, this raises a `NameError`:
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class Base(Generic[T]): ...
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```
### Cyclic inheritance as a generic parameter
```pyi
from typing import Generic, TypeVar
T = TypeVar("T")
class Derived(list[Derived[T]], Generic[T]): ...
```
### Direct cyclic inheritance
Inheritance that would result in a cyclic MRO is detected as an error.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
# error: [unresolved-reference]
class C(C, Generic[T]): ...
# error: [unresolved-reference]
class D(D[int], Generic[T]): ...
```
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification

View file

@ -0,0 +1,272 @@
# Generic functions: Legacy syntax
## Typevar must be used at least twice
If you're only using a typevar for a single parameter, you don't need the typevar — just use
`object` (or the typevar's upper bound):
```py
from typing import TypeVar
T = TypeVar("T")
# TODO: error, should be (x: object)
def typevar_not_needed(x: T) -> None:
pass
BoundedT = TypeVar("BoundedT", bound=int)
# TODO: error, should be (x: int)
def bounded_typevar_not_needed(x: BoundedT) -> None:
pass
```
Typevars are only needed if you use them more than once. For instance, to specify that two
parameters must both have the same type:
```py
def two_params(x: T, y: T) -> T:
return x
```
or to specify that a return value is the same as a parameter:
```py
def return_value(x: T) -> T:
return x
```
Each typevar must also appear _somewhere_ in the parameter list:
```py
def absurd() -> T:
# There's no way to construct a T!
raise ValueError("absurd")
```
## Inferring generic function parameter types
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
is bound to at each call site.
```py
from typing import TypeVar
T = TypeVar("T")
def f(x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(1.0)) # revealed: float
reveal_type(f(True)) # revealed: Literal[True]
reveal_type(f("string")) # revealed: Literal["string"]
```
## Inferring “deep” generic parameter types
The matching up of call arguments and discovery of constraints on typevars can be a recursive
process for arbitrarily-nested generic types in parameters.
```py
from typing import TypeVar
T = TypeVar("T")
def f(x: list[T]) -> T:
return x[0]
# TODO: revealed: float
reveal_type(f([1.0, 2.0])) # revealed: Unknown
```
## Inferring a bound typevar
<!-- snapshot-diagnostics -->
```py
from typing import TypeVar
from typing_extensions import reveal_type
T = TypeVar("T", bound=int)
def f(x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(True)) # revealed: Literal[True]
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: Unknown
```
## Inferring a constrained typevar
<!-- snapshot-diagnostics -->
```py
from typing import TypeVar
from typing_extensions import reveal_type
T = TypeVar("T", int, None)
def f(x: T) -> T:
return x
reveal_type(f(1)) # revealed: int
reveal_type(f(True)) # revealed: int
reveal_type(f(None)) # revealed: None
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: Unknown
```
## Typevar constraints
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar
in the function.
```py
from typing import TypeVar
T = TypeVar("T", bound=int)
def good_param(x: T) -> None:
reveal_type(x) # revealed: T
```
If the function is annotated as returning the typevar, this means that the upper bound is _not_
assignable to that typevar, since return types are contravariant. In `bad`, we can infer that
`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the
return value is not guaranteed to be compatible for all `T: int`.
```py
def good_return(x: T) -> T:
return x
def bad_return(x: T) -> T:
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
return x + 1
```
## All occurrences of the same typevar have the same type
If a typevar appears multiple times in a function signature, all occurrences have the same type.
```py
from typing import TypeVar
T = TypeVar("T")
S = TypeVar("S")
def different_types(cond: bool, t: T, s: S) -> T:
if cond:
return t
else:
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
return s
def same_types(cond: bool, t1: T, t2: T) -> T:
if cond:
return t1
else:
return t2
```
## All occurrences of the same constrained typevar have the same type
The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__`
methods that are compatible with the return type, so the `return` expression is always well-typed:
```py
from typing import TypeVar
T = TypeVar("T", int, str)
def same_constrained_types(t1: T, t2: T) -> T:
# TODO: no error
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`"
return t1 + t2
```
This is _not_ the same as a union type, because of this additional constraint that the two
occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types,
and an `int` and a `str` cannot be added together:
```py
def unions_are_different(t1: int | str, t2: int | str) -> int | str:
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
return t1 + t2
```
## Typevar inference is a unification problem
When inferring typevar assignments in a generic function call, we cannot simply solve constraints
eagerly for each parameter in turn. We must solve a unification problem involving all of the
parameters simultaneously.
```py
from typing import TypeVar
T = TypeVar("T")
def two_params(x: T, y: T) -> T:
return x
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
```
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
all of the constraints.
```py
def union_param(x: T | None) -> T:
if x is None:
raise ValueError
return x
reveal_type(union_param("a")) # revealed: Literal["a"]
reveal_type(union_param(1)) # revealed: Literal[1]
reveal_type(union_param(None)) # revealed: Unknown
```
```py
def union_and_nonunion_params(x: T | int, y: T) -> T:
return y
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
```
```py
S = TypeVar("S")
def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]:
return y
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
```
## Inferring nested generic function calls
We can infer type assignments in nested calls to multiple generic functions. If they use the same
type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below.
```py
from typing import TypeVar
T = TypeVar("T")
def f(x: T) -> tuple[T, int]:
return (x, 1)
def g(x: T) -> T | None:
return x
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
```

View file

@ -1,61 +1,74 @@
# Generic classes
# Generic classes: PEP 695 syntax
```toml
[environment]
python-version = "3.13"
```
## PEP 695 syntax
## Defining a generic class
TODO: Add a `red_knot_extension` function that asserts whether a function or class is generic.
This is a generic class defined using PEP 695 syntax:
At its simplest, to define a generic class using PEP 695 syntax, you add a list of typevars after
the class name.
```py
class C[T]: ...
from knot_extensions import generic_context
class SingleTypevar[T]: ...
class MultipleTypevars[T, S]: ...
reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
```
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
You cannot use the same typevar more than once.
```py
class D[U](C[U]): ...
# error: [invalid-syntax] "duplicate type parameter"
class RepeatedTypevar[T, T]: ...
```
A class that inherits from a generic class, but fills its type parameters with concrete types, is
_not_ generic:
You can only use typevars (TODO: or param specs or typevar tuples) in the class's generic context.
```py
class E(C[int]): ...
# TODO: error
class GenericOfType[int]: ...
```
A class that inherits from a generic class, and doesn't fill its type parameters at all, implicitly
uses the default value for the typevar. In this case, that default type is `Unknown`, so `F`
inherits from `C[Unknown]` and is not itself generic.
You can also define a generic class by inheriting from some _other_ generic class, and specializing
it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in
your base classes.
```py
class F(C): ...
class InheritedGeneric[U, V](MultipleTypevars[U, V]): ...
class InheritedGenericPartiallySpecialized[U](MultipleTypevars[U, int]): ...
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[U, V]
reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[U]
reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None
```
## Legacy syntax
If you don't specialize a generic base class, we use the default specialization, which maps each
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
the inheriting class generic.
This is a generic class defined using the legacy syntax:
```py
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
```
You cannot use PEP-695 syntax and the legacy syntax in the same class definition.
```py
from typing import Generic, TypeVar
T = TypeVar("T")
class C(Generic[T]): ...
# error: [invalid-generic-class] "Cannot both inherit from `Generic` and use PEP 695 type variables"
class BothGenericSyntaxes[U](Generic[T]): ...
```
A class that inherits from a generic class, and fills its type parameters with typevars, is generic.
```py
class D(C[T]): ...
```
(Examples `E` and `F` from above do not have analogues in the legacy syntax.)
## Specializing generic classes explicitly
The type parameter can be specified explicitly:
@ -84,10 +97,12 @@ class IntSubclass(int): ...
reveal_type(Bounded[int]()) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]()) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]()) # revealed: Unknown
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]()) # revealed: Unknown
reveal_type(BoundedByUnion[int]()) # revealed: BoundedByUnion[int]
@ -113,6 +128,7 @@ reveal_type(Constrained[str]()) # revealed: Constrained[str]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]()) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument to this function is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]()) # revealed: Unknown
```
@ -158,7 +174,7 @@ If the type of a constructor parameter is a class typevar, we can use that to in
parameter. The types inferred from a type context and from a constructor parameter must be
consistent with each other.
## `__new__` only
### `__new__` only
```py
class C[T]:
@ -171,7 +187,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]]
wrong_innards: C[int] = C("five")
```
## `__init__` only
### `__init__` only
```py
class C[T]:
@ -183,7 +199,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]]
wrong_innards: C[int] = C("five")
```
## Identical `__new__` and `__init__` signatures
### Identical `__new__` and `__init__` signatures
```py
class C[T]:
@ -198,7 +214,7 @@ reveal_type(C(1)) # revealed: C[Literal[1]]
wrong_innards: C[int] = C("five")
```
## Compatible `__new__` and `__init__` signatures
### Compatible `__new__` and `__init__` signatures
```py
class C[T]:
@ -224,9 +240,23 @@ reveal_type(D(1)) # revealed: D[Literal[1]]
wrong_innards: D[int] = D("five")
```
## `__init__` is itself generic
### Both present, `__new__` inherited from a generic base class
TODO: These do not currently work yet, because we don't correctly model the nested generic contexts.
If either method comes from a generic base class, we don't currently use its inferred specialization
to specialize the class.
```py
class C[T, U]:
def __new__(cls, *args, **kwargs) -> "C[T, U]":
return object.__new__(cls)
class D[V](C[V, int]):
def __init__(self, x: V) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
```
### `__init__` is itself generic
```py
class C[T]:

View file

@ -1,4 +1,4 @@
# Generic functions
# Generic functions: PEP 695 syntax
```toml
[environment]
@ -83,7 +83,7 @@ def f[T: int](x: T) -> T:
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f(True)) # revealed: Literal[True]
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: Unknown
```
@ -100,7 +100,7 @@ def f[T: (int, None)](x: T) -> T:
reveal_type(f(1)) # revealed: int
reveal_type(f(True)) # revealed: int
reveal_type(f(None)) # revealed: None
# error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
# error: [invalid-argument-type]
reveal_type(f("string")) # revealed: Unknown
```

View file

@ -3,5 +3,5 @@
## Empty dictionary
```py
reveal_type({}) # revealed: dict
reveal_type({}) # revealed: dict[Unknown, Unknown]
```

View file

@ -70,8 +70,8 @@ simultaneously:
class DuplicateBases(Protocol, Protocol[T]):
x: T
# TODO: should not have `Generic` multiple times and `Protocol` multiple times
# revealed: tuple[Literal[DuplicateBases], typing.Protocol, typing.Generic, @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# TODO: should not have `Protocol` multiple times
# revealed: tuple[Literal[DuplicateBases], typing.Protocol, @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
reveal_type(DuplicateBases.__mro__)
```

View file

@ -58,8 +58,7 @@ reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: ob
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
# TODO: needs support generics; should be `dict[str, Any]`:
reveal_type(typing.__dict__) # revealed: @Todo(specialized non-generic class)
reveal_type(typing.__dict__) # revealed: dict[str, Any]
```
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
@ -91,9 +90,8 @@ reveal_type(__dict__) # revealed: Literal["foo"]
import foo
from foo import __dict__ as foo_dict
# TODO: needs support generics; should be `dict[str, Any]` for both of these:
reveal_type(foo.__dict__) # revealed: @Todo(specialized non-generic class)
reveal_type(foo_dict) # revealed: @Todo(specialized non-generic class)
reveal_type(foo.__dict__) # revealed: dict[str, Any]
reveal_type(foo_dict) # revealed: dict[str, Any]
```
## Conditionally global or `ModuleType` attribute

View file

@ -0,0 +1,90 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a bound typevar
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 | from typing_extensions import reveal_type
3 |
4 | T = TypeVar("T", bound=int)
5 |
6 | def f(x: T) -> T:
7 | return x
8 |
9 | reveal_type(f(1)) # revealed: Literal[1]
10 | reveal_type(f(True)) # revealed: Literal[True]
11 | # error: [invalid-argument-type]
12 | reveal_type(f("string")) # revealed: Unknown
```
# Diagnostics
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:9:1
|
7 | return x
8 |
9 | reveal_type(f(1)) # revealed: Literal[1]
| ^^^^^^^^^^^^^^^^^ `Literal[1]`
10 | reveal_type(f(True)) # revealed: Literal[True]
11 | # error: [invalid-argument-type]
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:10:1
|
9 | reveal_type(f(1)) # revealed: Literal[1]
10 | reveal_type(f(True)) # revealed: Literal[True]
| ^^^^^^^^^^^^^^^^^^^^ `Literal[True]`
11 | # error: [invalid-argument-type]
12 | reveal_type(f("string")) # revealed: Unknown
|
```
```
error: lint:invalid-argument-type: Argument to this function is incorrect
--> src/mdtest_snippet.py:12:15
|
10 | reveal_type(f(True)) # revealed: Literal[True]
11 | # error: [invalid-argument-type]
12 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`
|
info: Type variable defined here
--> src/mdtest_snippet.py:4:1
|
2 | from typing_extensions import reveal_type
3 |
4 | T = TypeVar("T", bound=int)
| ^
5 |
6 | def f(x: T) -> T:
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:12:1
|
10 | reveal_type(f(True)) # revealed: Literal[True]
11 | # error: [invalid-argument-type]
12 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
```

View file

@ -0,0 +1,105 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: functions.md - Generic functions: Legacy syntax - Inferring a constrained typevar
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar
2 | from typing_extensions import reveal_type
3 |
4 | T = TypeVar("T", int, None)
5 |
6 | def f(x: T) -> T:
7 | return x
8 |
9 | reveal_type(f(1)) # revealed: int
10 | reveal_type(f(True)) # revealed: int
11 | reveal_type(f(None)) # revealed: None
12 | # error: [invalid-argument-type]
13 | reveal_type(f("string")) # revealed: Unknown
```
# Diagnostics
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:9:1
|
7 | return x
8 |
9 | reveal_type(f(1)) # revealed: int
| ^^^^^^^^^^^^^^^^^ `int`
10 | reveal_type(f(True)) # revealed: int
11 | reveal_type(f(None)) # revealed: None
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:10:1
|
9 | reveal_type(f(1)) # revealed: int
10 | reveal_type(f(True)) # revealed: int
| ^^^^^^^^^^^^^^^^^^^^ `int`
11 | reveal_type(f(None)) # revealed: None
12 | # error: [invalid-argument-type]
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:11:1
|
9 | reveal_type(f(1)) # revealed: int
10 | reveal_type(f(True)) # revealed: int
11 | reveal_type(f(None)) # revealed: None
| ^^^^^^^^^^^^^^^^^^^^ `None`
12 | # error: [invalid-argument-type]
13 | reveal_type(f("string")) # revealed: Unknown
|
```
```
error: lint:invalid-argument-type: Argument to this function is incorrect
--> src/mdtest_snippet.py:13:15
|
11 | reveal_type(f(None)) # revealed: None
12 | # error: [invalid-argument-type]
13 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T`
|
info: Type variable defined here
--> src/mdtest_snippet.py:4:1
|
2 | from typing_extensions import reveal_type
3 |
4 | T = TypeVar("T", int, None)
| ^
5 |
6 | def f(x: T) -> T:
|
```
```
info: revealed-type: Revealed type
--> src/mdtest_snippet.py:13:1
|
11 | reveal_type(f(None)) # revealed: None
12 | # error: [invalid-argument-type]
13 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|
```

View file

@ -3,8 +3,8 @@ source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: functions.md - Generic functions - Inferring a bound typevar
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md
mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a bound typevar
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md
---
# Python source files
@ -19,7 +19,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions
5 |
6 | reveal_type(f(1)) # revealed: Literal[1]
7 | reveal_type(f(True)) # revealed: Literal[True]
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`"
8 | # error: [invalid-argument-type]
9 | reveal_type(f("string")) # revealed: Unknown
```
@ -34,7 +34,7 @@ info: revealed-type: Revealed type
6 | reveal_type(f(1)) # revealed: Literal[1]
| ^^^^^^^^^^^^^^^^^ `Literal[1]`
7 | reveal_type(f(True)) # revealed: Literal[True]
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper bo...
8 | # error: [invalid-argument-type]
|
```
@ -46,7 +46,7 @@ info: revealed-type: Revealed type
6 | reveal_type(f(1)) # revealed: Literal[1]
7 | reveal_type(f(True)) # revealed: Literal[True]
| ^^^^^^^^^^^^^^^^^^^^ `Literal[True]`
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
8 | # error: [invalid-argument-type]
9 | reveal_type(f("string")) # revealed: Unknown
|
@ -57,7 +57,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect
--> src/mdtest_snippet.py:9:15
|
7 | reveal_type(f(True)) # revealed: Literal[True]
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
8 | # error: [invalid-argument-type]
9 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy upper bound of type variable `T`
|
@ -78,7 +78,7 @@ info: revealed-type: Revealed type
--> src/mdtest_snippet.py:9:1
|
7 | reveal_type(f(True)) # revealed: Literal[True]
8 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy upper b...
8 | # error: [invalid-argument-type]
9 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|

View file

@ -3,8 +3,8 @@ source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: functions.md - Generic functions - Inferring a constrained typevar
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions.md
mdtest name: functions.md - Generic functions: PEP 695 syntax - Inferring a constrained typevar
mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md
---
# Python source files
@ -20,7 +20,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/generics/functions
6 | reveal_type(f(1)) # revealed: int
7 | reveal_type(f(True)) # revealed: int
8 | reveal_type(f(None)) # revealed: None
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constraints of type variable `T`"
9 | # error: [invalid-argument-type]
10 | reveal_type(f("string")) # revealed: Unknown
```
@ -48,7 +48,7 @@ info: revealed-type: Revealed type
7 | reveal_type(f(True)) # revealed: int
| ^^^^^^^^^^^^^^^^^^^^ `int`
8 | reveal_type(f(None)) # revealed: None
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
9 | # error: [invalid-argument-type]
|
```
@ -61,7 +61,7 @@ info: revealed-type: Revealed type
7 | reveal_type(f(True)) # revealed: int
8 | reveal_type(f(None)) # revealed: None
| ^^^^^^^^^^^^^^^^^^^^ `None`
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
9 | # error: [invalid-argument-type]
10 | reveal_type(f("string")) # revealed: Unknown
|
@ -72,7 +72,7 @@ error: lint:invalid-argument-type: Argument to this function is incorrect
--> src/mdtest_snippet.py:10:15
|
8 | reveal_type(f(None)) # revealed: None
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
9 | # error: [invalid-argument-type]
10 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^ Argument type `Literal["string"]` does not satisfy constraints of type variable `T`
|
@ -93,7 +93,7 @@ info: revealed-type: Revealed type
--> src/mdtest_snippet.py:10:1
|
8 | reveal_type(f(None)) # revealed: None
9 | # error: [invalid-argument-type] "Argument to this function is incorrect: Argument type `Literal["string"]` does not satisfy constra...
9 | # error: [invalid-argument-type]
10 | reveal_type(f("string")) # revealed: Unknown
| ^^^^^^^^^^^^^^^^^^^^^^^^ `Unknown`
|

View file

@ -116,6 +116,6 @@ from typing import Tuple
class C(Tuple): ...
# TODO: generic protocols
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), @Todo(`Generic[]` subscript), Literal[object]]
# revealed: tuple[Literal[C], Literal[tuple], Literal[Sequence], Literal[Reversible], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, Literal[object]]
reveal_type(C.__mro__)
```

View file

@ -595,8 +595,6 @@ from functools import partial
def f(x: int, y: str) -> None: ...
# TODO: no error
# error: [invalid-assignment] "Object of type `partial` is not assignable to `(int, /) -> None`"
c1: Callable[[int], None] = partial(f, y="a")
```

View file

@ -17,6 +17,7 @@ pandas # slow
pandas-stubs # cycle panics (try_metaclass_)
pandera # cycle panics (try_metaclass_)
prefect # slow
pylint # cycle panics (self-recursive type alias)
pytest # cycle panics (signature_)
pywin32 # bad use-def map (binding with definitely-visible unbound)
schemathesis # cycle panics (signature_)
@ -27,4 +28,5 @@ spark # cycle panics (try_metaclass_)
steam.py # cycle panics (try_metaclass_), often hangs when multi-threaded
streamlit # cycle panic (signature_)
sympy # stack overflow
trio # cycle panics (deferred annotatation resolving in wrong scope)
xarray # cycle panics (try_metaclass_)

View file

@ -78,7 +78,6 @@ pycryptodome
pydantic
pyinstrument
pyjwt
pylint
pylox
pyodide
pyp
@ -101,7 +100,6 @@ static-frame
stone
strawberry
tornado
trio
twine
typeshed-stats
urllib3

View file

@ -589,11 +589,7 @@ impl<'db> Type<'db> {
pub fn contains_todo(&self, db: &'db dyn Db) -> bool {
match self {
Self::Dynamic(
DynamicType::Todo(_)
| DynamicType::SubscriptedProtocol
| DynamicType::SubscriptedGeneric,
) => true,
Self::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol) => true,
Self::AlwaysFalsy
| Self::AlwaysTruthy
@ -636,9 +632,7 @@ impl<'db> Type<'db> {
Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
SubclassOfInner::Dynamic(
DynamicType::Todo(_)
| DynamicType::SubscriptedProtocol
| DynamicType::SubscriptedGeneric,
DynamicType::Todo(_) | DynamicType::SubscriptedProtocol,
) => true,
SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false,
SubclassOfInner::Class(_) => false,
@ -656,17 +650,11 @@ impl<'db> Type<'db> {
Self::BoundSuper(bound_super) => {
matches!(
bound_super.pivot_class(db),
ClassBase::Dynamic(
DynamicType::Todo(_)
| DynamicType::SubscriptedGeneric
| DynamicType::SubscriptedProtocol
)
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::SubscriptedProtocol)
) || matches!(
bound_super.owner(db),
SuperOwnerKind::Dynamic(
DynamicType::Todo(_)
| DynamicType::SubscriptedGeneric
| DynamicType::SubscriptedProtocol
DynamicType::Todo(_) | DynamicType::SubscriptedProtocol
)
)
}
@ -4432,18 +4420,19 @@ impl<'db> Type<'db> {
// have the class's typevars still in the method signature when we attempt to call it. To
// do this, we instead use the _identity_ specialization, which maps each of the class's
// generic typevars to itself.
let (generic_origin, self_type) = match self {
let (generic_origin, generic_context, self_type) = match self {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => {
let specialization = generic_context.identity_specialization(db);
(
Some(class),
Some(generic_context),
Type::GenericAlias(GenericAlias::new(db, class, specialization)),
)
}
_ => (None, self),
_ => (None, None, self),
},
_ => (None, self),
_ => (None, None, self),
};
// As of now we do not model custom `__call__` on meta-classes, so the code below
@ -4555,12 +4544,18 @@ impl<'db> Type<'db> {
.and_then(Result::ok)
.as_ref()
.and_then(Bindings::single_element)
.and_then(|binding| combine_binding_specialization(db, binding));
.and_then(|binding| combine_binding_specialization(db, binding))
.filter(|specialization| {
Some(specialization.generic_context(db)) == generic_context
});
let init_specialization = init_call_outcome
.and_then(Result::ok)
.as_ref()
.and_then(Bindings::single_element)
.and_then(|binding| combine_binding_specialization(db, binding));
.and_then(|binding| combine_binding_specialization(db, binding))
.filter(|specialization| {
Some(specialization.generic_context(db)) == generic_context
});
let specialization =
combine_specializations(db, new_specialization, init_specialization);
let specialized = specialization
@ -4741,7 +4736,7 @@ impl<'db> Type<'db> {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Protocol],
fallback_type: Type::unknown(),
}),
KnownInstanceType::Generic => Err(InvalidTypeExpressionError {
KnownInstanceType::Generic(_) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Generic],
fallback_type: Type::unknown(),
}),
@ -5141,6 +5136,10 @@ impl<'db> Type<'db> {
}
}
Type::GenericAlias(alias) => {
alias.specialization(db).find_legacy_typevars(db, typevars);
}
Type::Dynamic(_)
| Type::Never
| Type::AlwaysTruthy
@ -5151,7 +5150,6 @@ impl<'db> Type<'db> {
| Type::DataclassTransformer(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::GenericAlias(_)
| Type::SubclassOf(_)
| Type::IntLiteral(_)
| Type::BooleanLiteral(_)
@ -5176,7 +5174,10 @@ impl<'db> Type<'db> {
match self {
Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db),
Type::StringLiteral(_) | Type::LiteralString => *self,
Type::KnownInstance(known_instance) => Type::string_literal(db, known_instance.repr()),
Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new(
db,
known_instance.repr(db).to_string().into_boxed_str(),
)),
// TODO: handle more complex types
_ => KnownClass::Str.to_instance(db),
}
@ -5194,7 +5195,10 @@ impl<'db> Type<'db> {
Type::string_literal(db, &format!("'{}'", literal.value(db).escape_default()))
}
Type::LiteralString => Type::LiteralString,
Type::KnownInstance(known_instance) => Type::string_literal(db, known_instance.repr()),
Type::KnownInstance(known_instance) => Type::StringLiteral(StringLiteralType::new(
db,
known_instance.repr(db).to_string().into_boxed_str(),
)),
// TODO: handle more complex types
_ => KnownClass::Str.to_instance(db),
}
@ -5390,9 +5394,6 @@ pub enum DynamicType {
/// Temporary type until we support generic protocols.
/// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly.
SubscriptedProtocol,
/// Temporary type until we support old-style generics.
/// We use a separate variant (instead of `Todo(…)`) in order to be able to match on them explicitly.
SubscriptedGeneric,
}
impl std::fmt::Display for DynamicType {
@ -5408,11 +5409,6 @@ impl std::fmt::Display for DynamicType {
} else {
"@Todo"
}),
DynamicType::SubscriptedGeneric => f.write_str(if cfg!(debug_assertions) {
"@Todo(`Generic[]` subscript)"
} else {
"@Todo"
}),
}
}
}
@ -5568,12 +5564,12 @@ impl<'db> InvalidTypeExpression<'db> {
InvalidTypeExpression::TypeQualifier(qualifier) => write!(
f,
"Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions)",
q = qualifier.repr()
q = qualifier.repr(self.db)
),
InvalidTypeExpression::TypeQualifierRequiresOneArgument(qualifier) => write!(
f,
"Type qualifier `{q}` is not allowed in type expressions (only in annotation expressions, and only with exactly one argument)",
q = qualifier.repr()
q = qualifier.repr(self.db)
),
InvalidTypeExpression::InvalidType(ty) => write!(
f,
@ -6932,6 +6928,8 @@ pub enum KnownFunction {
IsSingleton,
/// `knot_extensions.is_single_valued`
IsSingleValued,
/// `knot_extensions.generic_context`
GenericContext,
}
impl KnownFunction {
@ -6987,6 +6985,7 @@ impl KnownFunction {
| Self::IsSingleValued
| Self::IsSingleton
| Self::IsSubtypeOf
| Self::GenericContext
| Self::StaticAssert => module.is_knot_extensions(),
}
}
@ -8383,6 +8382,7 @@ pub(crate) mod tests {
KnownFunction::IsSingleton
| KnownFunction::IsSubtypeOf
| KnownFunction::GenericContext
| KnownFunction::StaticAssert
| KnownFunction::IsFullyStatic
| KnownFunction::IsDisjointFrom

View file

@ -564,6 +564,27 @@ impl<'db> Bindings<'db> {
}
}
Some(KnownFunction::GenericContext) => {
if let [Some(ty)] = overload.parameter_types() {
// TODO: Handle generic functions, and unions/intersections of
// generic types
overload.set_return_type(match ty {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => TupleType::from_elements(
db,
generic_context
.variables(db)
.iter()
.map(|typevar| Type::TypeVar(*typevar)),
),
None => Type::none(db),
},
_ => Type::none(db),
});
}
}
Some(KnownFunction::Len) => {
if let [Some(first_arg)] = overload.parameter_types() {
if let Some(len_ty) = first_arg.len(db) {

View file

@ -454,8 +454,26 @@ impl<'db> ClassLiteral<'db> {
self.known(db) == Some(known_class)
}
#[salsa::tracked]
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
// the knowledge that this class is not generic.
if self.is_known(db, KnownClass::VersionInfo) {
return None;
}
// We've already verified that the class literal does not contain both a PEP-695 generic
// scope and a `typing.Generic` base class.
//
// Note that if a class has an explicit legacy generic context (by inheriting from
// `typing.Generic`), and also an implicit one (by inheriting from other generic classes,
// specialized by typevars), the explicit one takes precedence.
self.pep695_generic_context(db)
.or_else(|| self.legacy_generic_context(db))
.or_else(|| self.inherited_legacy_generic_context(db))
}
#[salsa::tracked]
pub(crate) fn pep695_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
let scope = self.body_scope(db);
let class_def_node = scope.node(db).expect_class();
class_def_node.type_params.as_ref().map(|type_params| {
@ -464,6 +482,26 @@ impl<'db> ClassLiteral<'db> {
})
}
pub(crate) fn legacy_generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
self.explicit_bases(db).iter().find_map(|base| match base {
Type::KnownInstance(KnownInstanceType::Generic(generic_context)) => *generic_context,
_ => None,
})
}
pub(crate) fn inherited_legacy_generic_context(
self,
db: &'db dyn Db,
) -> Option<GenericContext<'db>> {
GenericContext::from_base_classes(
db,
self.explicit_bases(db)
.iter()
.copied()
.filter(|ty| matches!(ty, Type::GenericAlias(_))),
)
}
/// Return `true` if this class represents the builtin class `object`
pub(crate) fn is_object(self, db: &'db dyn Db) -> bool {
self.is_known(db, KnownClass::Object)
@ -919,10 +957,8 @@ impl<'db> ClassLiteral<'db> {
for superclass in mro_iter {
match superclass {
ClassBase::Dynamic(
DynamicType::SubscriptedGeneric | DynamicType::SubscriptedProtocol,
)
| ClassBase::Generic
ClassBase::Dynamic(DynamicType::SubscriptedProtocol)
| ClassBase::Generic(_)
| ClassBase::Protocol => {
// TODO: We currently skip `Protocol` when looking up class members, in order to
// avoid creating many dynamic types in our test suite that would otherwise
@ -1264,10 +1300,8 @@ impl<'db> ClassLiteral<'db> {
for superclass in self.iter_mro(db, specialization) {
match superclass {
ClassBase::Dynamic(
DynamicType::SubscriptedProtocol | DynamicType::SubscriptedGeneric,
)
| ClassBase::Generic
ClassBase::Dynamic(DynamicType::SubscriptedProtocol)
| ClassBase::Generic(_)
| ClassBase::Protocol => {
// TODO: We currently skip these when looking up instance members, in order to
// avoid creating many dynamic types in our test suite that would otherwise
@ -2237,6 +2271,43 @@ impl<'db> KnownClass {
.unwrap_or_else(Type::unknown)
}
/// Lookup a [`KnownClass`] in typeshed and return a [`Type`]
/// representing all possible instances of the generic class with a specialization.
///
/// If the class cannot be found in typeshed, or if you provide a specialization with the wrong
/// number of types, a debug-level log message will be emitted stating this.
pub(crate) fn to_specialized_instance(
self,
db: &'db dyn Db,
specialization: impl IntoIterator<Item = Type<'db>>,
) -> Type<'db> {
let class_literal = self.to_class_literal(db).expect_class_literal();
let Some(generic_context) = class_literal.generic_context(db) else {
return Type::unknown();
};
let types = specialization.into_iter().collect::<Box<[_]>>();
if types.len() != generic_context.len(db) {
// a cache of the `KnownClass`es that we have already seen mismatched-arity
// specializations for (and therefore that we've already logged a warning for)
static MESSAGES: LazyLock<Mutex<FxHashSet<KnownClass>>> = LazyLock::new(Mutex::default);
if MESSAGES.lock().unwrap().insert(self) {
tracing::info!(
"Wrong number of types when specializing {}. \
Falling back to `Unknown` for the symbol instead.",
self.display(db)
);
}
return Type::unknown();
}
let specialization = generic_context.specialize(db, types);
Type::instance(
db,
ClassType::Generic(GenericAlias::new(db, class_literal, specialization)),
)
}
/// Attempt to lookup a [`KnownClass`] in typeshed and return a [`Type`] representing that class-literal.
///
/// Return an error if the symbol cannot be found in the expected typeshed module,

View file

@ -1,3 +1,4 @@
use crate::types::generics::GenericContext;
use crate::types::{
todo_type, ClassType, DynamicType, KnownClass, KnownInstanceType, MroIterator, Type,
};
@ -21,7 +22,7 @@ pub enum ClassBase<'db> {
/// Bare `Generic` cannot be subclassed directly in user code,
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
/// `Protocol[T]`, or bare `Protocol`.
Generic,
Generic(Option<GenericContext<'db>>),
}
impl<'db> ClassBase<'db> {
@ -50,7 +51,13 @@ impl<'db> ClassBase<'db> {
write!(f, "<class '{}'>", alias.display(self.db))
}
ClassBase::Protocol => f.write_str("typing.Protocol"),
ClassBase::Generic => f.write_str("typing.Generic"),
ClassBase::Generic(generic_context) => {
f.write_str("typing.Generic")?;
if let Some(generic_context) = generic_context {
write!(f, "{}", generic_context.display(self.db))?;
}
Ok(())
}
}
}
}
@ -181,7 +188,9 @@ impl<'db> ClassBase<'db> {
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
}
KnownInstanceType::Protocol => Some(ClassBase::Protocol),
KnownInstanceType::Generic => Some(ClassBase::Generic),
KnownInstanceType::Generic(generic_context) => {
Some(ClassBase::Generic(generic_context))
}
},
}
}
@ -189,20 +198,22 @@ impl<'db> ClassBase<'db> {
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
match self {
Self::Class(class) => Some(class),
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
Self::Dynamic(_) | Self::Generic(_) | Self::Protocol => None,
}
}
/// Iterate over the MRO of this base
pub(super) fn mro(self, db: &'db dyn Db) -> impl Iterator<Item = ClassBase<'db>> {
match self {
ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic),
ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => ClassBaseMroIterator::length_3(
db,
self,
ClassBase::Dynamic(DynamicType::SubscriptedGeneric),
),
ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self),
ClassBase::Protocol => {
ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None))
}
ClassBase::Dynamic(DynamicType::SubscriptedProtocol) => {
ClassBaseMroIterator::length_3(db, self, ClassBase::Generic(None))
}
ClassBase::Dynamic(_) | ClassBase::Generic(_) => {
ClassBaseMroIterator::length_2(db, self)
}
ClassBase::Class(class) => ClassBaseMroIterator::from_class(db, class),
}
}
@ -220,7 +231,9 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
ClassBase::Dynamic(dynamic) => Type::Dynamic(dynamic),
ClassBase::Class(class) => class.into(),
ClassBase::Protocol => Type::KnownInstance(KnownInstanceType::Protocol),
ClassBase::Generic => Type::KnownInstance(KnownInstanceType::Generic),
ClassBase::Generic(generic_context) => {
Type::KnownInstance(KnownInstanceType::Generic(generic_context))
}
}
}
}

View file

@ -9,6 +9,7 @@ use crate::types::string_annotation::{
RAW_STRING_TYPE_ANNOTATION,
};
use crate::types::{class::ProtocolClassLiteral, KnownFunction, KnownInstanceType, Type};
use crate::Db;
use ruff_db::diagnostic::{Annotation, Diagnostic, Severity, Span, SubDiagnostic};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::Ranged;
@ -35,6 +36,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_CONTEXT_MANAGER);
registry.register_lint(&INVALID_DECLARATION);
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_OVERLOAD);
@ -393,6 +395,32 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid generic classes
///
/// ## Why is this bad?
/// There are several requirements that you must follow when defining a generic class.
///
/// ## Examples
/// ```python
/// from typing import Generic, TypeVar
///
/// T = TypeVar("T") # okay
///
/// # error: class uses both PEP-695 syntax and legacy syntax
/// class C[U](Generic[T]): ...
/// ```
///
/// ## References
/// - [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)
pub(crate) static INVALID_GENERIC_CLASS = {
summary: "detects invalid generic classes",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid legacy `TypeVar`s
@ -1378,6 +1406,7 @@ pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node:
}
pub(crate) fn report_invalid_arguments_to_annotated(
db: &dyn Db,
context: &InferContext,
subscript: &ast::ExprSubscript,
) {
@ -1387,7 +1416,7 @@ pub(crate) fn report_invalid_arguments_to_annotated(
builder.into_diagnostic(format_args!(
"Special form `{}` expected at least 2 arguments \
(one type and at least one metadata element)",
KnownInstanceType::Annotated.repr()
KnownInstanceType::Annotated.repr(db)
));
}
@ -1427,6 +1456,7 @@ pub(crate) fn report_bad_argument_to_get_protocol_members(
}
pub(crate) fn report_invalid_arguments_to_callable(
db: &dyn Db,
context: &InferContext,
subscript: &ast::ExprSubscript,
) {
@ -1435,7 +1465,7 @@ pub(crate) fn report_invalid_arguments_to_callable(
};
builder.into_diagnostic(format_args!(
"Special form `{}` expected exactly two arguments (parameter types and return type)",
KnownInstanceType::Callable.repr()
KnownInstanceType::Callable.repr(db)
));
}

View file

@ -14,7 +14,7 @@ use crate::types::{
StringLiteralType, SubclassOfInner, Type, TypeVarBoundOrConstraints, TypeVarInstance,
UnionType, WrapperDescriptorKind,
};
use crate::Db;
use crate::{Db, FxOrderSet};
use rustc_hash::FxHashMap;
impl<'db> Type<'db> {
@ -113,7 +113,7 @@ impl Display for DisplayRepresentation<'_> {
SubclassOfInner::Class(class) => write!(f, "type[{}]", class.name(self.db)),
SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"),
},
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr()),
Type::KnownInstance(known_instance) => write!(f, "{}", known_instance.repr(self.db)),
Type::FunctionLiteral(function) => {
let signature = function.signature(self.db);
@ -317,7 +317,7 @@ impl<'db> GenericContext<'db> {
}
pub struct DisplayGenericContext<'db> {
typevars: &'db [TypeVarInstance<'db>],
typevars: &'db FxOrderSet<TypeVarInstance<'db>>,
db: &'db dyn Db,
}
@ -376,7 +376,7 @@ impl<'db> Specialization<'db> {
}
pub struct DisplaySpecialization<'db> {
typevars: &'db [TypeVarInstance<'db>],
typevars: &'db FxOrderSet<TypeVarInstance<'db>>,
types: &'db [Type<'db>],
db: &'db dyn Db,
full: bool,

View file

@ -16,7 +16,7 @@ use crate::{Db, FxOrderSet};
#[salsa::interned(debug)]
pub struct GenericContext<'db> {
#[return_ref]
pub(crate) variables: Box<[TypeVarInstance<'db>]>,
pub(crate) variables: FxOrderSet<TypeVarInstance<'db>>,
}
impl<'db> GenericContext<'db> {
@ -26,7 +26,7 @@ impl<'db> GenericContext<'db> {
index: &'db SemanticIndex<'db>,
type_params_node: &ast::TypeParams,
) -> Self {
let variables: Box<[_]> = type_params_node
let variables: FxOrderSet<_> = type_params_node
.iter()
.filter_map(|type_param| Self::variable_from_type_param(db, index, type_param))
.collect();
@ -54,7 +54,7 @@ impl<'db> GenericContext<'db> {
}
}
/// Creates a generic context from the legecy `TypeVar`s that appear in a function parameter
/// Creates a generic context from the legacy `TypeVar`s that appear in a function parameter
/// list.
pub(crate) fn from_function_params(
db: &'db dyn Db,
@ -76,10 +76,29 @@ impl<'db> GenericContext<'db> {
if variables.is_empty() {
return None;
}
let variables: Box<[_]> = variables.into_iter().collect();
Some(Self::new(db, variables))
}
/// Creates a generic context from the legacy `TypeVar`s that appear in class's base class
/// list.
pub(crate) fn from_base_classes(
db: &'db dyn Db,
bases: impl Iterator<Item = Type<'db>>,
) -> Option<Self> {
let mut variables = FxOrderSet::default();
for base in bases {
base.find_legacy_typevars(db, &mut variables);
}
if variables.is_empty() {
return None;
}
Some(Self::new(db, variables))
}
pub(crate) fn len(self, db: &'db dyn Db) -> usize {
self.variables(db).len()
}
pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> {
let parameters = Parameters::new(
self.variables(db)
@ -130,11 +149,18 @@ impl<'db> GenericContext<'db> {
self.specialize(db, types.into())
}
pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool {
self.variables(db).is_subset(other.variables(db))
}
/// Creates a specialization of this generic context. Panics if the length of `types` does not
/// match the number of typevars in the generic context.
pub(crate) fn specialize(
self,
db: &'db dyn Db,
types: Box<[Type<'db>]>,
) -> Specialization<'db> {
assert!(self.variables(db).len() == types.len());
Specialization::new(db, self, types)
}
}
@ -205,12 +231,11 @@ impl<'db> Specialization<'db> {
/// Returns the type that a typevar is specialized to, or None if the typevar isn't part of
/// this specialization.
pub(crate) fn get(self, db: &'db dyn Db, typevar: TypeVarInstance<'db>) -> Option<Type<'db>> {
self.generic_context(db)
let index = self
.generic_context(db)
.variables(db)
.into_iter()
.zip(self.types(db))
.find(|(var, _)| **var == typevar)
.map(|(_, ty)| *ty)
.get_index_of(&typevar)?;
Some(self.types(db)[index])
}
pub(crate) fn is_subtype_of(self, db: &'db dyn Db, other: Specialization<'db>) -> bool {
@ -324,6 +349,16 @@ impl<'db> Specialization<'db> {
true
}
pub(crate) fn find_legacy_typevars(
self,
db: &'db dyn Db,
typevars: &mut FxOrderSet<TypeVarInstance<'db>>,
) {
for ty in self.types(db) {
ty.find_legacy_typevars(db, typevars);
}
}
}
/// Performs type inference between parameter annotations and argument types, producing a

View file

@ -72,10 +72,11 @@ use crate::types::diagnostic::{
report_possibly_unbound_attribute, TypeCheckDiagnostics, CALL_NON_CALLABLE,
CALL_POSSIBLY_UNBOUND_METHOD, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_BASE, INCONSISTENT_MRO,
INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
INVALID_LEGACY_TYPE_VARIABLE, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL,
UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_LEGACY_TYPE_VARIABLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_VARIABLE_CONSTRAINTS,
POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT,
UNSUPPORTED_OPERATOR,
};
use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
@ -92,7 +93,7 @@ use crate::types::{
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use crate::{Db, FxOrderSet};
use super::context::{InNoTypeCheck, InferContext};
use super::diagnostic::{
@ -767,7 +768,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let DefinitionKind::Class(class) = definition.kind(self.db()) {
ty.inner_type()
.into_class_literal()
.map(|ty| (ty, class.node()))
.map(|class_literal| (class_literal, class.node()))
} else {
None
}
@ -801,7 +802,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// - If the class is a protocol class: check for inheritance from a non-protocol class
for (i, base_class) in class.explicit_bases(self.db()).iter().enumerate() {
let base_class = match base_class {
Type::KnownInstance(KnownInstanceType::Generic) => {
Type::KnownInstance(KnownInstanceType::Generic(None)) => {
if let Some(builder) = self
.context
.report_lint(&INVALID_BASE, &class_node.bases()[i])
@ -976,6 +977,35 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
}
// (5) Check that a generic class does not have invalid or conflicting generic
// contexts.
if class.pep695_generic_context(self.db()).is_some()
&& class.legacy_generic_context(self.db()).is_some()
{
if let Some(builder) = self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
builder.into_diagnostic(
"Cannot both inherit from `Generic` and use PEP 695 type variables",
);
}
}
if let (Some(legacy), Some(inherited)) = (
class.legacy_generic_context(self.db()),
class.inherited_legacy_generic_context(self.db()),
) {
if !inherited.is_subset_of(self.db(), legacy) {
if let Some(builder) =
self.context.report_lint(&INVALID_GENERIC_CLASS, class_node)
{
builder.into_diagnostic(
"`Generic` base class must include all type \
variables used in other base classes",
);
}
}
}
}
}
@ -2011,9 +2041,11 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
if let Some(annotation) = parameter.annotation() {
let _annotated_ty = self.file_expression_type(annotation);
// TODO `dict[str, annotated_type]`
let ty = KnownClass::Dict.to_instance(self.db());
let annotated_ty = self.file_expression_type(annotation);
let ty = KnownClass::Dict.to_specialized_instance(
self.db(),
[KnownClass::Str.to_instance(self.db()), annotated_ty],
);
self.add_declaration_with_binding(
parameter.into(),
definition,
@ -2023,8 +2055,10 @@ impl<'db> TypeInferenceBuilder<'db> {
self.add_binding(
parameter.into(),
definition,
// TODO `dict[str, Unknown]`
KnownClass::Dict.to_instance(self.db()),
KnownClass::Dict.to_specialized_instance(
self.db(),
[KnownClass::Str.to_instance(self.db()), Type::unknown()],
),
);
}
}
@ -5547,8 +5581,6 @@ impl<'db> TypeInferenceBuilder<'db> {
| (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo),
(todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _, _)
| (_, todo @ Type::Dynamic(DynamicType::SubscriptedProtocol), _) => Some(todo),
(todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _, _)
| (_, todo @ Type::Dynamic(DynamicType::SubscriptedGeneric), _) => Some(todo),
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
@ -6641,9 +6673,16 @@ impl<'db> TypeInferenceBuilder<'db> {
) -> Type<'db> {
let slice_node = subscript.slice.as_ref();
let call_argument_types = match slice_node {
ast::Expr::Tuple(tuple) => CallArgumentTypes::positional(
tuple.elts.iter().map(|elt| self.infer_type_expression(elt)),
),
ast::Expr::Tuple(tuple) => {
let arguments = CallArgumentTypes::positional(
tuple.elts.iter().map(|elt| self.infer_type_expression(elt)),
);
self.store_expression_type(
slice_node,
TupleType::from_elements(self.db(), arguments.iter().map(|(_, ty)| ty)),
);
arguments
}
_ => CallArgumentTypes::positional([self.infer_type_expression(slice_node)]),
};
let signatures = Signatures::single(CallableSignature::single(
@ -6812,8 +6851,14 @@ impl<'db> TypeInferenceBuilder<'db> {
(Type::KnownInstance(KnownInstanceType::Protocol), _) => {
Type::Dynamic(DynamicType::SubscriptedProtocol)
}
(Type::KnownInstance(KnownInstanceType::Generic), _) => {
Type::Dynamic(DynamicType::SubscriptedGeneric)
(Type::KnownInstance(KnownInstanceType::Generic(None)), Type::Tuple(typevars)) => {
self.infer_subscript_legacy_generic_class(value_node, typevars.elements(self.db()))
}
(Type::KnownInstance(KnownInstanceType::Generic(None)), typevar) => self
.infer_subscript_legacy_generic_class(value_node, std::slice::from_ref(&typevar)),
(Type::KnownInstance(KnownInstanceType::Generic(Some(_))), _) => {
// TODO: emit a diagnostic
todo_type!("doubly-specialized typing.Generic")
}
(Type::KnownInstance(known_instance), _)
if known_instance.class().is_special_form() =>
@ -6934,7 +6979,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if !value_ty.into_class_literal().is_some_and(|class| {
class
.iter_mro(self.db(), None)
.contains(&ClassBase::Dynamic(DynamicType::SubscriptedGeneric))
.any(|base| matches!(base, ClassBase::Generic(_)))
}) {
report_non_subscriptable(
&self.context,
@ -6969,6 +7014,35 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
fn infer_subscript_legacy_generic_class(
&mut self,
value_node: &ast::Expr,
typevars: &[Type<'db>],
) -> Type<'db> {
let typevars: Option<FxOrderSet<_>> = typevars
.iter()
.map(|typevar| match typevar {
Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => Some(*typevar),
_ => {
if let Some(builder) =
self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node)
{
builder.into_diagnostic(format_args!(
"`{}` is not a valid argument to `typing.Generic`",
typevar.display(self.db()),
));
}
None
}
})
.collect();
let Some(typevars) = typevars else {
return Type::unknown();
};
let generic_context = GenericContext::new(self.db(), typevars);
Type::KnownInstance(KnownInstanceType::Generic(Some(generic_context)))
}
fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> {
enum SliceArg {
Arg(Option<i32>),
@ -7137,7 +7211,11 @@ impl<'db> TypeInferenceBuilder<'db> {
}) = slice
{
if arguments.len() < 2 {
report_invalid_arguments_to_annotated(&self.context, subscript);
report_invalid_arguments_to_annotated(
self.db(),
&self.context,
subscript,
);
}
if let [inner_annotation, metadata @ ..] = &arguments[..] {
@ -7155,7 +7233,11 @@ impl<'db> TypeInferenceBuilder<'db> {
TypeAndQualifiers::unknown()
}
} else {
report_invalid_arguments_to_annotated(&self.context, subscript);
report_invalid_arguments_to_annotated(
self.db(),
&self.context,
subscript,
);
self.infer_annotation_expression_impl(slice)
}
}
@ -7169,7 +7251,7 @@ impl<'db> TypeInferenceBuilder<'db> {
builder.into_diagnostic(format_args!(
"Type qualifier `{type_qualifier}` \
expects exactly one type parameter",
type_qualifier = known_instance.repr(),
type_qualifier = known_instance.repr(self.db()),
));
}
Type::unknown().into()
@ -7829,7 +7911,7 @@ impl<'db> TypeInferenceBuilder<'db> {
elts: arguments, ..
}) = arguments_slice
else {
report_invalid_arguments_to_annotated(&self.context, subscript);
report_invalid_arguments_to_annotated(self.db(), &self.context, subscript);
// `Annotated[]` with less than two arguments is an error at runtime.
// However, we still treat `Annotated[T]` as `T` here for the purpose of
@ -7839,7 +7921,7 @@ impl<'db> TypeInferenceBuilder<'db> {
};
if arguments.len() < 2 {
report_invalid_arguments_to_annotated(&self.context, subscript);
report_invalid_arguments_to_annotated(self.db(), &self.context, subscript);
}
let [type_expr, metadata @ ..] = &arguments[..] else {
@ -7923,7 +8005,7 @@ impl<'db> TypeInferenceBuilder<'db> {
};
if !correct_argument_number {
report_invalid_arguments_to_callable(&self.context, subscript);
report_invalid_arguments_to_callable(self.db(), &self.context, subscript);
}
let callable_type = if let (Some(parameters), Some(return_type), true) =
@ -7952,7 +8034,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"Special form `{}` expected exactly one type parameter",
known_instance.repr()
known_instance.repr(self.db())
));
}
Type::unknown()
@ -7979,7 +8061,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"Special form `{}` expected exactly one type parameter",
known_instance.repr()
known_instance.repr(self.db())
));
}
Type::unknown()
@ -7995,7 +8077,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"Special form `{}` expected exactly one type parameter",
known_instance.repr()
known_instance.repr(self.db())
));
}
Type::unknown()
@ -8028,7 +8110,7 @@ impl<'db> TypeInferenceBuilder<'db> {
"Expected the first argument to `{}` \
to be a callable object, \
but got an object of type `{}`",
known_instance.repr(),
known_instance.repr(self.db()),
argument_type.display(db)
));
}
@ -8093,7 +8175,7 @@ impl<'db> TypeInferenceBuilder<'db> {
builder.into_diagnostic(format_args!(
"Type qualifier `{}` is not allowed in type expressions \
(only in annotation expressions)",
known_instance.repr()
known_instance.repr(self.db())
));
}
self.infer_type_expression(arguments_slice)
@ -8122,9 +8204,14 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_expression(arguments_slice);
Type::Dynamic(DynamicType::SubscriptedProtocol)
}
KnownInstanceType::Generic => {
KnownInstanceType::Generic(_) => {
self.infer_type_expression(arguments_slice);
Type::Dynamic(DynamicType::SubscriptedGeneric)
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"`typing.Generic` is not allowed in type expressions",
));
}
Type::unknown()
}
KnownInstanceType::NoReturn
| KnownInstanceType::Never
@ -8136,7 +8223,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"Type `{}` expected no type parameter",
known_instance.repr()
known_instance.repr(self.db())
));
}
Type::unknown()
@ -8150,7 +8237,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"Special form `{}` expected no type parameter",
known_instance.repr()
known_instance.repr(self.db())
));
}
Type::unknown()
@ -8161,7 +8248,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
let mut diag = builder.into_diagnostic(format_args!(
"Type `{}` expected no type parameter",
known_instance.repr()
known_instance.repr(self.db())
));
diag.info("Did you mean to use `Literal[...]` instead?");
}

View file

@ -8,6 +8,9 @@
//! variant can only be inhabited by one or two specific objects at runtime with
//! locations that are known in advance.
use std::fmt::Display;
use super::generics::GenericContext;
use super::{class::KnownClass, ClassType, Truthiness, Type, TypeAliasType, TypeVarInstance};
use crate::db::Db;
use crate::module_resolver::{file_to_module, KnownModule};
@ -59,7 +62,7 @@ pub enum KnownInstanceType<'db> {
/// The symbol `typing.Protocol` (which can also be found as `typing_extensions.Protocol`)
Protocol,
/// The symbol `typing.Generic` (which can also be found as `typing_extensions.Generic`)
Generic,
Generic(Option<GenericContext<'db>>),
/// The symbol `typing.Type` (which can also be found as `typing_extensions.Type`)
Type,
/// A single instance of `typing.TypeVar`
@ -142,7 +145,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::ChainMap
| Self::OrderedDict
| Self::Protocol
| Self::Generic
| Self::Generic(_)
| Self::ReadOnly
| Self::TypeAliasType(_)
| Self::Unknown
@ -156,54 +159,10 @@ impl<'db> KnownInstanceType<'db> {
}
/// Return the repr of the symbol at runtime
pub(crate) fn repr(self) -> &'db str {
match self {
Self::Annotated => "typing.Annotated",
Self::Literal => "typing.Literal",
Self::LiteralString => "typing.LiteralString",
Self::Optional => "typing.Optional",
Self::Union => "typing.Union",
Self::NoReturn => "typing.NoReturn",
Self::Never => "typing.Never",
Self::Any => "typing.Any",
Self::Tuple => "typing.Tuple",
Self::Type => "typing.Type",
Self::TypingSelf => "typing.Self",
Self::Final => "typing.Final",
Self::ClassVar => "typing.ClassVar",
Self::Callable => "typing.Callable",
Self::Concatenate => "typing.Concatenate",
Self::Unpack => "typing.Unpack",
Self::Required => "typing.Required",
Self::NotRequired => "typing.NotRequired",
Self::TypeAlias => "typing.TypeAlias",
Self::TypeGuard => "typing.TypeGuard",
Self::TypedDict => "typing.TypedDict",
Self::TypeIs => "typing.TypeIs",
Self::List => "typing.List",
Self::Dict => "typing.Dict",
Self::DefaultDict => "typing.DefaultDict",
Self::Set => "typing.Set",
Self::FrozenSet => "typing.FrozenSet",
Self::Counter => "typing.Counter",
Self::Deque => "typing.Deque",
Self::ChainMap => "typing.ChainMap",
Self::OrderedDict => "typing.OrderedDict",
Self::Protocol => "typing.Protocol",
Self::Generic => "typing.Generic",
Self::ReadOnly => "typing.ReadOnly",
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
// it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
Self::TypeVar(_) => "typing.TypeVar",
Self::TypeAliasType(_) => "typing.TypeAliasType",
Self::Unknown => "knot_extensions.Unknown",
Self::AlwaysTruthy => "knot_extensions.AlwaysTruthy",
Self::AlwaysFalsy => "knot_extensions.AlwaysFalsy",
Self::Not => "knot_extensions.Not",
Self::Intersection => "knot_extensions.Intersection",
Self::TypeOf => "knot_extensions.TypeOf",
Self::CallableTypeOf => "knot_extensions.CallableTypeOf",
pub(crate) fn repr(self, db: &'db dyn Db) -> impl Display + 'db {
KnownInstanceRepr {
known_instance: self,
db,
}
}
@ -243,7 +202,7 @@ impl<'db> KnownInstanceType<'db> {
Self::ChainMap => KnownClass::StdlibAlias,
Self::OrderedDict => KnownClass::StdlibAlias,
Self::Protocol => KnownClass::SpecialForm, // actually `_ProtocolMeta` at runtime but this is what typeshed says
Self::Generic => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says
Self::Generic(_) => KnownClass::SpecialForm, // actually `type` at runtime but this is what typeshed says
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::TypeOf => KnownClass::SpecialForm,
@ -287,7 +246,7 @@ impl<'db> KnownInstanceType<'db> {
"Counter" => Self::Counter,
"ChainMap" => Self::ChainMap,
"OrderedDict" => Self::OrderedDict,
"Generic" => Self::Generic,
"Generic" => Self::Generic(None),
"Protocol" => Self::Protocol,
"Optional" => Self::Optional,
"Union" => Self::Union,
@ -347,7 +306,7 @@ impl<'db> KnownInstanceType<'db> {
| Self::NoReturn
| Self::Tuple
| Self::Type
| Self::Generic
| Self::Generic(_)
| Self::Callable => module.is_typing(),
Self::Annotated
| Self::Protocol
@ -383,3 +342,67 @@ impl<'db> KnownInstanceType<'db> {
self.class().to_class_literal(db)
}
}
struct KnownInstanceRepr<'db> {
known_instance: KnownInstanceType<'db>,
db: &'db dyn Db,
}
impl Display for KnownInstanceRepr<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.known_instance {
KnownInstanceType::Annotated => f.write_str("typing.Annotated"),
KnownInstanceType::Literal => f.write_str("typing.Literal"),
KnownInstanceType::LiteralString => f.write_str("typing.LiteralString"),
KnownInstanceType::Optional => f.write_str("typing.Optional"),
KnownInstanceType::Union => f.write_str("typing.Union"),
KnownInstanceType::NoReturn => f.write_str("typing.NoReturn"),
KnownInstanceType::Never => f.write_str("typing.Never"),
KnownInstanceType::Any => f.write_str("typing.Any"),
KnownInstanceType::Tuple => f.write_str("typing.Tuple"),
KnownInstanceType::Type => f.write_str("typing.Type"),
KnownInstanceType::TypingSelf => f.write_str("typing.Self"),
KnownInstanceType::Final => f.write_str("typing.Final"),
KnownInstanceType::ClassVar => f.write_str("typing.ClassVar"),
KnownInstanceType::Callable => f.write_str("typing.Callable"),
KnownInstanceType::Concatenate => f.write_str("typing.Concatenate"),
KnownInstanceType::Unpack => f.write_str("typing.Unpack"),
KnownInstanceType::Required => f.write_str("typing.Required"),
KnownInstanceType::NotRequired => f.write_str("typing.NotRequired"),
KnownInstanceType::TypeAlias => f.write_str("typing.TypeAlias"),
KnownInstanceType::TypeGuard => f.write_str("typing.TypeGuard"),
KnownInstanceType::TypedDict => f.write_str("typing.TypedDict"),
KnownInstanceType::TypeIs => f.write_str("typing.TypeIs"),
KnownInstanceType::List => f.write_str("typing.List"),
KnownInstanceType::Dict => f.write_str("typing.Dict"),
KnownInstanceType::DefaultDict => f.write_str("typing.DefaultDict"),
KnownInstanceType::Set => f.write_str("typing.Set"),
KnownInstanceType::FrozenSet => f.write_str("typing.FrozenSet"),
KnownInstanceType::Counter => f.write_str("typing.Counter"),
KnownInstanceType::Deque => f.write_str("typing.Deque"),
KnownInstanceType::ChainMap => f.write_str("typing.ChainMap"),
KnownInstanceType::OrderedDict => f.write_str("typing.OrderedDict"),
KnownInstanceType::Protocol => f.write_str("typing.Protocol"),
KnownInstanceType::Generic(generic_context) => {
f.write_str("typing.Generic")?;
if let Some(generic_context) = generic_context {
write!(f, "{}", generic_context.display(self.db))?;
}
Ok(())
}
KnownInstanceType::ReadOnly => f.write_str("typing.ReadOnly"),
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
// it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
KnownInstanceType::TypeVar(_) => f.write_str("typing.TypeVar"),
KnownInstanceType::TypeAliasType(_) => f.write_str("typing.TypeAliasType"),
KnownInstanceType::Unknown => f.write_str("knot_extensions.Unknown"),
KnownInstanceType::AlwaysTruthy => f.write_str("knot_extensions.AlwaysTruthy"),
KnownInstanceType::AlwaysFalsy => f.write_str("knot_extensions.AlwaysFalsy"),
KnownInstanceType::Not => f.write_str("knot_extensions.Not"),
KnownInstanceType::Intersection => f.write_str("knot_extensions.Intersection"),
KnownInstanceType::TypeOf => f.write_str("knot_extensions.TypeOf"),
KnownInstanceType::CallableTypeOf => f.write_str("knot_extensions.CallableTypeOf"),
}
}
}

View file

@ -159,8 +159,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(_, ClassBase::Class(_)) => Ordering::Greater,
(ClassBase::Protocol, _) => Ordering::Less,
(_, ClassBase::Protocol) => Ordering::Greater,
(ClassBase::Generic, _) => Ordering::Less,
(_, ClassBase::Generic) => Ordering::Greater,
(ClassBase::Generic(left), ClassBase::Generic(right)) => left.cmp(right),
(ClassBase::Generic(_), _) => Ordering::Less,
(_, ClassBase::Generic(_)) => Ordering::Greater,
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
dynamic_elements_ordering(*left, *right)
}
@ -250,8 +251,11 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
(KnownInstanceType::OrderedDict, _) => Ordering::Less,
(_, KnownInstanceType::OrderedDict) => Ordering::Greater,
(KnownInstanceType::Generic, _) => Ordering::Less,
(_, KnownInstanceType::Generic) => Ordering::Greater,
(KnownInstanceType::Generic(left), KnownInstanceType::Generic(right)) => {
left.cmp(right)
}
(KnownInstanceType::Generic(_), _) => Ordering::Less,
(_, KnownInstanceType::Generic(_)) => Ordering::Greater,
(KnownInstanceType::Protocol, _) => Ordering::Less,
(_, KnownInstanceType::Protocol) => Ordering::Greater,
@ -380,9 +384,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
#[cfg(not(debug_assertions))]
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
(DynamicType::SubscriptedGeneric, _) => Ordering::Less,
(_, DynamicType::SubscriptedGeneric) => Ordering::Greater,
(DynamicType::SubscriptedProtocol, _) => Ordering::Less,
(_, DynamicType::SubscriptedProtocol) => Ordering::Greater,
}

View file

@ -26,3 +26,7 @@ def is_gradual_equivalent_to(type_a: Any, type_b: Any) -> bool: ...
def is_fully_static(type: Any) -> bool: ...
def is_singleton(type: Any) -> bool: ...
def is_single_valued(type: Any) -> bool: ...
# Returns the generic context of a type as a tuple of typevars, or `None` if the
# type is not generic.
def generic_context(type: Any) -> Any: ...

View file

@ -440,6 +440,16 @@
}
]
},
"invalid-generic-class": {
"title": "detects invalid generic classes",
"description": "## What it does\nChecks for the creation of invalid generic classes\n\n## Why is this bad?\nThere are several requirements that you must follow when defining a generic class.\n\n## Examples\n```python\nfrom typing import Generic, TypeVar\n\nT = TypeVar(\"T\") # okay\n\n# error: class uses both PEP-695 syntax and legacy syntax\nclass C[U](Generic[T]): ...\n```\n\n## References\n- [Typing spec: Generics](https://typing.python.org/en/latest/spec/generics.html#introduction)",
"default": "error",
"oneOf": [
{
"$ref": "#/definitions/Level"
}
]
},
"invalid-ignore-comment": {
"title": "detects ignore comments that use invalid syntax",
"description": "## What it does\nChecks for `type: ignore` and `knot: ignore` comments that are syntactically incorrect.\n\n## Why is this bad?\nA syntactically incorrect ignore comment is probably a mistake and is useless.\n\n## Examples\n```py\na = 20 / 0 # type: ignoree\n```\n\nUse instead:\n\n```py\na = 20 / 0 # type: ignore\n```",