From 056258c767f8cf42eade8c23860cc63e51ee977c Mon Sep 17 00:00:00 2001 From: Douglas Creager Date: Fri, 5 Dec 2025 15:37:44 -0500 Subject: [PATCH] cs assignability for paramspecs --- .../mdtest/generics/pep695/paramspec.md | 4 +- .../src/types/signatures.rs | 66 +++++++++++++++++-- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 6483428bb3..f3d63d1689 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -392,15 +392,13 @@ def f3(x: int, y: str) -> int: return 1 # TODO: This should reveal `(x: int, y: str) -> bool` but there's a cycle: https://github.com/astral-sh/ty/issues/1729 -reveal_type(f3) # revealed: ((x: int, y: str) -> bool) | ((x: Divergent, y: Divergent) -> bool) +reveal_type(f3) # revealed: ((x: int, y: str) -> bool) | ((...) -> bool) reveal_type(f3(1, "a")) # revealed: bool reveal_type(f3(x=1, y="a")) # revealed: bool reveal_type(f3(1, y="a")) # revealed: bool reveal_type(f3(y="a", x=1)) # revealed: bool -# TODO: There should only be one error but the type of `f3` is a union: https://github.com/astral-sh/ty/issues/1729 -# error: [missing-argument] "No argument provided for required parameter `y`" # error: [missing-argument] "No argument provided for required parameter `y`" f3(1) # error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["a"]`" diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 180a26efd6..42818f3012 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -29,10 +29,10 @@ use crate::types::generics::{ }; use crate::types::infer::nearest_enclosing_class; use crate::types::{ - ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableTypeKind, ClassLiteral, - FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, - KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, - TypeMapping, TypeRelation, VarianceInferable, todo_type, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, CallableTypeKind, + ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, + IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, + TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -1145,6 +1145,57 @@ impl<'db> Signature<'db> { return result; } + // If either parameter list is a ParamSpec, then we can create a constraint set that holds + // when the ParamSpec is assignable to the other parameter list. + if relation.is_constraint_set_assignability() { + match ( + self.parameters.as_paramspec(), + other.parameters.as_paramspec(), + ) { + (Some(self_bound_typevar), Some(other_bound_typevar)) => { + return ConstraintSet::constrain_typevar( + db, + self_bound_typevar, + Type::TypeVar(other_bound_typevar), + Type::TypeVar(other_bound_typevar), + relation, + ); + } + + (Some(self_bound_typevar), None) => { + let upper = Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new(other.parameters.clone(), None)), + CallableTypeKind::ParamSpecValue, + )); + return ConstraintSet::constrain_typevar( + db, + self_bound_typevar, + Type::Never, + upper, + relation, + ); + } + + (None, Some(other_bound_typevar)) => { + let lower = Type::Callable(CallableType::new( + db, + CallableSignature::single(Signature::new(self.parameters.clone(), None)), + CallableTypeKind::ParamSpecValue, + )); + return ConstraintSet::constrain_typevar( + db, + other_bound_typevar, + lower, + Type::object(), + relation, + ); + } + + (None, None) => {} + } + } + let mut parameters = ParametersZip { current_self: None, current_other: None, @@ -1562,6 +1613,13 @@ impl<'db> Parameters<'db> { matches!(self.kind, ParametersKind::Gradual) } + pub(crate) const fn as_paramspec(&self) -> Option> { + match self.kind { + ParametersKind::ParamSpec(bound_typevar) => Some(bound_typevar), + _ => None, + } + } + /// Return todo parameters: (*args: Todo, **kwargs: Todo) pub(crate) fn todo() -> Self { Self {