mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[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:
parent
f7cae4ffb5
commit
96697c98f3
40 changed files with 1547 additions and 279 deletions
|
@ -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__)
|
||||
```
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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]:
|
|
@ -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
|
||||
```
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
# Variance
|
||||
# Variance: PEP 695 syntax
|
||||
|
||||
```toml
|
||||
[environment]
|
|
@ -3,5 +3,5 @@
|
|||
## Empty dictionary
|
||||
|
||||
```py
|
||||
reveal_type({}) # revealed: dict
|
||||
reveal_type({}) # revealed: dict[Unknown, Unknown]
|
||||
```
|
||||
|
|
|
@ -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__)
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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__)
|
||||
```
|
||||
|
|
|
@ -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")
|
||||
```
|
||||
|
||||
|
|
|
@ -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_)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?");
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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: ...
|
||||
|
|
|
@ -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```",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue