From 888a22e84972981ed1b9f79c00c27b259589e9ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 4 Sep 2025 23:34:37 +0100 Subject: [PATCH] [ty] Reduce false positives for `ParamSpec`s and `TypeVarTuple`s (#20239) --- .../annotations/unsupported_special_forms.md | 17 ++++++ crates/ty_python_semantic/src/types/infer.rs | 56 +++++++++---------- 2 files changed, 44 insertions(+), 29 deletions(-) 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 3b10945df9..8a6f499655 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 @@ -28,6 +28,23 @@ def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P. class Foo: def method(self, x: Self): reveal_type(x) # revealed: Self@method + +def ex2(msg: str): + def wrapper(fn: Callable[P, R_co]) -> Callable[P, R_co]: + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R_co: + print(msg) + return fn(*args, **kwargs) + return wrapped + return wrapper + +def ex3(msg: str): + P = ParamSpec("P") + def wrapper(fn: Callable[P, R_co]) -> Callable[P, R_co]: + def wrapped(*args: P.args, **kwargs: P.kwargs) -> R_co: + print(msg) + return fn(*args, **kwargs) + return wrapped + return wrapper ``` ## Type expressions diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 83c974ff4f..8a19e88624 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -125,6 +125,7 @@ use crate::types::typed_dict::{ validate_typed_dict_key_assignment, }; use crate::types::unpacker::{UnpackResult, Unpacker}; +use crate::types::visitor::any_over_type; use crate::types::{ CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, @@ -9271,26 +9272,27 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let typevars: Result, GenericContextError> = typevars .iter() - .map(|typevar| match typevar { - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => bind_typevar( - self.db(), - self.module(), - self.index, - self.scope().file_scope_id(self.db()), - self.typevar_binding_context, - *typevar, - ) - .ok_or(GenericContextError::InvalidArgument), - Type::Dynamic(DynamicType::TodoUnpack) => Err(GenericContextError::NotYetSupported), - Type::NominalInstance(nominal) - if matches!( + .map(|typevar| { + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = typevar { + bind_typevar( + self.db(), + self.module(), + self.index, + self.scope().file_scope_id(self.db()), + self.typevar_binding_context, + *typevar, + ) + .ok_or(GenericContextError::InvalidArgument) + } else if any_over_type(self.db(), *typevar, &|ty| match ty { + Type::Dynamic(DynamicType::TodoUnpack) => true, + Type::NominalInstance(nominal) => matches!( nominal.class(self.db()).known(self.db()), Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec) - ) => - { + ), + _ => false, + }) { Err(GenericContextError::NotYetSupported) - } - _ => { + } else { if let Some(builder) = self.context.report_lint(&INVALID_ARGUMENT_TYPE, value_node) { @@ -11265,18 +11267,14 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `Callable[]`. return None; } - match self.infer_name_load(name) { - Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => { - return Some(Parameters::todo()); - } - Type::NominalInstance(nominal) - if nominal - .class(self.db()) - .is_known(self.db(), KnownClass::ParamSpec) => - { - return Some(Parameters::todo()); - } - _ => {} + if any_over_type(self.db(), self.infer_name_load(name), &|ty| match ty { + Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => true, + Type::NominalInstance(nominal) => nominal + .class(self.db()) + .is_known(self.db(), KnownClass::ParamSpec), + _ => false, + }) { + return Some(Parameters::todo()); } } _ => {}