Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def _[T: (T if cond else U)[0], U](): pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class _[T: (0, T[0])]:
def _(x: T):
if x:
pass
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None
def func(c: Callable[P2, None]):
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None

# TODO: error: paramspec is unbound
# error: [invalid-type-arguments] "ParamSpec `P2` is unbound"
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None

# error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`"
Expand Down Expand Up @@ -327,15 +327,14 @@ reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str,
reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int
reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int

# TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int`
# TODO: error: paramspec is unbound
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "ParamSpec `P2` is unbound"
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> Unknown
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> int
```

Nor can they be omitted when there are more than one `ParamSpec`s.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,91 @@ reveal_type(C[int, int]) # revealed: <type alias 'C[Unknown]'>
And non-generic types cannot be specialized:

```py
from typing import TypeVar, Protocol, TypedDict

type B = ...

# error: [non-subscriptable] "Cannot subscript non-generic type alias"
reveal_type(B[int]) # revealed: Unknown

# error: [non-subscriptable] "Cannot subscript non-generic type alias"
def _(b: B[int]): ...
def _(b: B[int]):
reveal_type(b) # revealed: Unknown

type IntOrStr = int | str

# error: [non-subscriptable] "Cannot subscript non-generic type alias"
def _(c: IntOrStr[int]):
reveal_type(c) # revealed: Unknown

