diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md index 4c54139cc4..fe9c54ddd0 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/stdlib_typing_aliases.md @@ -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__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md index 1c7665d84d..3f254ebe47 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md +++ b/crates/red_knot_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md @@ -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: diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 17079b86b8..72dca325f9 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 3abc301372..c2646aaccd 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/decorators.md b/crates/red_knot_python_semantic/resources/mdtest/decorators.md index b1233f3d0f..923bf2e3eb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/decorators.md +++ b/crates/red_knot_python_semantic/resources/mdtest/decorators.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md index 8ff82e29df..1cfcf5c1a5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md +++ b/crates/red_knot_python_semantic/resources/mdtest/directives/cast.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md index da1a3de3f0..ef75db6482 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md +++ b/crates/red_knot_python_semantic/resources/mdtest/exception/except_star.md @@ -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] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md b/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md index 9db77532cb..d7d01ed8f5 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md +++ b/crates/red_knot_python_semantic/resources/mdtest/expression/lambda.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md b/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md index a4efccbd3a..b4d7b8cd14 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md +++ b/crates/red_knot_python_semantic/resources/mdtest/function/parameters.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md new file mode 100644 index 0000000000..70b9c91902 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/builtins.md @@ -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 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md new file mode 100644 index 0000000000..935aa2e7d9 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md new file mode 100644 index 0000000000..9f20d754cb --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/functions.md @@ -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 + + + +```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 + + + +```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 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md b/crates/red_knot_python_semantic/resources/mdtest/generics/legacy/variables.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/legacy.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/legacy/variables.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md similarity index 71% rename from crates/red_knot_python_semantic/resources/mdtest/generics/classes.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md index 82b2b8f7eb..400f4dc8d4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/classes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -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]: diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md similarity index 95% rename from crates/red_knot_python_semantic/resources/mdtest/generics/functions.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md index 22c62b1d35..ee5ed72c98 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/functions.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md similarity index 100% rename from crates/red_knot_python_semantic/resources/mdtest/generics/pep695.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variables.md diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md similarity index 99% rename from crates/red_knot_python_semantic/resources/mdtest/generics/variance.md rename to crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md index 1541a46fbc..489e74a77e 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics/variance.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics/pep695/variance.md @@ -1,4 +1,4 @@ -# Variance +# Variance: PEP 695 syntax ```toml [environment] diff --git a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md index 9a57135f2a..37abd2b98b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md +++ b/crates/red_knot_python_semantic/resources/mdtest/literal/collections/dictionary.md @@ -3,5 +3,5 @@ ## Empty dictionary ```py -reveal_type({}) # revealed: dict +reveal_type({}) # revealed: dict[Unknown, Unknown] ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/protocols.md b/crates/red_knot_python_semantic/resources/mdtest/protocols.md index 30afd66e7c..7f681a23c4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/protocols.md +++ b/crates/red_knot_python_semantic/resources/mdtest/protocols.md @@ -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__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md index 8cbe449526..f4fde3a2bb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md +++ b/crates/red_knot_python_semantic/resources/mdtest/scopes/moduletype_attrs.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap new file mode 100644 index 0000000000..902b25a6e7 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_bound_typevar.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap new file mode 100644 index 0000000000..f84cf8c783 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___Legacy_syntax_-_Inferring_a_constrained_typevar.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap similarity index 67% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap index 1fcfed3058..5c03b2551b 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_bound_typevar.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_bound_typevar.snap @@ -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` | diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap similarity index 71% rename from crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap rename to crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap index 2e2802007b..d1b259b730 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions_-_Inferring_a_constrained_typevar.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/functions.md_-_Generic_functions___PEP_695_syntax_-_Inferring_a_constrained_typevar.snap @@ -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` | diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 04b39d1f50..f07efe0e2f 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -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__) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 68c7e8ce1a..840c4f48e3 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -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") ``` diff --git a/crates/red_knot_python_semantic/resources/primer/bad.txt b/crates/red_knot_python_semantic/resources/primer/bad.txt index bf180717b9..4d2f648ab2 100644 --- a/crates/red_knot_python_semantic/resources/primer/bad.txt +++ b/crates/red_knot_python_semantic/resources/primer/bad.txt @@ -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_) diff --git a/crates/red_knot_python_semantic/resources/primer/good.txt b/crates/red_knot_python_semantic/resources/primer/good.txt index 0455d5c8fe..219abfbf11 100644 --- a/crates/red_knot_python_semantic/resources/primer/good.txt +++ b/crates/red_knot_python_semantic/resources/primer/good.txt @@ -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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index ed5ffef2c8..a519c44fc3 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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 diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 9b79610974..3921ae1e67 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -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) { diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index d118d28af2..cc77fdd4d3 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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> { + // 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> { 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> { + 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::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>, + ) -> 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::>(); + 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>> = 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, diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 03b627233d..28f1484d31 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -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>), } impl<'db> ClassBase<'db> { @@ -50,7 +51,13 @@ impl<'db> ClassBase<'db> { write!(f, "", 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> { 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> { 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> 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)) + } } } } diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index f563004890..c199c220e4 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -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) )); } diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index c878a81539..fc30a4f465 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -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>, db: &'db dyn Db, } @@ -376,7 +376,7 @@ impl<'db> Specialization<'db> { } pub struct DisplaySpecialization<'db> { - typevars: &'db [TypeVarInstance<'db>], + typevars: &'db FxOrderSet>, types: &'db [Type<'db>], db: &'db dyn Db, full: bool, diff --git a/crates/red_knot_python_semantic/src/types/generics.rs b/crates/red_knot_python_semantic/src/types/generics.rs index 8589f14a5c..0210cd2062 100644 --- a/crates/red_knot_python_semantic/src/types/generics.rs +++ b/crates/red_knot_python_semantic/src/types/generics.rs @@ -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>, } 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>, + ) -> Option { + 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> { - 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>, + ) { + for ty in self.types(db) { + ty.find_legacy_typevars(db, typevars); + } + } } /// Performs type inference between parameter annotations and argument types, producing a diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 991eb960da..0734bcf7f8 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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> = 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), @@ -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?"); } diff --git a/crates/red_knot_python_semantic/src/types/known_instance.rs b/crates/red_knot_python_semantic/src/types/known_instance.rs index 7a8cb2e464..b1cce6d979 100644 --- a/crates/red_knot_python_semantic/src/types/known_instance.rs +++ b/crates/red_knot_python_semantic/src/types/known_instance.rs @@ -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>), /// 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"), + } + } +} diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index d62baf9b90..bd390aff6e 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -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, } diff --git a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi index d7e69093d8..163a1d1cc6 100644 --- a/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi +++ b/crates/red_knot_vendored/knot_extensions/knot_extensions.pyi @@ -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: ... diff --git a/knot.schema.json b/knot.schema.json index aeda31fd53..49bc35855b 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -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```",