[ty] Avoid diagnostic when `typing_extensions.ParamSpec` uses `default` parameter (#21839)

## Summary

fixes: https://github.com/astral-sh/ty/issues/1798

## Test Plan

Add mdtest.
This commit is contained in:
Dhruv Manilawala 2025-12-08 18:04:30 +05:30 committed by GitHub
parent dfd6ed0524
commit a364195335
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 92 additions and 10 deletions

View File

@ -102,6 +102,38 @@ Other values are invalid.
P4 = ParamSpec("P4", default=int) P4 = ParamSpec("P4", default=int)
``` ```
### `default` parameter in `typing_extensions.ParamSpec`
```toml
[environment]
python-version = "3.12"
```
The `default` parameter to `ParamSpec` is available from `typing_extensions` in Python 3.12 and
earlier.
```py
from typing import ParamSpec
from typing_extensions import ParamSpec as ExtParamSpec
# This shouldn't emit a diagnostic
P1 = ExtParamSpec("P1", default=[int, str])
# But, this should
# error: [invalid-paramspec] "The `default` parameter of `typing.ParamSpec` was added in Python 3.13"
P2 = ParamSpec("P2", default=[int, str])
```
And, it allows the same set of values as `typing.ParamSpec`.
```py
P3 = ExtParamSpec("P3", default=...)
P4 = ExtParamSpec("P4", default=P3)
# error: [invalid-paramspec]
P5 = ExtParamSpec("P5", default=int)
```
### Forward references in stub files ### Forward references in stub files
Stubs natively support forward references, so patterns that would raise `NameError` at runtime are Stubs natively support forward references, so patterns that would raise `NameError` at runtime are

View File

@ -0,0 +1,19 @@
# `ParamSpec` regression on 3.9
```toml
[environment]
python-version = "3.9"
```
This used to panic when run on Python 3.9 because `ParamSpec` was introduced in Python 3.10 and the
diagnostic message for `invalid-exception-caught` expects to construct `typing.ParamSpec`.
```py
# error: [invalid-syntax]
def foo[**P]() -> None:
try:
pass
# error: [invalid-exception-caught] "Invalid object caught in an exception handler: Object has type `typing.ParamSpec`"
except P:
pass
```

View File

@ -4168,6 +4168,8 @@ pub enum KnownClass {
SpecialForm, SpecialForm,
TypeVar, TypeVar,
ParamSpec, ParamSpec,
// typing_extensions.ParamSpec
ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features
ParamSpecArgs, ParamSpecArgs,
ParamSpecKwargs, ParamSpecKwargs,
ProtocolMeta, ProtocolMeta,
@ -4239,6 +4241,7 @@ impl KnownClass {
| Self::TypeVar | Self::TypeVar
| Self::ExtensionsTypeVar | Self::ExtensionsTypeVar
| Self::ParamSpec | Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs | Self::ParamSpecArgs
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::TypeVarTuple | Self::TypeVarTuple
@ -4371,6 +4374,7 @@ impl KnownClass {
| KnownClass::TypeVar | KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar | KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec | KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs | KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs | KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple | KnownClass::TypeVarTuple
@ -4457,6 +4461,7 @@ impl KnownClass {
| KnownClass::TypeVar | KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar | KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec | KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs | KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs | KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple | KnownClass::TypeVarTuple
@ -4543,6 +4548,7 @@ impl KnownClass {
| KnownClass::TypeVar | KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar | KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec | KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs | KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs | KnownClass::ParamSpecKwargs
| KnownClass::TypeVarTuple | KnownClass::TypeVarTuple
@ -4634,6 +4640,7 @@ impl KnownClass {
| Self::TypeVar | Self::TypeVar
| Self::ExtensionsTypeVar | Self::ExtensionsTypeVar
| Self::ParamSpec | Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs | Self::ParamSpecArgs
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::TypeVarTuple | Self::TypeVarTuple
@ -4733,6 +4740,7 @@ impl KnownClass {
| KnownClass::TypeVar | KnownClass::TypeVar
| KnownClass::ExtensionsTypeVar | KnownClass::ExtensionsTypeVar
| KnownClass::ParamSpec | KnownClass::ParamSpec
| KnownClass::ExtensionsParamSpec
| KnownClass::ParamSpecArgs | KnownClass::ParamSpecArgs
| KnownClass::ParamSpecKwargs | KnownClass::ParamSpecKwargs
| KnownClass::ProtocolMeta | KnownClass::ProtocolMeta
@ -4806,6 +4814,7 @@ impl KnownClass {
Self::TypeVar => "TypeVar", Self::TypeVar => "TypeVar",
Self::ExtensionsTypeVar => "TypeVar", Self::ExtensionsTypeVar => "TypeVar",
Self::ParamSpec => "ParamSpec", Self::ParamSpec => "ParamSpec",
Self::ExtensionsParamSpec => "ParamSpec",
Self::ParamSpecArgs => "ParamSpecArgs", Self::ParamSpecArgs => "ParamSpecArgs",
Self::ParamSpecKwargs => "ParamSpecKwargs", Self::ParamSpecKwargs => "ParamSpecKwargs",
Self::TypeVarTuple => "TypeVarTuple", Self::TypeVarTuple => "TypeVarTuple",
@ -5139,11 +5148,18 @@ impl KnownClass {
Self::TypeAliasType Self::TypeAliasType
| Self::ExtensionsTypeVar | Self::ExtensionsTypeVar
| Self::TypeVarTuple | Self::TypeVarTuple
| Self::ParamSpec | Self::ExtensionsParamSpec
| Self::ParamSpecArgs | Self::ParamSpecArgs
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::Deprecated | Self::Deprecated
| Self::NewType => KnownModule::TypingExtensions, | Self::NewType => KnownModule::TypingExtensions,
Self::ParamSpec => {
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
KnownModule::Typing
} else {
KnownModule::TypingExtensions
}
}
Self::NoDefaultType => { Self::NoDefaultType => {
let python_version = Program::get(db).python_version(db); let python_version = Program::get(db).python_version(db);
@ -5247,6 +5263,7 @@ impl KnownClass {
| Self::TypeVar | Self::TypeVar
| Self::ExtensionsTypeVar | Self::ExtensionsTypeVar
| Self::ParamSpec | Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs | Self::ParamSpecArgs
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::TypeVarTuple | Self::TypeVarTuple
@ -5337,6 +5354,7 @@ impl KnownClass {
| Self::TypeVar | Self::TypeVar
| Self::ExtensionsTypeVar | Self::ExtensionsTypeVar
| Self::ParamSpec | Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::ParamSpecArgs | Self::ParamSpecArgs
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::TypeVarTuple | Self::TypeVarTuple
@ -5420,7 +5438,7 @@ impl KnownClass {
"Iterable" => &[Self::Iterable], "Iterable" => &[Self::Iterable],
"Iterator" => &[Self::Iterator], "Iterator" => &[Self::Iterator],
"Mapping" => &[Self::Mapping], "Mapping" => &[Self::Mapping],
"ParamSpec" => &[Self::ParamSpec], "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec],
"ParamSpecArgs" => &[Self::ParamSpecArgs], "ParamSpecArgs" => &[Self::ParamSpecArgs],
"ParamSpecKwargs" => &[Self::ParamSpecKwargs], "ParamSpecKwargs" => &[Self::ParamSpecKwargs],
"TypeVarTuple" => &[Self::TypeVarTuple], "TypeVarTuple" => &[Self::TypeVarTuple],
@ -5542,6 +5560,8 @@ impl KnownClass {
| Self::TypedDictFallback | Self::TypedDictFallback
| Self::TypeVar | Self::TypeVar
| Self::ExtensionsTypeVar | Self::ExtensionsTypeVar
| Self::ParamSpec
| Self::ExtensionsParamSpec
| Self::NamedTupleLike | Self::NamedTupleLike
| Self::ConstraintSet | Self::ConstraintSet
| Self::GenericContext | Self::GenericContext
@ -5555,7 +5575,6 @@ impl KnownClass {
| Self::TypeAliasType | Self::TypeAliasType
| Self::NoDefaultType | Self::NoDefaultType
| Self::SupportsIndex | Self::SupportsIndex
| Self::ParamSpec
| Self::ParamSpecArgs | Self::ParamSpecArgs
| Self::ParamSpecKwargs | Self::ParamSpecKwargs
| Self::TypeVarTuple | Self::TypeVarTuple
@ -5970,6 +5989,7 @@ mod tests {
KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => {
PythonVersion::PY311 PythonVersion::PY311
} }
KnownClass::ParamSpec => PythonVersion::PY310,
_ => PythonVersion::PY37, _ => PythonVersion::PY37,
}; };
(class, version_added) (class, version_added)

View File

@ -5033,9 +5033,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
) => { ) => {
self.infer_legacy_typevar(target, call_expr, definition, typevar_class) self.infer_legacy_typevar(target, call_expr, definition, typevar_class)
} }
Some(KnownClass::ParamSpec) => { Some(
self.infer_paramspec(target, call_expr, definition) paramspec_class @ (KnownClass::ParamSpec
} | KnownClass::ExtensionsParamSpec),
) => self.infer_legacy_paramspec(
target,
call_expr,
definition,
paramspec_class,
),
Some(KnownClass::NewType) => { Some(KnownClass::NewType) => {
self.infer_newtype_expression(target, call_expr, definition) self.infer_newtype_expression(target, call_expr, definition)
} }
@ -5080,11 +5086,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
target_ty target_ty
} }
fn infer_paramspec( fn infer_legacy_paramspec(
&mut self, &mut self,
target: &ast::Expr, target: &ast::Expr,
call_expr: &ast::ExprCall, call_expr: &ast::ExprCall,
definition: Definition<'db>, definition: Definition<'db>,
known_class: KnownClass,
) -> Type<'db> { ) -> Type<'db> {
fn error<'db>( fn error<'db>(
context: &InferContext<'db, '_>, context: &InferContext<'db, '_>,
@ -5101,7 +5108,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let db = self.db(); let db = self.db();
let arguments = &call_expr.arguments; let arguments = &call_expr.arguments;
let assume_all_features = self.in_stub(); let is_typing_extensions = known_class == KnownClass::ExtensionsParamSpec;
let assume_all_features = self.in_stub() || is_typing_extensions;
let python_version = Program::get(db).python_version(db); let python_version = Program::get(db).python_version(db);
let have_features_from = let have_features_from =
|version: PythonVersion| assume_all_features || python_version >= version; |version: PythonVersion| assume_all_features || python_version >= version;
@ -5594,7 +5602,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_type_expression(&bound.value); self.infer_type_expression(&bound.value);
} }
if let Some(default) = arguments.find_keyword("default") { if let Some(default) = arguments.find_keyword("default") {
if let Some(KnownClass::ParamSpec) = known_class { if matches!(
known_class,
Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec)
) {
self.infer_paramspec_default(&default.value); self.infer_paramspec_default(&default.value);
} else { } else {
self.infer_type_expression(&default.value); self.infer_type_expression(&default.value);
@ -8440,7 +8451,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
); );
} }
} }
Some(KnownClass::ParamSpec) => { Some(KnownClass::ParamSpec | KnownClass::ExtensionsParamSpec) => {
if let Some(builder) = self if let Some(builder) = self
.context .context
.report_lint(&INVALID_PARAMSPEC, call_expression) .report_lint(&INVALID_PARAMSPEC, call_expression)