type ListOfInts = list[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized"
def _(l: ListOfInts[int]):
reveal_type(l) # revealed: Unknown

type List[T] = list[T]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed"
def _(l: List[int][int]):
reveal_type(l) # revealed: Unknown

# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'list[T@DoubleSpecialization]'>` is already specialized"
type DoubleSpecialization[T] = list[T][T]

def _(d: DoubleSpecialization[int]):
reveal_type(d) # revealed: Unknown

type Tuple = tuple[int, str]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: `tuple[int, str]` is already specialized"
def _(doubly_specialized: Tuple[int]):
reveal_type(doubly_specialized) # revealed: Unknown

T = TypeVar("T")

class LegacyProto(Protocol[T]):
pass

type LegacyProtoInt = LegacyProto[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: `LegacyProto[int]` is already specialized"
def _(x: LegacyProtoInt[int]):
reveal_type(x) # revealed: Unknown

class Proto[T](Protocol):
pass

type ProtoInt = Proto[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Proto[int]` is already specialized"
def _(x: ProtoInt[int]):
reveal_type(x) # revealed: Unknown

# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
x: T

type LegacyDictInt = LegacyDict[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias"
def _(x: LegacyDictInt[int]):
reveal_type(x) # revealed: Unknown

class Dict[T](TypedDict):
x: T

type DictInt = Dict[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: `Dict` is already specialized"
def _(x: DictInt[int]):
reveal_type(x) # revealed: Unknown

type Union = list[str] | list[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[str] | list[int]` is already specialized"
def _(x: Union[int]):
reveal_type(x) # revealed: Unknown
```

If the type variable has an upper bound, the specialized type must satisfy that bound:
Expand All @@ -98,6 +176,15 @@ reveal_type(BoundedByUnion[int]) # revealed: <type alias 'BoundedByUnion[int]'>
reveal_type(BoundedByUnion[IntSubclass]) # revealed: <type alias 'BoundedByUnion[IntSubclass]'>
reveal_type(BoundedByUnion[str]) # revealed: <type alias 'BoundedByUnion[str]'>
reveal_type(BoundedByUnion[int | str]) # revealed: <type alias 'BoundedByUnion[int | str]'>

type TupleOfIntAndStr[T: int, U: str] = tuple[T, U]

def _(x: TupleOfIntAndStr[int, str]):
reveal_type(x) # revealed: tuple[int, str]

# error: [invalid-type-arguments] "Type `int` is not assignable to upper bound `str` of type variable `U@TupleOfIntAndStr`"
def _(x: TupleOfIntAndStr[int, int]):
reveal_type(x) # revealed: tuple[int, Unknown]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior is consistent with pyright.
mypy and pyrefly do not replace even the failed parts with Unknown, which is not a good idea.

```

If the type variable is constrained, the specialized type must satisfy those constraints:
Expand All @@ -119,6 +206,15 @@ reveal_type(Constrained[int | str]) # revealed: <type alias 'Constrained[int |

# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object]) # revealed: <type alias 'Constrained[Unknown]'>

type TupleOfIntOrStr[T: (int, str), U: (int, str)] = tuple[T, U]

def _(x: TupleOfIntOrStr[int, str]):
reveal_type(x) # revealed: tuple[int, str]

# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `U@TupleOfIntOrStr`"
def _(x: TupleOfIntOrStr[int, object]):
reveal_type(x) # revealed: tuple[int, Unknown]
```

If the type variable has a default, it can be omitted:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def func[**P2](c: Callable[P2, None]):

P2 = ParamSpec("P2")

# TODO: error: paramspec is unbound
# error: [invalid-type-arguments] "ParamSpec `P2` is unbound"
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None

# error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`"
Expand Down Expand Up @@ -281,14 +281,14 @@ reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str,
reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int
reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int

# TODO: error: paramspec is unbound
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "ParamSpec `P2` is unbound"
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> Unknown
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> int
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> Unknown
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> int
```

Nor can they be omitted when there are more than one `ParamSpec`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -653,13 +653,92 @@ def g(obj: Y[bool, range]):

A generic alias that is already fully specialized cannot be specialized again:

```toml
[environment]
python-version = "3.12"
```

```py
from typing import Protocol, TypeVar, TypedDict

ListOfInts = list[int]

# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'list[int]'>` is already specialized"
def _(doubly_specialized: ListOfInts[int]):
# TODO: This should ideally be `list[Unknown]` or `Unknown`
reveal_type(doubly_specialized) # revealed: list[int]
reveal_type(doubly_specialized) # revealed: Unknown

type ListOfInts2 = list[int]
# error: [non-subscriptable] "Cannot subscript non-generic type alias: `list[int]` is already specialized"
DoublySpecialized = ListOfInts2[int]

def _(doubly_specialized: DoublySpecialized):
reveal_type(doubly_specialized) # revealed: Unknown

# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'list[int]'>` is already specialized"
List = list[int][int]

def _(doubly_specialized: List):
reveal_type(doubly_specialized) # revealed: Unknown

Tuple = tuple[int, str]

# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'tuple[int, str]'>` is already specialized"
def _(doubly_specialized: Tuple[int]):
reveal_type(doubly_specialized) # revealed: Unknown

T = TypeVar("T")

class LegacyProto(Protocol[T]):
pass

LegacyProtoInt = LegacyProto[int]

# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'LegacyProto[int]'>` is already specialized"
def _(doubly_specialized: LegacyProtoInt[int]):
reveal_type(doubly_specialized) # revealed: Unknown

class Proto[T](Protocol):
pass

ProtoInt = Proto[int]

# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'Proto[int]'>` is already specialized"
def _(doubly_specialized: ProtoInt[int]):
reveal_type(doubly_specialized) # revealed: Unknown

# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
x: T

# TODO: should be a `non-subscriptable` error
LegacyDictInt = LegacyDict[int]

# TODO: should be a `non-subscriptable` error
def _(doubly_specialized: LegacyDictInt[int]):
# TODO: should be `Unknown`
reveal_type(doubly_specialized) # revealed: @Todo(Inference of subscript on special form)

class Dict[T](TypedDict):
x: T

DictInt = Dict[int]

# error: [non-subscriptable] "Cannot subscript non-generic type: `<class 'Dict[int]'>` is already specialized"
def _(doubly_specialized: DictInt[int]):
reveal_type(doubly_specialized) # revealed: Unknown

Union = list[str] | list[int]

# error: [non-subscriptable] "Cannot subscript non-generic type: `<types.UnionType special form 'list[str] | list[int]'>` is already specialized"
def _(doubly_specialized: Union[int]):
reveal_type(doubly_specialized) # revealed: Unknown

type MyListAlias[T] = list[T]
MyListOfInts = MyListAlias[int]

# error: [non-subscriptable] "Cannot subscript non-generic type alias: Double specialization is not allowed"
def _(doubly_specialized: MyListOfInts[int]):
reveal_type(doubly_specialized) # revealed: Unknown
```

Specializing a generic implicit type alias with an incorrect number of type arguments also results
Expand Down Expand Up @@ -695,23 +774,21 @@ def this_does_not_work() -> TypeOf[IntOrStr]:
raise NotImplementedError()

def _(
# TODO: Better error message (of kind `invalid-type-form`)?
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
# error: [non-subscriptable] "Cannot subscript non-generic type"
specialized: this_does_not_work()[int],
):
reveal_type(specialized) # revealed: int | str
reveal_type(specialized) # revealed: Unknown
```

Similarly, if you try to specialize a union type without a binding context, we emit an error:

```py
# TODO: Better error message (of kind `invalid-type-form`)?
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
# error: [non-subscriptable] "Cannot subscript non-generic type"
x: (list[T] | set[T])[int]

def _():
# TODO: `list[Unknown] | set[Unknown]` might be better
reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar]
reveal_type(x) # revealed: Unknown
```

### Multiple definitions
Expand Down
35 changes: 35 additions & 0 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,41 @@ impl<'db> Type<'db> {
matches!(self, Type::GenericAlias(_))
}

/// Returns whether the definition of this type is generic
/// (this is different from whether this type *is* a generic type; a type that is already fully specialized is not a generic type).
pub(crate) fn is_definition_generic(self, db: &'db dyn Db) -> bool {
match self {
Type::Union(union) => union
.elements(db)
.iter()
.any(|ty| ty.is_definition_generic(db)),
Type::Intersection(intersection) => {
intersection
.positive(db)
.iter()
.any(|ty| ty.is_definition_generic(db))
|| intersection
.negative(db)
.iter()
.any(|ty| ty.is_definition_generic(db))
}
Type::NominalInstance(instance_type) => instance_type.is_definition_generic(),
Type::ProtocolInstance(protocol) => {
matches!(protocol.inner, Protocol::FromClass(class) if class.is_generic())
}
Type::TypedDict(typed_dict) => typed_dict
.defining_class()
.is_some_and(ClassType::is_generic),
Type::Dynamic(dynamic) => {
matches!(dynamic, DynamicType::UnknownGeneric(_))
}
// Due to inheritance rules, enums cannot be generic.
Type::EnumLiteral(_) => false,
// Once generic NewType is officially specified, handle it.
_ => false,
}
}

const fn is_dynamic(&self) -> bool {
matches!(self, Type::Dynamic(_))
}
Expand Down
Loading
Loading