diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md index 49399f9c5a..201ce8d0e2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -102,6 +102,38 @@ Other values are invalid. 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 Stubs natively support forward references, so patterns that would raise `NameError` at runtime are diff --git a/crates/ty_python_semantic/resources/mdtest/regression/paramspec_on_python39.md b/crates/ty_python_semantic/resources/mdtest/regression/paramspec_on_python39.md new file mode 100644 index 0000000000..1760669cf0 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/regression/paramspec_on_python39.md @@ -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 +``` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 514b312313..855e8922a0 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -4168,6 +4168,8 @@ pub enum KnownClass { SpecialForm, TypeVar, ParamSpec, + // typing_extensions.ParamSpec + ExtensionsParamSpec, // must be distinct from typing.ParamSpec, backports new features ParamSpecArgs, ParamSpecKwargs, ProtocolMeta, @@ -4239,6 +4241,7 @@ impl KnownClass { | Self::TypeVar | Self::ExtensionsTypeVar | Self::ParamSpec + | Self::ExtensionsParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple @@ -4371,6 +4374,7 @@ impl KnownClass { | KnownClass::TypeVar | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs | KnownClass::TypeVarTuple @@ -4457,6 +4461,7 @@ impl KnownClass { | KnownClass::TypeVar | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs | KnownClass::TypeVarTuple @@ -4543,6 +4548,7 @@ impl KnownClass { | KnownClass::TypeVar | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs | KnownClass::TypeVarTuple @@ -4634,6 +4640,7 @@ impl KnownClass { | Self::TypeVar | Self::ExtensionsTypeVar | Self::ParamSpec + | Self::ExtensionsParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple @@ -4733,6 +4740,7 @@ impl KnownClass { | KnownClass::TypeVar | KnownClass::ExtensionsTypeVar | KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs | KnownClass::ProtocolMeta @@ -4806,6 +4814,7 @@ impl KnownClass { Self::TypeVar => "TypeVar", Self::ExtensionsTypeVar => "TypeVar", Self::ParamSpec => "ParamSpec", + Self::ExtensionsParamSpec => "ParamSpec", Self::ParamSpecArgs => "ParamSpecArgs", Self::ParamSpecKwargs => "ParamSpecKwargs", Self::TypeVarTuple => "TypeVarTuple", @@ -5139,11 +5148,18 @@ impl KnownClass { Self::TypeAliasType | Self::ExtensionsTypeVar | Self::TypeVarTuple - | Self::ParamSpec + | Self::ExtensionsParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::Deprecated | Self::NewType => KnownModule::TypingExtensions, + Self::ParamSpec => { + if Program::get(db).python_version(db) >= PythonVersion::PY310 { + KnownModule::Typing + } else { + KnownModule::TypingExtensions + } + } Self::NoDefaultType => { let python_version = Program::get(db).python_version(db); @@ -5247,6 +5263,7 @@ impl KnownClass { | Self::TypeVar | Self::ExtensionsTypeVar | Self::ParamSpec + | Self::ExtensionsParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple @@ -5337,6 +5354,7 @@ impl KnownClass { | Self::TypeVar | Self::ExtensionsTypeVar | Self::ParamSpec + | Self::ExtensionsParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple @@ -5420,7 +5438,7 @@ impl KnownClass { "Iterable" => &[Self::Iterable], "Iterator" => &[Self::Iterator], "Mapping" => &[Self::Mapping], - "ParamSpec" => &[Self::ParamSpec], + "ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec], "ParamSpecArgs" => &[Self::ParamSpecArgs], "ParamSpecKwargs" => &[Self::ParamSpecKwargs], "TypeVarTuple" => &[Self::TypeVarTuple], @@ -5542,6 +5560,8 @@ impl KnownClass { | Self::TypedDictFallback | Self::TypeVar | Self::ExtensionsTypeVar + | Self::ParamSpec + | Self::ExtensionsParamSpec | Self::NamedTupleLike | Self::ConstraintSet | Self::GenericContext @@ -5555,7 +5575,6 @@ impl KnownClass { | Self::TypeAliasType | Self::NoDefaultType | Self::SupportsIndex - | Self::ParamSpec | Self::ParamSpecArgs | Self::ParamSpecKwargs | Self::TypeVarTuple @@ -5970,6 +5989,7 @@ mod tests { KnownClass::Member | KnownClass::Nonmember | KnownClass::StrEnum => { PythonVersion::PY311 } + KnownClass::ParamSpec => PythonVersion::PY310, _ => PythonVersion::PY37, }; (class, version_added) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6014bb1f3f..b30dac0ac2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -5033,9 +5033,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) => { self.infer_legacy_typevar(target, call_expr, definition, typevar_class) } - Some(KnownClass::ParamSpec) => { - self.infer_paramspec(target, call_expr, definition) - } + Some( + paramspec_class @ (KnownClass::ParamSpec + | KnownClass::ExtensionsParamSpec), + ) => self.infer_legacy_paramspec( + target, + call_expr, + definition, + paramspec_class, + ), Some(KnownClass::NewType) => { self.infer_newtype_expression(target, call_expr, definition) } @@ -5080,11 +5086,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { target_ty } - fn infer_paramspec( + fn infer_legacy_paramspec( &mut self, target: &ast::Expr, call_expr: &ast::ExprCall, definition: Definition<'db>, + known_class: KnownClass, ) -> Type<'db> { fn error<'db>( context: &InferContext<'db, '_>, @@ -5101,7 +5108,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); 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 have_features_from = |version: PythonVersion| assume_all_features || python_version >= version; @@ -5594,7 +5602,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_type_expression(&bound.value); } 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); } else { 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 .context .report_lint(&INVALID_PARAMSPEC, call_expression)