diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 4218eee1af..951c364462 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -95,7 +95,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -126,7 +126,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -190,7 +190,7 @@ class B(A): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -217,7 +217,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -329,7 +329,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -359,7 +359,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -650,7 +650,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -679,7 +679,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -756,7 +756,7 @@ class C[U](Generic[T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -795,7 +795,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -830,7 +830,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -864,7 +864,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -896,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -946,7 +946,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -966,13 +966,44 @@ weakens a type checker's ability to accurately reason about your code.
def f(a: int = ''): ...
```
+## `invalid-paramspec`
+
+
+Default level: error ·
+Added in 0.0.1-alpha.1 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Checks for the creation of invalid `ParamSpec`s
+
+**Why is this bad?**
+
+There are several requirements that you must follow when creating a `ParamSpec`.
+
+**Examples**
+
+```python
+from typing import ParamSpec
+
+P1 = ParamSpec("P1") # okay
+P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assigned to
+```
+
+**References**
+
+- [Typing spec: ParamSpec](https://typing.python.org/en/latest/spec/generics.html#paramspec)
+
## `invalid-protocol`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1006,7 +1037,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1055,7 +1086,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1080,7 +1111,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1138,7 +1169,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1165,7 +1196,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1195,7 +1226,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1225,7 +1256,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1259,7 +1290,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1293,7 +1324,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1328,7 +1359,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1353,7 +1384,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1386,7 +1417,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1415,7 +1446,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1439,7 +1470,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1465,7 +1496,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1492,7 +1523,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1550,7 +1581,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1580,7 +1611,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1609,7 +1640,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1636,7 +1667,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1664,7 +1695,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1710,7 +1741,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1737,7 +1768,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1765,7 +1796,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1790,7 +1821,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1815,7 +1846,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1852,7 +1883,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1880,7 +1911,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1905,7 +1936,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1946,7 +1977,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2034,7 +2065,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2062,7 +2093,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2094,7 +2125,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2126,7 +2157,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2153,7 +2184,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2177,7 +2208,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2235,7 +2266,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2274,7 +2305,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2337,7 +2368,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2361,7 +2392,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_ide/src/goto_type_definition.rs b/crates/ty_ide/src/goto_type_definition.rs
index f39e5dc4b6..7d86743af4 100644
--- a/crates/ty_ide/src/goto_type_definition.rs
+++ b/crates/ty_ide/src/goto_type_definition.rs
@@ -276,10 +276,20 @@ mod tests {
"#,
);
- // TODO: Goto type definition currently doesn't work for type param specs
- // because the inference doesn't support them yet.
- // This snapshot should show a single target pointing to `T`
- assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
+ assert_snapshot!(test.goto_type_definition(), @r"
+ info[goto-type-definition]: Type definition
+ --> main.py:2:14
+ |
+ 2 | type Alias[**P = [int, str]] = Callable[P, int]
+ | ^
+ |
+ info: Source
+ --> main.py:2:41
+ |
+ 2 | type Alias[**P = [int, str]] = Callable[P, int]
+ | ^
+ |
+ ");
}
#[test]
diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs
index b555295678..1b348b82b9 100644
--- a/crates/ty_ide/src/hover.rs
+++ b/crates/ty_ide/src/hover.rs
@@ -1633,11 +1633,12 @@ def ab(a: int, *, c: int):
"#,
);
+ // TODO: This should be `P@Alias ()`
assert_snapshot!(test.hover(), @r"
- @Todo
+ typing.ParamSpec
---------------------------------------------
```python
- @Todo
+ typing.ParamSpec
```
---------------------------------------------
info[hover]: Hovered content is
diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md
index 780b2a87db..e7e55f7a44 100644
--- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md
+++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md
@@ -307,8 +307,9 @@ Using a `ParamSpec` in a `Callable` annotation:
from typing_extensions import Callable
def _[**P1](c: Callable[P1, int]):
- reveal_type(P1.args) # revealed: @Todo(ParamSpec)
- reveal_type(P1.kwargs) # revealed: @Todo(ParamSpec)
+ # TODO: Should reveal `ParamSpecArgs` and `ParamSpecKwargs`
+ reveal_type(P1.args) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs)
+ reveal_type(P1.kwargs) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs)
# TODO: Signature should be (**P1) -> int
reveal_type(c) # revealed: (...) -> int
diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
index c61a94a8d6..c5d737d9eb 100644
--- a/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
+++ b/crates/ty_python_semantic/resources/mdtest/annotations/unsupported_special_forms.md
@@ -21,8 +21,9 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]:
def g() -> TypeGuard[int]: ...
def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co:
- reveal_type(args) # revealed: tuple[@Todo(Support for `typing.ParamSpec`), ...]
- reveal_type(kwargs) # revealed: dict[str, @Todo(Support for `typing.ParamSpec`)]
+ # TODO: Should reveal a type representing `P.args` and `P.kwargs`
+ reveal_type(args) # revealed: tuple[@Todo(ParamSpecArgs / ParamSpecKwargs), ...]
+ reveal_type(kwargs) # revealed: dict[str, @Todo(ParamSpecArgs / ParamSpecKwargs)]
return callback(42, *args, **kwargs)
class Foo:
diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
index c520b7e883..a1f47c3b11 100644
--- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
+++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
@@ -26,9 +26,12 @@ reveal_type(generic_context(SingleTypevar))
# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))
-# TODO: support `ParamSpec`/`TypeVarTuple` properly (these should not reveal `None`)
-reveal_type(generic_context(SingleParamSpec)) # revealed: None
-reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: None
+# revealed: tuple[P@SingleParamSpec]
+reveal_type(generic_context(SingleParamSpec))
+# revealed: tuple[P@TypeVarAndParamSpec, T@TypeVarAndParamSpec]
+reveal_type(generic_context(TypeVarAndParamSpec))
+
+# TODO: support `TypeVarTuple` properly (these should not reveal `None`)
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: None
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: None
```
diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec.md b/crates/ty_python_semantic/resources/mdtest/paramspec.md
new file mode 100644
index 0000000000..4ebc336d7f
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/paramspec.md
@@ -0,0 +1,159 @@
+# `ParamSpec`
+
+## Definition
+
+### Valid
+
+```py
+from typing import ParamSpec
+
+P = ParamSpec("P")
+reveal_type(type(P)) # revealed:
+reveal_type(P) # revealed: typing.ParamSpec
+reveal_type(P.__name__) # revealed: Literal["P"]
+```
+
+The paramspec name can also be provided as a keyword argument:
+
+```py
+from typing import ParamSpec
+
+P = ParamSpec(name="P")
+reveal_type(P.__name__) # revealed: Literal["P"]
+```
+
+### Must be directly assigned to a variable
+
+```py
+from typing import ParamSpec
+
+P = ParamSpec("P")
+# error: [invalid-paramspec]
+P1: ParamSpec = ParamSpec("P1")
+
+# error: [invalid-paramspec]
+tuple_with_typevar = ("foo", ParamSpec("W"))
+reveal_type(tuple_with_typevar[1]) # revealed: ParamSpec
+```
+
+```py
+from typing_extensions import ParamSpec
+
+T = ParamSpec("T")
+# error: [invalid-paramspec]
+P1: ParamSpec = ParamSpec("P1")
+
+# error: [invalid-paramspec]
+tuple_with_typevar = ("foo", ParamSpec("P2"))
+reveal_type(tuple_with_typevar[1]) # revealed: ParamSpec
+```
+
+### `ParamSpec` parameter must match variable name
+
+```py
+from typing import ParamSpec
+
+P1 = ParamSpec("P1")
+
+# error: [invalid-paramspec]
+P2 = ParamSpec("P3")
+```
+
+### Accepts only a single `name` argument
+
+> The runtime should accept bounds and covariant and contravariant arguments in the declaration just
+> as typing.TypeVar does, but for now we will defer the standardization of the semantics of those
+> options to a later PEP.
+
+```py
+from typing import ParamSpec
+
+# error: [invalid-paramspec]
+P1 = ParamSpec("P1", bound=int)
+# error: [invalid-paramspec]
+P2 = ParamSpec("P2", int, str)
+# error: [invalid-paramspec]
+P3 = ParamSpec("P3", covariant=True)
+# error: [invalid-paramspec]
+P4 = ParamSpec("P4", contravariant=True)
+```
+
+### Defaults
+
+```toml
+[environment]
+python-version = "3.13"
+```
+
+The default value for a `ParamSpec` can be either a list of types, `...`, or another `ParamSpec`.
+
+```py
+from typing import ParamSpec
+
+P1 = ParamSpec("P1", default=[int, str])
+P2 = ParamSpec("P2", default=...)
+P3 = ParamSpec("P3", default=P2)
+```
+
+Other values are invalid.
+
+```py
+# error: [invalid-paramspec]
+P4 = ParamSpec("P4", default=int)
+```
+
+### PEP 695
+
+```toml
+[environment]
+python-version = "3.12"
+```
+
+#### Valid
+
+```py
+def foo1[**P]() -> None:
+ reveal_type(P) # revealed: typing.ParamSpec
+
+def foo2[**P = ...]() -> None:
+ reveal_type(P) # revealed: typing.ParamSpec
+
+def foo3[**P = [int, str]]() -> None:
+ reveal_type(P) # revealed: typing.ParamSpec
+
+def foo4[**P, **Q = P]():
+ reveal_type(P) # revealed: typing.ParamSpec
+ reveal_type(Q) # revealed: typing.ParamSpec
+```
+
+#### Invalid
+
+ParamSpec, when defined using the new syntax, does not allow defining bounds or constraints.
+
+This results in a lot of syntax errors mainly because the AST doesn't accept them in this position.
+The parser could do a better job in recovering from these errors.
+
+
+
+```py
+# error: [invalid-syntax]
+# error: [invalid-syntax]
+# error: [invalid-syntax]
+# error: [invalid-syntax]
+# error: [invalid-syntax]
+# error: [invalid-syntax]
+def foo[**P: int]() -> None:
+ # error: [invalid-syntax]
+ # error: [invalid-syntax]
+ pass
+```
+
+
+
+#### Invalid default
+
+```py
+# error: [invalid-paramspec]
+def foo[**P = int]() -> None:
+ pass
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md
index a39b6a6f16..1386a9e158 100644
--- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md
+++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md
@@ -1171,9 +1171,7 @@ class EggsLegacy(Generic[T, P]): ...
static_assert(not is_assignable_to(Spam, Callable[..., Any]))
static_assert(not is_assignable_to(SpamLegacy, Callable[..., Any]))
static_assert(not is_assignable_to(Eggs, Callable[..., Any]))
-
-# TODO: should pass
-static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any])) # error: [static-assert-error]
+static_assert(not is_assignable_to(EggsLegacy, Callable[..., Any]))
```
### Classes with `__call__` as attribute
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index 59e1ef4030..bc75895833 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -4358,6 +4358,13 @@ impl<'db> Type<'db> {
.into()
}
+ Type::KnownInstance(KnownInstanceType::TypeVar(typevar))
+ if typevar.kind(db).is_paramspec()
+ && matches!(name.as_str(), "args" | "kwargs") =>
+ {
+ Place::bound(todo_type!("ParamSpecArgs / ParamSpecKwargs")).into()
+ }
+
Type::NominalInstance(..)
| Type::ProtocolInstance(..)
| Type::BooleanLiteral(..)
@@ -7024,7 +7031,7 @@ impl<'db> Type<'db> {
Type::TypeVar(bound_typevar) => {
if matches!(
bound_typevar.typevar(db).kind(db),
- TypeVarKind::Legacy | TypeVarKind::TypingSelf
+ TypeVarKind::Legacy | TypeVarKind::TypingSelf | TypeVarKind::ParamSpec
) && binding_context.is_none_or(|binding_context| {
bound_typevar.binding_context(db) == BindingContext::Definition(binding_context)
}) {
@@ -7743,6 +7750,9 @@ impl<'db> KnownInstanceType<'db> {
fn class(self, db: &'db dyn Db) -> KnownClass {
match self {
Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm,
+ Self::TypeVar(typevar_instance) if typevar_instance.kind(db).is_paramspec() => {
+ KnownClass::ParamSpec
+ }
Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => {
KnownClass::GenericAlias
@@ -7808,7 +7818,13 @@ impl<'db> KnownInstanceType<'db> {
// 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::TypeVar(typevar_instance) => {
+ if typevar_instance.kind(self.db).is_paramspec() {
+ f.write_str("typing.ParamSpec")
+ } else {
+ f.write_str("typing.TypeVar")
+ }
+ }
KnownInstanceType::Deprecated(_) => f.write_str("warnings.deprecated"),
KnownInstanceType::Field(field) => {
f.write_str("dataclasses.Field")?;
@@ -7864,9 +7880,6 @@ pub enum DynamicType<'db> {
///
/// This variant should be created with the `todo_type!` macro.
Todo(TodoType),
- /// A special Todo-variant for PEP-695 `ParamSpec` types. A temporary variant to detect and special-
- /// case the handling of these types in `Callable` annotations.
- TodoPEP695ParamSpec,
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
TodoTypeAlias,
@@ -7894,13 +7907,6 @@ impl std::fmt::Display for DynamicType<'_> {
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
// any other type
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
- DynamicType::TodoPEP695ParamSpec => {
- if cfg!(debug_assertions) {
- f.write_str("@Todo(ParamSpec)")
- } else {
- f.write_str("@Todo")
- }
- }
DynamicType::TodoUnpack => {
if cfg!(debug_assertions) {
f.write_str("@Todo(typing.Unpack)")
@@ -8239,12 +8245,20 @@ pub enum TypeVarKind {
Pep695,
/// `typing.Self`
TypingSelf,
+ /// `P = ParamSpec("P")`
+ ParamSpec,
+ /// `def foo[**P]() -> None: ...`
+ Pep695ParamSpec,
}
impl TypeVarKind {
const fn is_self(self) -> bool {
matches!(self, Self::TypingSelf)
}
+
+ const fn is_paramspec(self) -> bool {
+ matches!(self, Self::ParamSpec | Self::Pep695ParamSpec)
+ }
}
/// The identity of a type variable.
@@ -8597,6 +8611,15 @@ impl<'db> TypeVarInstance<'db> {
let expr = &call_expr.arguments.find_keyword("default")?.value;
Some(definition_expression_type(db, definition, expr))
}
+ // PEP 695 ParamSpec
+ DefinitionKind::ParamSpec(paramspec) => {
+ let paramspec_node = paramspec.node(&module);
+ Some(definition_expression_type(
+ db,
+ definition,
+ paramspec_node.default.as_ref()?,
+ ))
+ }
_ => None,
}
}
diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs
index bed18de8b9..071d4b92b7 100644
--- a/crates/ty_python_semantic/src/types/class_base.rs
+++ b/crates/ty_python_semantic/src/types/class_base.rs
@@ -49,10 +49,7 @@ impl<'db> ClassBase<'db> {
ClassBase::Dynamic(DynamicType::Any) => "Any",
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
ClassBase::Dynamic(
- DynamicType::Todo(_)
- | DynamicType::TodoPEP695ParamSpec
- | DynamicType::TodoTypeAlias
- | DynamicType::TodoUnpack,
+ DynamicType::Todo(_) | DynamicType::TodoTypeAlias | DynamicType::TodoUnpack,
) => "@Todo",
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
ClassBase::Protocol => "Protocol",
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 5d647f108f..33efcc74fd 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -63,6 +63,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_EXCEPTION_CAUGHT);
registry.register_lint(&INVALID_GENERIC_CLASS);
registry.register_lint(&INVALID_LEGACY_TYPE_VARIABLE);
+ registry.register_lint(&INVALID_PARAMSPEC);
registry.register_lint(&INVALID_TYPE_ALIAS_TYPE);
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_OVERLOAD);
@@ -880,6 +881,30 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Checks for the creation of invalid `ParamSpec`s
+ ///
+ /// ## Why is this bad?
+ /// There are several requirements that you must follow when creating a `ParamSpec`.
+ ///
+ /// ## Examples
+ /// ```python
+ /// from typing import ParamSpec
+ ///
+ /// P1 = ParamSpec("P1") # okay
+ /// P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assigned to
+ /// ```
+ ///
+ /// ## References
+ /// - [Typing spec: ParamSpec](https://typing.python.org/en/latest/spec/generics.html#paramspec)
+ pub(crate) static INVALID_PARAMSPEC = {
+ summary: "detects invalid ParamSpec usage",
+ status: LintStatus::stable("0.0.1-alpha.1"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Checks for the creation of invalid `TypeAliasType`s
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index e72b4af8db..8806dff536 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -59,11 +59,12 @@ use crate::types::diagnostic::{
DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE,
INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION,
INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS,
- INVALID_NAMED_TUPLE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PROTOCOL,
- INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS,
- IncompatibleBases, NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT,
- SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL,
- UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
+ INVALID_NAMED_TUPLE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC,
+ INVALID_PROTOCOL, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
+ INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
+ POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS,
+ UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
+ UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY,
hint_if_stdlib_attribute_exists_on_other_versions,
hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation,
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict,
@@ -1296,6 +1297,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
DefinitionKind::TypeVar(typevar) => {
self.infer_typevar_deferred(typevar.node(self.module()));
}
+ DefinitionKind::ParamSpec(paramspec) => {
+ self.infer_paramspec_deferred(paramspec.node(self.module()));
+ }
DefinitionKind::Assignment(assignment) => {
self.infer_assignment_deferred(assignment.value(self.module()));
}
@@ -3182,18 +3186,120 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let ast::TypeParamParamSpec {
range: _,
node_index: _,
- name: _,
+ name,
default,
} = node;
- self.infer_optional_expression(default.as_deref(), TypeContext::default());
- let pep_695_todo = Type::Dynamic(DynamicType::TodoPEP695ParamSpec);
+ if default.is_some() {
+ self.deferred.insert(definition);
+ }
+ let identity = TypeVarIdentity::new(
+ self.db(),
+ &name.id,
+ Some(definition),
+ TypeVarKind::Pep695ParamSpec,
+ );
+ let ty = Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
+ self.db(),
+ identity,
+ None, // ParamSpec, when declared using PEP 695 syntax, has no bounds or constraints
+ None, // explicit_variance
+ default.as_deref().map(|_| TypeVarDefaultEvaluation::Lazy),
+ )));
self.add_declaration_with_binding(
node.into(),
definition,
- &DeclaredAndInferredType::are_the_same_type(pep_695_todo),
+ &DeclaredAndInferredType::are_the_same_type(ty),
);
}
+ fn infer_paramspec_deferred(&mut self, node: &ast::TypeParamParamSpec) {
+ let ast::TypeParamParamSpec {
+ range: _,
+ node_index: _,
+ name: _,
+ default: Some(default),
+ } = node
+ else {
+ return;
+ };
+ let previous_deferred_state =
+ std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred);
+ let default_ty = self.infer_paramspec_default(default);
+ self.store_expression_type(default, default_ty);
+ self.deferred_state = previous_deferred_state;
+ }
+
+ fn infer_paramspec_default(&mut self, default: &ast::Expr) -> Type<'db> {
+ // This is the same logic as `TypeInferenceBuilder::infer_callable_parameter_types` except
+ // for the subscript branch which is required for `Concatenate` but that cannot be
+ // specified in this context.
+ match default {
+ ast::Expr::EllipsisLiteral(_) => {
+ CallableType::single(self.db(), Signature::new(Parameters::gradual_form(), None))
+ }
+ ast::Expr::List(ast::ExprList { elts, .. }) => {
+ let mut parameter_types = Vec::with_capacity(elts.len());
+
+ // Whether to infer `Todo` for the parameters
+ let mut return_todo = false;
+
+ for param in elts {
+ let param_type = self.infer_type_expression(param);
+ // This is similar to what we currently do for inferring tuple type expression.
+ // We currently infer `Todo` for the parameters to avoid invalid diagnostics
+ // when trying to check for assignability or any other relation. For example,
+ // `*tuple[int, str]`, `Unpack[]`, etc. are not yet supported.
+ return_todo |= param_type.is_todo()
+ && matches!(param, ast::Expr::Starred(_) | ast::Expr::Subscript(_));
+ parameter_types.push(param_type);
+ }
+
+ let parameters = if return_todo {
+ // TODO: `Unpack`
+ Parameters::todo()
+ } else {
+ Parameters::new(parameter_types.iter().map(|param_type| {
+ Parameter::positional_only(None).with_annotated_type(*param_type)
+ }))
+ };
+
+ CallableType::single(self.db(), Signature::new(parameters, None))
+ }
+ ast::Expr::Name(name) => {
+ let name_ty = self.infer_name_load(name);
+ let is_paramspec = match name_ty {
+ Type::KnownInstance(known_instance) => {
+ known_instance.class(self.db()) == KnownClass::ParamSpec
+ }
+ Type::NominalInstance(nominal) => {
+ nominal.has_known_class(self.db(), KnownClass::ParamSpec)
+ }
+ _ => false,
+ };
+ if is_paramspec {
+ name_ty
+ } else {
+ if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default) {
+ builder.into_diagnostic(
+ "The default value to `ParamSpec` must be either a list of types, \
+ `ParamSpec`, or `...`",
+ );
+ }
+ Type::unknown()
+ }
+ }
+ _ => {
+ if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default) {
+ builder.into_diagnostic(
+ "The default value to `ParamSpec` must be either a list of types, \
+ `ParamSpec`, or `...`",
+ );
+ }
+ Type::unknown()
+ }
+ }
+ }
+
fn infer_typevartuple_definition(
&mut self,
node: &ast::TypeParamTypeVarTuple,
@@ -4324,17 +4430,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
TypeContext::default(),
);
- let typevar_class = callable_type
+ let ty = match callable_type
.as_class_literal()
.and_then(|cls| cls.known(self.db()))
- .filter(|cls| {
- matches!(cls, KnownClass::TypeVar | KnownClass::ExtensionsTypeVar)
- });
-
- let ty = if let Some(typevar_class) = typevar_class {
- self.infer_legacy_typevar(target, call_expr, definition, typevar_class)
- } else {
- self.infer_call_expression_impl(call_expr, callable_type, tcx)
+ {
+ Some(
+ typevar_class @ (KnownClass::TypeVar | KnownClass::ExtensionsTypeVar),
+ ) => {
+ self.infer_legacy_typevar(target, call_expr, definition, typevar_class)
+ }
+ Some(KnownClass::ParamSpec) => {
+ self.infer_paramspec(target, call_expr, definition)
+ }
+ Some(_) | None => {
+ self.infer_call_expression_impl(call_expr, callable_type, tcx)
+ }
};
self.store_expression_type(value, ty);
@@ -4371,6 +4481,160 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
target_ty
}
+ fn infer_paramspec(
+ &mut self,
+ target: &ast::Expr,
+ call_expr: &ast::ExprCall,
+ definition: Definition<'db>,
+ ) -> Type<'db> {
+ fn error<'db>(
+ context: &InferContext<'db, '_>,
+ message: impl std::fmt::Display,
+ node: impl Ranged,
+ ) -> Type<'db> {
+ if let Some(builder) = context.report_lint(&INVALID_PARAMSPEC, node) {
+ builder.into_diagnostic(message);
+ }
+ // If the call doesn't create a valid paramspec, we'll emit diagnostics and fall back to
+ // just creating a regular instance of `typing.ParamSpec`.
+ KnownClass::ParamSpec.to_instance(context.db())
+ }
+
+ let db = self.db();
+ let arguments = &call_expr.arguments;
+ let assume_all_features = self.in_stub();
+ let python_version = Program::get(db).python_version(db);
+ let have_features_from =
+ |version: PythonVersion| assume_all_features || python_version >= version;
+
+ let mut default = None;
+ let mut name_param_ty = None;
+
+ if arguments.args.len() > 1 {
+ return error(
+ &self.context,
+ "`ParamSpec` can only have one positional argument",
+ call_expr,
+ );
+ }
+
+ if let Some(starred) = arguments.args.iter().find(|arg| arg.is_starred_expr()) {
+ return error(
+ &self.context,
+ "Starred arguments are not supported in `ParamSpec` creation",
+ starred,
+ );
+ }
+
+ for kwarg in &arguments.keywords {
+ let Some(identifier) = kwarg.arg.as_ref() else {
+ return error(
+ &self.context,
+ "Starred arguments are not supported in `ParamSpec` creation",
+ kwarg,
+ );
+ };
+ match identifier.id().as_str() {
+ "name" => {
+ // Duplicate keyword argument is a syntax error, so we don't have to check if
+ // `name_param_ty.is_some()` here.
+ if !arguments.args.is_empty() {
+ return error(
+ &self.context,
+ "The `name` parameter of `ParamSpec` can only be provided once",
+ kwarg,
+ );
+ }
+ name_param_ty =
+ Some(self.infer_expression(&kwarg.value, TypeContext::default()));
+ }
+ "bound" | "covariant" | "contravariant" | "infer_variance" => {
+ return error(
+ &self.context,
+ "The variance and bound arguments for `ParamSpec` do not have defined semantics yet",
+ call_expr,
+ );
+ }
+ "default" => {
+ if !have_features_from(PythonVersion::PY313) {
+ // We don't return here; this error is informational since this will error
+ // at runtime, but the user's intent is plain, we may as well respect it.
+ error(
+ &self.context,
+ "The `default` parameter of `typing.ParamSpec` was added in Python 3.13",
+ kwarg,
+ );
+ }
+ default = Some(TypeVarDefaultEvaluation::Lazy);
+ }
+ name => {
+ // We don't return here; this error is informational since this will error
+ // at runtime, but it will likely cause fewer cascading errors if we just
+ // ignore the unknown keyword and still understand as much of the typevar as we
+ // can.
+ error(
+ &self.context,
+ format_args!("Unknown keyword argument `{name}` in `ParamSpec` creation"),
+ kwarg,
+ );
+ self.infer_expression(&kwarg.value, TypeContext::default());
+ }
+ }
+ }
+
+ let Some(name_param_ty) = name_param_ty.or_else(|| {
+ arguments
+ .find_positional(0)
+ .map(|arg| self.infer_expression(arg, TypeContext::default()))
+ }) else {
+ return error(
+ &self.context,
+ "The `name` parameter of `ParamSpec` is required.",
+ call_expr,
+ );
+ };
+
+ let Some(name_param) = name_param_ty.as_string_literal().map(|name| name.value(db)) else {
+ return error(
+ &self.context,
+ "The first argument to `ParamSpec` must be a string literal",
+ call_expr,
+ );
+ };
+
+ let ast::Expr::Name(ast::ExprName {
+ id: target_name, ..
+ }) = target
+ else {
+ return error(
+ &self.context,
+ "A `ParamSpec` definition must be a simple variable assignment",
+ target,
+ );
+ };
+
+ if name_param != target_name {
+ return error(
+ &self.context,
+ format_args!(
+ "The name of a `ParamSpec` (`{name_param}`) must match \
+ the name of the variable it is assigned to (`{target_name}`)"
+ ),
+ target,
+ );
+ }
+
+ if default.is_some() {
+ self.deferred.insert(definition);
+ }
+
+ let identity =
+ TypeVarIdentity::new(db, target_name, Some(definition), TypeVarKind::ParamSpec);
+ Type::KnownInstance(KnownInstanceType::TypeVar(TypeVarInstance::new(
+ db, identity, None, None, default,
+ )))
+ }
+
fn infer_legacy_typevar(
&mut self,
target: &ast::Expr,
@@ -4617,8 +4881,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
fn infer_assignment_deferred(&mut self, value: &ast::Expr) {
- // Infer deferred bounds/constraints/defaults of a legacy TypeVar.
- let ast::Expr::Call(ast::ExprCall { arguments, .. }) = value else {
+ // Infer deferred bounds/constraints/defaults of a legacy TypeVar / ParamSpec.
+ let ast::Expr::Call(ast::ExprCall {
+ func, arguments, ..
+ }) = value
+ else {
return;
};
for arg in arguments.args.iter().skip(1) {
@@ -4628,7 +4895,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_expression(&bound.value);
}
if let Some(default) = arguments.find_keyword("default") {
- self.infer_type_expression(&default.value);
+ let func_ty = self.get_or_infer_expression(func, TypeContext::default());
+ if func_ty.as_class_literal().is_some_and(|class_literal| {
+ class_literal.is_known(self.db(), KnownClass::ParamSpec)
+ }) {
+ self.infer_paramspec_default(&default.value);
+ } else {
+ self.infer_type_expression(&default.value);
+ }
}
}
@@ -7047,22 +7321,33 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.to_class_type(self.db())
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
{
- if matches!(
- class.known(self.db()),
- Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar)
- ) {
- // Inference of correctly-placed `TypeVar` definitions is done in
- // `TypeInferenceBuilder::infer_legacy_typevar`, and doesn't use the full
- // call-binding machinery. If we reach here, it means that someone is trying to
- // instantiate a `typing.TypeVar` in an invalid context.
- if let Some(builder) = self
- .context
- .report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
- {
- builder.into_diagnostic(
- "A `TypeVar` definition must be a simple variable assignment",
- );
+ // Inference of correctly-placed `TypeVar` and `ParamSpec` definitions is done in
+ // `TypeInferenceBuilder::infer_legacy_typevar` and
+ // `TypeInferenceBuilder::infer_paramspec`, and doesn't use the full
+ // call-binding machinery. If we reach here, it means that someone is trying to
+ // instantiate a `typing.TypeVar` and `typing.ParamSpec` in an invalid context.
+ match class.known(self.db()) {
+ Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar) => {
+ if let Some(builder) = self
+ .context
+ .report_lint(&INVALID_LEGACY_TYPE_VARIABLE, call_expression)
+ {
+ builder.into_diagnostic(
+ "A `TypeVar` definition must be a simple variable assignment",
+ );
+ }
}
+ Some(KnownClass::ParamSpec) => {
+ if let Some(builder) = self
+ .context
+ .report_lint(&INVALID_PARAMSPEC, call_expression)
+ {
+ builder.into_diagnostic(
+ "A `ParamSpec` definition must be a simple variable assignment",
+ );
+ }
+ }
+ _ => {}
}
let db = self.db();
@@ -8270,10 +8555,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(
todo @ Type::Dynamic(
- DynamicType::Todo(_)
- | DynamicType::TodoPEP695ParamSpec
- | DynamicType::TodoUnpack
- | DynamicType::TodoTypeAlias,
+ DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias,
),
_,
_,
@@ -8281,10 +8563,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| (
_,
todo @ Type::Dynamic(
- DynamicType::Todo(_)
- | DynamicType::TodoPEP695ParamSpec
- | DynamicType::TodoUnpack
- | DynamicType::TodoTypeAlias,
+ DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoTypeAlias,
),
_,
) => Some(todo),
diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
index 1e1ff82c0b..c6b2bbbef0 100644
--- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs
@@ -1524,7 +1524,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.db(),
self.infer_name_load(name),
&|ty| match ty {
- Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => true,
+ Type::KnownInstance(known_instance) => {
+ known_instance.class(self.db()) == KnownClass::ParamSpec
+ }
Type::NominalInstance(nominal) => {
nominal.has_known_class(self.db(), KnownClass::ParamSpec)
}
diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs
index e45e0c9ba5..f6797f87d9 100644
--- a/crates/ty_python_semantic/src/types/type_ordering.rs
+++ b/crates/ty_python_semantic/src/types/type_ordering.rs
@@ -262,9 +262,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
#[cfg(not(debug_assertions))]
(DynamicType::Todo(TodoType), DynamicType::Todo(TodoType)) => Ordering::Equal,
- (DynamicType::TodoPEP695ParamSpec, _) => Ordering::Less,
- (_, DynamicType::TodoPEP695ParamSpec) => Ordering::Greater,
-
(DynamicType::TodoUnpack, _) => Ordering::Less,
(_, DynamicType::TodoUnpack) => Ordering::Greater,
diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
index ba3b75028c..7373c4cf25 100644
--- a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
+++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap
@@ -61,6 +61,7 @@ Settings: Settings {
"invalid-named-tuple": Error (Default),
"invalid-overload": Error (Default),
"invalid-parameter-default": Error (Default),
+ "invalid-paramspec": Error (Default),
"invalid-protocol": Error (Default),
"invalid-raise": Error (Default),
"invalid-return-type": Error (Default),
diff --git a/ty.schema.json b/ty.schema.json
index 55d5bdf996..cae55e4a1b 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -643,6 +643,16 @@
}
]
},
+ "invalid-paramspec": {
+ "title": "detects invalid ParamSpec usage",
+ "description": "## What it does\nChecks for the creation of invalid `ParamSpec`s\n\n## Why is this bad?\nThere are several requirements that you must follow when creating a `ParamSpec`.\n\n## Examples\n```python\nfrom typing import ParamSpec\n\nP1 = ParamSpec(\"P1\") # okay\nP2 = ParamSpec(\"S2\") # error: ParamSpec name must match the variable it's assigned to\n```\n\n## References\n- [Typing spec: ParamSpec](https://typing.python.org/en/latest/spec/generics.html#paramspec)",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"invalid-protocol": {
"title": "detects invalid protocol class definitions",
"description": "## What it does\nChecks for protocol classes that will raise `TypeError` at runtime.\n\n## Why is this bad?\nAn invalidly defined protocol class may lead to the type checker inferring\nunexpected things. It may also lead to `TypeError`s at runtime.\n\n## Examples\nA `Protocol` class cannot inherit from a non-`Protocol` class;\nthis raises a `TypeError` at runtime:\n\n```pycon\n>>> from typing import Protocol\n>>> class Foo(int, Protocol): ...\n...\nTraceback (most recent call last):\n File \"\", line 1, in \n class Foo(int, Protocol): ...\nTypeError: Protocols can only inherit from other protocols, got \n```",