From cd1d906ffa93f127f9c99e6078a045cd925b43a9 Mon Sep 17 00:00:00 2001 From: David Peter Date: Sat, 10 May 2025 11:59:25 +0200 Subject: [PATCH] [ty] Silence false positives for PEP-695 ParamSpec annotations (#18001) ## Summary Suppress false positives for uses of PEP-695 `ParamSpec` in `Callable` annotations: ```py from typing_extensions import Callable def f[**P](c: Callable[P, int]): pass ``` addresses a comment here: https://github.com/astral-sh/ty/issues/157#issuecomment-2859284721 ## Test Plan Adapted Markdown tests --- .../resources/mdtest/annotations/callable.md | 8 ++++--- crates/ty_python_semantic/src/types.rs | 16 ++++++++++++-- .../src/types/class_base.rs | 2 +- crates/ty_python_semantic/src/types/infer.rs | 21 +++++++++++++++---- .../src/types/type_ordering.rs | 3 +++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index 1fe2aa65d5..dc27fdb7d4 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -249,10 +249,12 @@ Using a `ParamSpec` in a `Callable` annotation: ```py from typing_extensions import Callable -# TODO: Not an error; remove once `ParamSpec` is supported -# error: [invalid-type-form] def _[**P1](c: Callable[P1, int]): - reveal_type(c) # revealed: (...) -> Unknown + reveal_type(P1.args) # revealed: @Todo(ParamSpec) + reveal_type(P1.kwargs) # revealed: @Todo(ParamSpec) + + # TODO: Signature should be (**P1) -> int + reveal_type(c) # revealed: (...) -> int ``` And, using the legacy syntax: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index dcc0932e9a..ed62c97ca2 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -662,7 +662,7 @@ impl<'db> Type<'db> { pub fn contains_todo(&self, db: &'db dyn Db) -> bool { match self { - Self::Dynamic(DynamicType::Todo(_)) => true, + Self::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec) => true, Self::AlwaysFalsy | Self::AlwaysTruthy @@ -703,7 +703,9 @@ impl<'db> Type<'db> { } Self::SubclassOf(subclass_of) => match subclass_of.subclass_of() { - SubclassOfInner::Dynamic(DynamicType::Todo(_)) => true, + SubclassOfInner::Dynamic( + DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec, + ) => true, SubclassOfInner::Dynamic(DynamicType::Unknown | DynamicType::Any) => false, SubclassOfInner::Class(_) => false, }, @@ -5502,6 +5504,9 @@ pub enum DynamicType { /// /// 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, } impl std::fmt::Display for DynamicType { @@ -5512,6 +5517,13 @@ 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") + } + } } } } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 551f47dafb..d5b4e440d7 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -76,7 +76,7 @@ impl<'db> ClassBase<'db> { ClassBase::Class(class) => class.name(db), ClassBase::Dynamic(DynamicType::Any) => "Any", ClassBase::Dynamic(DynamicType::Unknown) => "Unknown", - ClassBase::Dynamic(DynamicType::Todo(_)) => "@Todo", + ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec) => "@Todo", ClassBase::Protocol(_) => "Protocol", ClassBase::Generic(_) => "Generic", } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 2e3fa5b721..e13aa08605 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -2612,7 +2612,7 @@ impl<'db> TypeInferenceBuilder<'db> { default, } = node; self.infer_optional_expression(default.as_deref()); - let pep_695_todo = todo_type!("PEP-695 ParamSpec definition types"); + let pep_695_todo = Type::Dynamic(DynamicType::TodoPEP695ParamSpec); self.add_declaration_with_binding( node.into(), definition, @@ -5797,8 +5797,16 @@ impl<'db> TypeInferenceBuilder<'db> { | (_, any @ Type::Dynamic(DynamicType::Any), _) => Some(any), (unknown @ Type::Dynamic(DynamicType::Unknown), _, _) | (_, unknown @ Type::Dynamic(DynamicType::Unknown), _) => Some(unknown), - (todo @ Type::Dynamic(DynamicType::Todo(_)), _, _) - | (_, todo @ Type::Dynamic(DynamicType::Todo(_)), _) => Some(todo), + ( + todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec), + _, + _, + ) + | ( + _, + todo @ Type::Dynamic(DynamicType::Todo(_) | DynamicType::TodoPEP695ParamSpec), + _, + ) => Some(todo), (Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never), (Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some( @@ -8651,9 +8659,14 @@ impl<'db> TypeInferenceBuilder<'db> { // `Callable[]`. return None; } + ast::Expr::Name(name) + if self.infer_name_load(name) + == Type::Dynamic(DynamicType::TodoPEP695ParamSpec) => + { + return Some(Parameters::todo()); + } _ => { if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, parameters) { - // TODO: Check whether `Expr::Name` is a ParamSpec builder.into_diagnostic(format_args!( "The first argument to `Callable` \ must be either a list of types, \ diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 2099badb0d..4849c36aa9 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -386,5 +386,8 @@ 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, } }