From 8795d9f0cb8818aa02262a92f1baf096cbdee04c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 29 Nov 2025 12:19:39 +0530 Subject: [PATCH] [ty] Split `ParamSpec` mdtests to separate legacy and PEP 695 tests (#21687) ## Summary This is another small refactor for https://github.com/astral-sh/ruff/pull/21445 that splits the single `paramspec.md` into `generics/legacy/paramspec.md` and `generics/pep695/paramspec.md`. ## Test Plan Make sure that all mdtests pass. --- .../mdtest/{ => generics/legacy}/paramspec.md | 58 +---------------- .../mdtest/generics/pep695/paramspec.md | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+), 57 deletions(-) rename crates/ty_python_semantic/resources/mdtest/{ => generics/legacy}/paramspec.md (67%) create mode 100644 crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md similarity index 67% rename from crates/ty_python_semantic/resources/mdtest/paramspec.md rename to crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 254cd9d073..2ce9f14852 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -1,4 +1,4 @@ -# `ParamSpec` +# Legacy `ParamSpec` ## Definition @@ -115,59 +115,3 @@ P = ParamSpec("P", default=[A, B]) class A: ... class B: ... ``` - -### 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/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md new file mode 100644 index 0000000000..62b50b05ef --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -0,0 +1,64 @@ +# PEP 695 `ParamSpec` + +`ParamSpec` was introduced in Python 3.12 while the support for specifying defaults was added in +Python 3.13. + +```toml +[environment] +python-version = "3.13" +``` + +## Definition + +```py +def foo1[**P]() -> None: + reveal_type(P) # revealed: typing.ParamSpec +``` + +## Bounds and constraints + +`ParamSpec`, when defined using the new syntax, does not allow defining bounds or constraints. + +TODO: 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 +``` + + + +## Default + +The default value for a `ParamSpec` can be either a list of types, `...`, or another `ParamSpec`. + +```py +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 +``` + +Other values are invalid. + +```py +# error: [invalid-paramspec] +def foo[**P = int]() -> None: + pass +```