From 97dd83daabd8ebffa32d7870c9b9a7cf1e0c2730 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 14 Nov 2025 13:52:20 +0530 Subject: [PATCH 01/59] Add support for `P.args` and `P.kwargs` --- crates/ty_python_semantic/src/types.rs | 73 +++++++-- .../ty_python_semantic/src/types/display.rs | 97 +++++++----- .../ty_python_semantic/src/types/generics.rs | 6 +- .../src/types/infer/builder.rs | 139 +++++++++++++++--- .../infer/builder/annotation_expression.rs | 15 +- .../types/infer/builder/type_expression.rs | 37 ++--- .../src/types/signatures.rs | 123 +++++++++++++--- 7 files changed, 372 insertions(+), 118 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b7f4327ad6ae7..21e97c64d5069 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4466,11 +4466,16 @@ impl<'db> Type<'db> { .into() } - Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) + Type::TypeVar(typevar) if typevar.kind(db).is_paramspec() && matches!(name.as_str(), "args" | "kwargs") => { - Place::bound(todo_type!("ParamSpecArgs / ParamSpecKwargs")).into() + Place::bound(Type::TypeVar(match name.as_str() { + "args" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), + "kwargs" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), + _ => unreachable!(), + })) + .into() } Type::NominalInstance(instance) @@ -6556,6 +6561,15 @@ impl<'db> Type<'db> { KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::TypeVar(typevar) => { + // A `ParamSpec` type variable cannot be used in type expressions. + if typevar.kind(db).is_paramspec() { + return Err(InvalidTypeExpressionError { + invalid_expressions: smallvec::smallvec_inline![ + InvalidTypeExpression::InvalidType(*self, scope_id) + ], + fallback_type: Type::unknown(), + }); + } let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( db, @@ -6775,9 +6789,12 @@ impl<'db> Type<'db> { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), - Some( - KnownClass::ParamSpec | KnownClass::ParamSpecArgs | KnownClass::ParamSpecKwargs, - ) => Ok(todo_type!("Support for `typing.ParamSpec`")), + Some(KnownClass::ParamSpecArgs) => { + Ok(todo_type!("Support for `typing.ParamSpecArgs`")) + } + Some(KnownClass::ParamSpecKwargs) => { + Ok(todo_type!("Support for `typing.ParamSpecKwargs`")) + } Some(KnownClass::TypeVarTuple) => Ok(todo_type!( "Support for `typing.TypeVarTuple` instances in type expressions" )), @@ -6996,7 +7013,7 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) => match type_mapping { TypeMapping::BindLegacyTypevars(binding_context) => { - Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context)) + Type::TypeVar(BoundTypeVarInstance::new(db, typevar, *binding_context, None)) } TypeMapping::Specialization(_) | TypeMapping::PartialSpecialization(_) | @@ -7801,6 +7818,8 @@ pub struct TrackedConstraintSet<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for TrackedConstraintSet<'_> {} +// TODO: The origin is either `TypeVarInstance` or `BoundTypeVarInstance` + /// Singleton types that are heavily special-cased by ty. Despite its name, /// quite a different type to [`NominalInstanceType`]. /// @@ -8593,12 +8612,12 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( #[salsa::tracked] impl<'db> TypeVarInstance<'db> { - pub(crate) fn with_binding_context( + pub(crate) fn as_bound_type_var_instance( self, db: &'db dyn Db, binding_context: Definition<'db>, ) -> BoundTypeVarInstance<'db> { - BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context)) + BoundTypeVarInstance::new(db, self, BindingContext::Definition(binding_context), None) } pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { @@ -8904,6 +8923,21 @@ impl<'db> BindingContext<'db> { } } +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, get_size2::GetSize)] +pub enum ParamSpecAttrKind { + Args, + Kwargs, +} + +impl std::fmt::Display for ParamSpecAttrKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParamSpecAttrKind::Args => f.write_str("args"), + ParamSpecAttrKind::Kwargs => f.write_str("kwargs"), + } + } +} + /// The identity of a bound type variable. /// /// This identifies a specific binding of a typevar to a context (e.g., `T@ClassC` vs `T@FunctionF`), @@ -8916,14 +8950,17 @@ impl<'db> BindingContext<'db> { pub struct BoundTypeVarIdentity<'db> { pub(crate) identity: TypeVarIdentity<'db>, pub(crate) binding_context: BindingContext<'db>, + paramspec_attr: Option, } /// A type variable that has been bound to a generic context, and which can be specialized to a /// concrete type. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +#[derive(PartialOrd, Ord)] pub struct BoundTypeVarInstance<'db> { pub typevar: TypeVarInstance<'db>, binding_context: BindingContext<'db>, + paramspec_attr: Option, } // The Salsa heap is tracked separately. @@ -8938,9 +8975,22 @@ impl<'db> BoundTypeVarInstance<'db> { BoundTypeVarIdentity { identity: self.typevar(db).identity(db), binding_context: self.binding_context(db), + paramspec_attr: self.paramspec_attr(db), } } + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { + self.typevar(db).name(db) + } + + pub(crate) fn kind(self, db: &'db dyn Db) -> TypeVarKind { + self.typevar(db).kind(db) + } + + pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { + Self::new(db, self.typevar(db), self.binding_context(db), Some(kind)) + } + /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. /// differences in their bounds or constraints due to materialization. pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { @@ -8967,7 +9017,7 @@ impl<'db> BoundTypeVarInstance<'db> { Some(variance), None, // _default ); - Self::new(db, typevar, BindingContext::Synthetic) + Self::new(db, typevar, BindingContext::Synthetic, None) } /// Create a new synthetic `Self` type variable with the given upper bound. @@ -8989,7 +9039,7 @@ impl<'db> BoundTypeVarInstance<'db> { Some(TypeVarVariance::Invariant), None, // _default ); - Self::new(db, typevar, binding_context) + Self::new(db, typevar, binding_context, None) } pub(crate) fn variance_with_polarity( @@ -9065,6 +9115,7 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).normalized_impl(db, visitor), self.binding_context(db), + self.paramspec_attr(db), ) } @@ -9079,6 +9130,7 @@ impl<'db> BoundTypeVarInstance<'db> { self.typevar(db) .materialize_impl(db, materialization_kind, visitor), self.binding_context(db), + self.paramspec_attr(db), ) } @@ -9087,6 +9139,7 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).to_instance(db)?, self.binding_context(db), + self.paramspec_attr(db), )) } } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index b8a8a05ac4ca0..9bd0e2fdf5788 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -20,7 +20,9 @@ use crate::semantic_index::{scope::ScopeKind, semantic_index}; use crate::types::class::{ClassLiteral, ClassType, GenericAlias}; use crate::types::function::{FunctionType, OverloadLiteral}; use crate::types::generics::{GenericContext, Specialization}; -use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signature}; +use crate::types::signatures::{ + CallableSignature, Parameter, Parameters, ParametersKind, Signature, +}; use crate::types::tuple::TupleSpec; use crate::types::visitor::TypeVisitor; use crate::types::{ @@ -643,6 +645,9 @@ impl Display for DisplayBoundTypeVarIdentity<'_> { if let Some(binding_context) = self.bound_typevar_identity.binding_context.name(self.db) { write!(f, "@{binding_context}")?; } + if let Some(paramspec_attr) = self.bound_typevar_identity.paramspec_attr { + write!(f, ".{paramspec_attr}")?; + } Ok(()) } } @@ -1116,57 +1121,69 @@ impl DisplaySignature<'_> { if multiline { writer.write_str("\n ")?; } - if self.parameters.is_gradual() { - // We represent gradual form as `...` in the signature, internally the parameters still - // contain `(*args, **kwargs)` parameters. - writer.write_str("...")?; - } else { - let mut star_added = false; - let mut needs_slash = false; - let mut first = true; - let arg_separator = if multiline { ",\n " } else { ", " }; - - for parameter in self.parameters.as_slice() { - // Handle special separators - if !star_added && parameter.is_keyword_only() { + match self.parameters.kind() { + ParametersKind::Standard => { + let mut star_added = false; + let mut needs_slash = false; + let mut first = true; + let arg_separator = if multiline { ",\n " } else { ", " }; + + for parameter in self.parameters.as_slice() { + // Handle special separators + if !star_added && parameter.is_keyword_only() { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('*')?; + star_added = true; + first = false; + } + if parameter.is_positional_only() { + needs_slash = true; + } else if needs_slash { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('/')?; + needs_slash = false; + first = false; + } + + // Add comma before parameter if not first if !first { writer.write_str(arg_separator)?; } - writer.write_char('*')?; - star_added = true; + + // Write parameter with range tracking + let param_name = parameter.display_name(); + writer.write_parameter( + ¶meter.display_with(self.db, self.settings.singleline()), + param_name.as_deref(), + )?; + first = false; } - if parameter.is_positional_only() { - needs_slash = true; - } else if needs_slash { + + if needs_slash { if !first { writer.write_str(arg_separator)?; } writer.write_char('/')?; - needs_slash = false; - first = false; - } - - // Add comma before parameter if not first - if !first { - writer.write_str(arg_separator)?; } - - // Write parameter with range tracking - let param_name = parameter.display_name(); - writer.write_parameter( - ¶meter.display_with(self.db, self.settings.singleline()), - param_name.as_deref(), - )?; - - first = false; } - - if needs_slash { - if !first { - writer.write_str(arg_separator)?; + ParametersKind::Gradual => { + // We represent gradual form as `...` in the signature, internally the parameters still + // contain `(*args, **kwargs)` parameters. + writer.write_str("...")?; + } + ParametersKind::ParamSpec(origin) => { + writer.write_str(&format!("**{}", origin.name(self.db)))?; + if let Some(name) = origin + .binding_context(self.db) + .and_then(|binding_context| binding_context.name(self.db)) + { + writer.write_str(&format!("@{name}"))?; } - writer.write_char('/')?; } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 8be33c95fc6b5..c4174f0dbe4c5 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -64,7 +64,7 @@ pub(crate) fn bind_typevar<'db>( if outer.kind().is_class() { if let NodeWithScopeKind::Function(function) = inner.node() { let definition = index.expect_single_definition(function); - return Some(typevar.with_binding_context(db, definition)); + return Some(typevar.as_bound_type_var_instance(db, definition)); } } } @@ -73,7 +73,7 @@ pub(crate) fn bind_typevar<'db>( .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar)) .or_else(|| { typevar_binding_context.map(|typevar_binding_context| { - typevar.with_binding_context(db, typevar_binding_context) + typevar.as_bound_type_var_instance(db, typevar_binding_context) }) }) } @@ -358,7 +358,7 @@ impl<'db> GenericContext<'db> { else { return None; }; - Some(typevar.with_binding_context(db, binding_context)) + Some(typevar.as_bound_type_var_instance(db, binding_context)) } // TODO: Support these! ast::TypeParam::ParamSpec(_) => None, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 05c619306079b..ea656b2d76018 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -55,16 +55,16 @@ use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, Meth use crate::types::context::{InNoTypeCheck, InferContext}; use crate::types::cyclic::CycleDetector; use crate::types::diagnostic::{ - CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, CYCLIC_CLASS_DEFINITION, - DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, INVALID_ARGUMENT_TYPE, - INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, - INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, INVALID_METACLASS, - INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, - INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, - INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, - POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, - UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, - UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, + self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS, + CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO, + INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, + INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_LEGACY_TYPE_VARIABLE, + INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, + INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, + INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, + NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, + SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, + UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, @@ -103,11 +103,11 @@ use crate::types::{ CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, - PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, - TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, - TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, - binding_type, todo_type, + PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, SpecialFormType, + SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, + TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, + TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, + UnionType, binding_type, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -2539,7 +2539,40 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { todo_type!("PEP 646") } else { let annotated_type = self.file_expression_type(annotation); - Type::homogeneous_tuple(self.db(), annotated_type) + if let Type::TypeVar(typevar) = annotated_type + && typevar.kind(self.db()).is_paramspec() + { + match typevar.paramspec_attr(self.db()) { + // `*args: P.args` + Some(ParamSpecAttrKind::Args) => annotated_type, + + // `*args: P.kwargs` + Some(ParamSpecAttrKind::Kwargs) => { + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + let name = typevar.name(self.db()); + let mut diag = builder.into_diagnostic(format_args!( + "`{name}.kwargs` is valid only in `**kwargs` annotation", + )); + diag.set_primary_message(format_args!( + "Did you mean `{name}.args`?" + )); + diagnostic::add_type_expression_reference_link(diag); + } + // TODO: Should this be `Unknown` instead? + Type::homogeneous_tuple(self.db(), Type::unknown()) + } + + // `*args: P` + None => { + // TODO: Should this be `Unknown` instead? + Type::homogeneous_tuple(self.db(), Type::unknown()) + } + } + } else { + Type::homogeneous_tuple(self.db(), annotated_type) + } }; self.add_declaration_with_binding( @@ -2622,7 +2655,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { typing_self(db, self.scope(), Some(method_definition), class_literal) } - /// Set initial declared/inferred types for a `*args` variadic positional parameter. + /// Set initial declared/inferred types for a `**kwargs` keyword-variadic parameter. /// /// The annotated type is implicitly wrapped in a string-keyed dictionary. /// @@ -2635,11 +2668,47 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { definition: Definition<'db>, ) { if let Some(annotation) = parameter.annotation() { - let annotated_ty = self.file_expression_type(annotation); - let ty = KnownClass::Dict.to_specialized_instance( - self.db(), - [KnownClass::Str.to_instance(self.db()), annotated_ty], - ); + let annotated_type = self.file_expression_type(annotation); + tracing::debug!("annotated_type: {}", annotated_type.display(self.db())); + let ty = if let Type::TypeVar(typevar) = annotated_type + && typevar.kind(self.db()).is_paramspec() + { + match typevar.paramspec_attr(self.db()) { + // `**kwargs: P.args` + Some(ParamSpecAttrKind::Args) => { + if let Some(builder) = + self.context.report_lint(&INVALID_TYPE_FORM, annotation) + { + let name = typevar.name(self.db()); + let mut diag = builder.into_diagnostic(format_args!( + "`{name}.args` is valid only in `*args` annotation", + )); + diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); + diagnostic::add_type_expression_reference_link(diag); + } + // TODO: Should this be `Unknown` instead? + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ) + } + + // `**kwargs: P.kwargs` + Some(ParamSpecAttrKind::Kwargs) => annotated_type, + + // `**kwargs: P` + // TODO: Should this be `Unknown` instead? + None => KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ), + } + } else { + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), annotated_type], + ) + }; self.add_declaration_with_binding( parameter.into(), definition, @@ -8875,10 +8944,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_attribute_load(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> { let ast::ExprAttribute { value, attr, .. } = attribute; - let value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); + let mut value_type = self.infer_maybe_standalone_expression(value, TypeContext::default()); let db = self.db(); let mut constraint_keys = vec![]; + tracing::debug!( + "value_type for attribute access: {}", + value_type.display(db) + ); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_type + && typevar.kind(db).is_paramspec() + && let Some(bound_typevar) = bind_typevar( + db, + self.index, + self.scope().file_scope_id(db), + self.typevar_binding_context, + typevar, + ) + { + value_type = Type::TypeVar(bound_typevar); + tracing::debug!("updated value_type: {}", value_type.display(db)); + } + let mut assigned_type = None; if let Some(place_expr) = PlaceExpr::try_from_expr(attribute) { let (resolved, keys) = self.infer_place_load( @@ -8890,6 +8977,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assigned_type = Some(ty); } } + tracing::debug!("assigned_type for attribute access: {:?}", assigned_type); let fallback_place = value_type.member(db, &attr.id); // Exclude non-definitely-bound places for purposes of reachability // analysis. We currently do not perform boundness analysis for implicit @@ -8988,6 +9076,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) .inner_type(); + tracing::debug!( + "resolved_type for attribute access: {}", + resolved_type.display(db) + ); + self.check_deprecated(attr, resolved_type); // Even if we can obtain the attribute type based on the assignments, we still perform default type inference diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 5e1f85269572d..f30eab7a8430f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -144,11 +144,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } ast::Expr::Attribute(attribute) => match attribute.ctx { - ast::ExprContext::Load => infer_name_or_attribute( - self.infer_attribute_expression(attribute), - annotation, - self, - ), + ast::ExprContext::Load => { + let attribute_type = self.infer_attribute_expression(attribute); + if let Type::TypeVar(typevar) = attribute_type + && typevar.paramspec_attr(self.db()).is_some() + { + TypeAndQualifiers::declared(attribute_type) + } else { + infer_name_or_attribute(attribute_type, annotation, self) + } + } ast::ExprContext::Invalid => TypeAndQualifiers::declared(Type::unknown()), ast::ExprContext::Store | ast::ExprContext::Del => TypeAndQualifiers::declared( todo_type!("Attribute expression annotation in Store/Del context"), diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 20c5362a0b8b9..c2655548a5936 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -2,14 +2,15 @@ use itertools::Either; use ruff_python_ast as ast; use super::{DeferredExpressionState, TypeInferenceBuilder}; +use crate::semantic_index::semantic_index; use crate::types::diagnostic::{ self, INVALID_TYPE_FORM, NON_SUBSCRIPTABLE, report_invalid_argument_number_to_special_form, report_invalid_arguments_to_callable, }; -use crate::types::signatures::Signature; +use crate::types::generics::bind_typevar; +use crate::types::signatures::{ParamSpecOrigin, Signature}; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; -use crate::types::visitor::any_over_type; use crate::types::{ CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, @@ -1535,21 +1536,23 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // `Callable[]`. return None; } - if any_over_type( - self.db(), - self.infer_name_load(name), - &|ty| match ty { - Type::KnownInstance(known_instance) => { - known_instance.class(self.db()) == KnownClass::ParamSpec - } - Type::NominalInstance(nominal) => { - nominal.has_known_class(self.db(), KnownClass::ParamSpec) - } - _ => false, - }, - true, - ) { - return Some(Parameters::todo()); + let name_ty = self.infer_name_load(name); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = name_ty + && typevar.kind(self.db()).is_paramspec() + { + let index = semantic_index(self.db(), self.scope().file(self.db())); + let origin = bind_typevar( + self.db(), + index, + self.scope().file_scope_id(self.db()), + self.typevar_binding_context, + typevar, + ) + .map_or( + ParamSpecOrigin::Unbounded(typevar), + ParamSpecOrigin::Bounded, + ); + return Some(Parameters::paramspec(self.db(), origin)); } } _ => {} diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 7c48b4c2897b6..a064b3fa74b05 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -29,9 +29,10 @@ use crate::types::generics::{ }; use crate::types::infer::nearest_enclosing_class; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, FindLegacyTypeVarsVisitor, - HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, - NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassLiteral, + FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, + KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, + TypeContext, TypeMapping, TypeRelation, TypeVarInstance, VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -1169,10 +1170,56 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] -pub(crate) struct Parameters<'db> { - // TODO: use SmallVec here once invariance bug is fixed - value: Vec>, +#[derive( + Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, Ord, PartialOrd, get_size2::GetSize, +)] +pub(crate) enum ParamSpecOrigin<'db> { + Bounded(BoundTypeVarInstance<'db>), + Unbounded(TypeVarInstance<'db>), +} + +impl<'db> ParamSpecOrigin<'db> { + pub(crate) fn with_paramspec_attr( + self, + db: &'db dyn Db, + paramspec_attr: ParamSpecAttrKind, + ) -> Self { + match self { + ParamSpecOrigin::Bounded(typevar) => { + ParamSpecOrigin::Bounded(typevar.with_paramspec_attr(db, paramspec_attr)) + } + ParamSpecOrigin::Unbounded(_) => self, + } + } + + pub(crate) fn name(&self, db: &'db dyn Db) -> &ast::name::Name { + match self { + ParamSpecOrigin::Bounded(bound) => bound.typevar(db).name(db), + ParamSpecOrigin::Unbounded(unbound) => unbound.name(db), + } + } + + pub(crate) fn binding_context(&self, db: &'db dyn Db) -> Option> { + match self { + ParamSpecOrigin::Bounded(bound) => Some(bound.binding_context(db)), + ParamSpecOrigin::Unbounded(_) => None, + } + } + + pub(crate) fn into_type(self) -> Type<'db> { + match self { + ParamSpecOrigin::Bounded(bound) => Type::TypeVar(bound), + ParamSpecOrigin::Unbounded(unbound) => { + Type::KnownInstance(KnownInstanceType::TypeVar(unbound)) + } + } + } +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) enum ParametersKind<'db> { + #[default] + Standard, /// Whether this parameter list represents a gradual form using `...` as the only parameter. /// @@ -1193,27 +1240,41 @@ pub(crate) struct Parameters<'db> { /// some adjustments to represent that. /// /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable - is_gradual: bool, + Gradual, + + // TODO: Need to store the name of the paramspec variable for the display implementation. + ParamSpec(ParamSpecOrigin<'db>), +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] +pub(crate) struct Parameters<'db> { + // TODO: use SmallVec here once invariance bug is fixed + value: Vec>, + kind: ParametersKind<'db>, } impl<'db> Parameters<'db> { pub(crate) fn new(parameters: impl IntoIterator>) -> Self { let value: Vec> = parameters.into_iter().collect(); - let is_gradual = value.len() == 2 + let kind = if value.len() == 2 && value .iter() .any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())) && value.iter().any(|p| { p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()) - }); - Self { value, is_gradual } + }) { + ParametersKind::Gradual + } else { + ParametersKind::Standard + }; + Self { value, kind } } /// Create an empty parameter list. pub(crate) fn empty() -> Self { Self { value: Vec::new(), - is_gradual: false, + kind: ParametersKind::Standard, } } @@ -1221,8 +1282,12 @@ impl<'db> Parameters<'db> { self.value.as_slice() } + pub(crate) const fn kind(&self) -> ParametersKind<'db> { + self.kind + } + pub(crate) const fn is_gradual(&self) -> bool { - self.is_gradual + matches!(self.kind, ParametersKind::Gradual) } /// Return todo parameters: (*args: Todo, **kwargs: Todo) @@ -1234,7 +1299,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(todo_type!("todo signature **kwargs")), ], - is_gradual: true, + kind: ParametersKind::Gradual, } } @@ -1251,7 +1316,25 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::Dynamic(DynamicType::Any)), ], - is_gradual: true, + kind: ParametersKind::Gradual, + } + } + + pub(crate) fn paramspec(db: &'db dyn Db, origin: ParamSpecOrigin<'db>) -> Self { + Self { + value: vec![ + Parameter::variadic(Name::new_static("args")).with_annotated_type( + origin + .with_paramspec_attr(db, ParamSpecAttrKind::Args) + .into_type(), + ), + Parameter::keyword_variadic(Name::new_static("kwargs")).with_annotated_type( + origin + .with_paramspec_attr(db, ParamSpecAttrKind::Kwargs) + .into_type(), + ), + ], + kind: ParametersKind::ParamSpec(origin), } } @@ -1269,7 +1352,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::Dynamic(DynamicType::Unknown)), ], - is_gradual: true, + kind: ParametersKind::Gradual, } } @@ -1281,7 +1364,7 @@ impl<'db> Parameters<'db> { Parameter::keyword_variadic(Name::new_static("kwargs")) .with_annotated_type(Type::object()), ], - is_gradual: false, + kind: ParametersKind::Standard, } } @@ -1471,13 +1554,13 @@ impl<'db> Parameters<'db> { // Note that we've already flipped the materialization in Signature.apply_type_mapping_impl(), // so the "top" materialization here is the bottom materialization of the whole Signature. // It might make sense to flip the materialization here instead. - TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual => { + TypeMapping::Materialize(MaterializationKind::Top) if self.is_gradual() => { Parameters::object() } // TODO: This is wrong, the empty Parameters is not a subtype of all materializations. // The bottom materialization is not currently representable and implementing it // properly requires extending the Parameters struct. - TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual => { + TypeMapping::Materialize(MaterializationKind::Bottom) if self.is_gradual() => { Parameters::empty() } _ => Self { @@ -1486,7 +1569,7 @@ impl<'db> Parameters<'db> { .iter() .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) .collect(), - is_gradual: self.is_gradual, + kind: self.kind, }, } } From c32615af370f852b6fb83ff6698f74df8d8b7f5c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 14 Nov 2025 16:04:34 +0530 Subject: [PATCH 02/59] Avoid raising error when `P` is used in invalid context --- crates/ty_python_semantic/src/types.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 21e97c64d5069..0d9719427bbd4 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6561,15 +6561,17 @@ impl<'db> Type<'db> { KnownInstanceType::TypeAliasType(alias) => Ok(Type::TypeAlias(*alias)), KnownInstanceType::NewType(newtype) => Ok(Type::NewTypeInstance(*newtype)), KnownInstanceType::TypeVar(typevar) => { - // A `ParamSpec` type variable cannot be used in type expressions. - if typevar.kind(db).is_paramspec() { - return Err(InvalidTypeExpressionError { - invalid_expressions: smallvec::smallvec_inline![ - InvalidTypeExpression::InvalidType(*self, scope_id) - ], - fallback_type: Type::unknown(), - }); - } + // TODO: A `ParamSpec` type variable cannot be used in type expressions. This + // requires storing additional context as it's allowed in some places + // (`Concatenate`, `Callable`) but not others. + // if typevar.kind(db).is_paramspec() { + // return Err(InvalidTypeExpressionError { + // invalid_expressions: smallvec::smallvec_inline![ + // InvalidTypeExpression::InvalidType(*self, scope_id) + // ], + // fallback_type: Type::unknown(), + // }); + // } let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( db, From ddedf2040745e2da4e5f12b3ec18fa8702d4395f Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 15 Nov 2025 12:27:36 +0530 Subject: [PATCH 03/59] Small docs tweak --- .../ty_python_semantic/src/types/signatures.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a064b3fa74b05..657fe8bf943bc 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1216,15 +1216,17 @@ impl<'db> ParamSpecOrigin<'db> { } } +/// The kind of parameter list represented. +// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number +// of required positional parameters followed by a gradual form. Our representation will need +// some adjustments to represent that. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum ParametersKind<'db> { + /// A standard parameter list. #[default] Standard, - /// Whether this parameter list represents a gradual form using `...` as the only parameter. - /// - /// If this is `true`, the `value` will still contain the variadic and keyword-variadic - /// parameters. + /// Represents a gradual parameter list using `...` as the only parameter. /// /// Per [the typing specification], any signature with a variadic and a keyword-variadic /// argument, both annotated (explicitly or implicitly) as `Any` or `Unknown`, is considered @@ -1235,14 +1237,10 @@ pub(crate) enum ParametersKind<'db> { /// /// Note: This flag can also result from invalid forms of `Callable` annotations. /// - /// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number - /// of required positional parameters followed by a gradual form. Our representation will need - /// some adjustments to represent that. - /// - /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable + /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable Gradual, - // TODO: Need to store the name of the paramspec variable for the display implementation. + /// Represents a `ParamSpec` parameter list. ParamSpec(ParamSpecOrigin<'db>), } From e4229529040d02af10159cf2d5a0371ef4f315cd Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 18 Nov 2025 17:41:58 +0530 Subject: [PATCH 04/59] Update `CallableType` to recode as ParamSpec value --- crates/ty_python_semantic/src/types.rs | 54 +++-- crates/ty_python_semantic/src/types/class.rs | 45 ++-- .../ty_python_semantic/src/types/display.rs | 213 +++++++++++------- .../ty_python_semantic/src/types/function.rs | 6 +- .../src/types/infer/builder.rs | 28 +-- 5 files changed, 214 insertions(+), 132 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 0d9719427bbd4..c03b361287127 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1568,13 +1568,13 @@ impl<'db> Type<'db> { Type::KnownBoundMethod(method) => Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(method.signatures(db)), - false, + CallableTypeKind::Regular, ))), Type::WrapperDescriptor(wrapper_descriptor) => Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(wrapper_descriptor.signatures(db)), - false, + CallableTypeKind::Regular, ))), Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some(CallableType::single( @@ -10565,7 +10565,7 @@ impl<'db> BoundMethodType<'db> { .iter() .map(|signature| signature.bind_self(db, Some(self_instance))), ), - true, + CallableTypeKind::FunctionLike, ) } @@ -10631,6 +10631,20 @@ impl<'db> BoundMethodType<'db> { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)] +pub enum CallableTypeKind { + /// Represents regular callable objects. + Regular, + + /// Represents function-like objects, like the synthesized methods of dataclasses or + /// `NamedTuples`. These callables act like real functions when accessed as attributes on + /// instances, i.e. they bind `self`. + FunctionLike, + + /// Represents the value bound to a `typing.ParamSpec` type variable. + ParamSpecValue, +} + /// This type represents the set of all callable objects with a certain, possibly overloaded, /// signature. /// @@ -10647,10 +10661,7 @@ pub struct CallableType<'db> { #[returns(ref)] pub(crate) signatures: CallableSignature<'db>, - /// We use `CallableType` to represent function-like objects, like the synthesized methods - /// of dataclasses or NamedTuples. These callables act like real functions when accessed - /// as attributes on instances, i.e. they bind `self`. - is_function_like: bool, + kind: CallableTypeKind, } pub(super) fn walk_callable_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -10672,7 +10683,7 @@ impl<'db> CallableType<'db> { Type::Callable(CallableType::new( db, CallableSignature::single(signature), - false, + CallableTypeKind::Regular, )) } @@ -10683,7 +10694,16 @@ impl<'db> CallableType<'db> { Type::Callable(CallableType::new( db, CallableSignature::single(signature), - true, + CallableTypeKind::FunctionLike, + )) + } + + /// Create a callable type which represents the value bound to a `ParamSpec` type variable. + pub(crate) fn paramspec_value(db: &'db dyn Db, signature: Signature<'db>) -> Type<'db> { + Type::Callable(CallableType::new( + db, + CallableSignature::single(signature), + CallableTypeKind::ParamSpecValue, )) } @@ -10693,7 +10713,15 @@ impl<'db> CallableType<'db> { } pub(crate) fn bind_self(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new(db, self.signatures(db).bind_self(db, None), false) + CallableType::new( + db, + self.signatures(db).bind_self(db, None), + CallableTypeKind::Regular, + ) + } + + pub(crate) fn is_function_like(self, db: &'db dyn Db) -> bool { + matches!(self.kind(db), CallableTypeKind::FunctionLike) } /// Create a callable type which represents a fully-static "bottom" callable. @@ -10701,7 +10729,7 @@ impl<'db> CallableType<'db> { /// Specifically, this represents a callable type with a single signature: /// `(*args: object, **kwargs: object) -> Never`. pub(crate) fn bottom(db: &'db dyn Db) -> CallableType<'db> { - Self::new(db, CallableSignature::bottom(), false) + Self::new(db, CallableSignature::bottom(), CallableTypeKind::Regular) } /// Return a "normalized" version of this `Callable` type. @@ -10711,7 +10739,7 @@ impl<'db> CallableType<'db> { CallableType::new( db, self.signatures(db).normalized_impl(db, visitor), - self.is_function_like(db), + self.kind(db), ) } @@ -10726,7 +10754,7 @@ impl<'db> CallableType<'db> { db, self.signatures(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), - self.is_function_like(db), + self.kind(db), ) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d939dabb01807..f3a0ff28497ee 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -32,13 +32,13 @@ use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::typed_dict::typed_dict_params_from_class_def; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ - ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, DataclassFlags, - DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, - IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, - MaterializationKind, NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, - TypeContext, TypeMapping, TypeRelation, TypedDictParams, UnionBuilder, VarianceInferable, - declaration_type, determine_upper_bound, exceeds_max_specialization_depth, - infer_definition_types, + ApplyTypeMappingVisitor, Binding, BoundSuperType, CallableType, CallableTypeKind, + DataclassFlags, DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, + HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownInstanceType, + ManualPEP695TypeAliasType, MaterializationKind, NormalizedVisitor, PropertyInstanceType, + StringLiteralType, TypeAliasType, TypeContext, TypeMapping, TypeRelation, TypedDictParams, + UnionBuilder, VarianceInferable, declaration_type, determine_upper_bound, + exceeds_max_specialization_depth, infer_definition_types, }; use crate::{ Db, FxIndexMap, FxIndexSet, FxOrderSet, Program, @@ -935,8 +935,11 @@ impl<'db> ClassType<'db> { let getitem_signature = CallableSignature::from_overloads(overload_signatures); - let getitem_type = - Type::Callable(CallableType::new(db, getitem_signature, true)); + let getitem_type = Type::Callable(CallableType::new( + db, + getitem_signature, + CallableTypeKind::FunctionLike, + )); Member::definitely_declared(getitem_type) }) .unwrap_or_else(fallback_member_lookup) @@ -1092,7 +1095,7 @@ impl<'db> ClassType<'db> { let dunder_new_bound_method = Type::Callable(CallableType::new( db, dunder_new_signature.bind_self(db, Some(instance_ty)), - true, + CallableTypeKind::FunctionLike, )); if returns_non_subclass { @@ -1142,7 +1145,7 @@ impl<'db> ClassType<'db> { Some(Type::Callable(CallableType::new( db, synthesized_dunder_init_signature, - true, + CallableTypeKind::FunctionLike, ))) } else { None @@ -1962,9 +1965,11 @@ impl<'db> ClassLiteral<'db> { ) -> PlaceAndQualifiers<'db> { fn into_function_like_callable<'d>(db: &'d dyn Db, ty: Type<'d>) -> Type<'d> { match ty { - Type::Callable(callable_ty) => { - Type::Callable(CallableType::new(db, callable_ty.signatures(db), true)) - } + Type::Callable(callable_ty) => Type::Callable(CallableType::new( + db, + callable_ty.signatures(db), + CallableTypeKind::FunctionLike, + )), Type::Union(union) => { union.map(db, |element| into_function_like_callable(db, *element)) } @@ -2451,7 +2456,7 @@ impl<'db> ClassLiteral<'db> { ]), Some(Type::none(db)), )), - true, + CallableTypeKind::FunctionLike, ))); } @@ -2474,7 +2479,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(overloads), - true, + CallableTypeKind::FunctionLike, ))) } (CodeGeneratorKind::TypedDict, "__getitem__") => { @@ -2498,7 +2503,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(overloads), - true, + CallableTypeKind::FunctionLike, ))) } (CodeGeneratorKind::TypedDict, "get") => { @@ -2594,7 +2599,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(overloads), - true, + CallableTypeKind::FunctionLike, ))) } (CodeGeneratorKind::TypedDict, "pop") => { @@ -2648,7 +2653,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(overloads), - true, + CallableTypeKind::FunctionLike, ))) } (CodeGeneratorKind::TypedDict, "setdefault") => { @@ -2673,7 +2678,7 @@ impl<'db> ClassLiteral<'db> { Some(Type::Callable(CallableType::new( db, CallableSignature::from_overloads(overloads), - true, + CallableTypeKind::FunctionLike, ))) } (CodeGeneratorKind::TypedDict, "update") => { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 9bd0e2fdf5788..ca3c196ce9f73 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -26,9 +26,9 @@ use crate::types::signatures::{ use crate::types::tuple::TupleSpec; use crate::types::visitor::TypeVisitor; use crate::types::{ - BoundTypeVarIdentity, CallableType, IntersectionType, KnownBoundMethodType, KnownClass, - MaterializationKind, Protocol, ProtocolInstanceType, StringLiteralType, SubclassOfInner, Type, - UnionType, WrapperDescriptorKind, visitor, + BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, + KnownClass, MaterializationKind, Protocol, ProtocolInstanceType, StringLiteralType, + SubclassOfInner, Type, UnionType, WrapperDescriptorKind, visitor, }; use ruff_db::parsed::parsed_module; @@ -1037,6 +1037,7 @@ impl<'db> CallableType<'db> { ) -> DisplayCallableType<'db> { DisplayCallableType { signatures: self.signatures(db), + kind: self.kind(db), db, settings, } @@ -1045,6 +1046,7 @@ impl<'db> CallableType<'db> { pub(crate) struct DisplayCallableType<'db> { signatures: &'db CallableSignature<'db>, + kind: CallableTypeKind, db: &'db dyn Db, settings: DisplaySettings<'db>, } @@ -1052,9 +1054,18 @@ pub(crate) struct DisplayCallableType<'db> { impl Display for DisplayCallableType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.signatures.overloads.as_slice() { - [signature] => signature - .display_with(self.db, self.settings.clone()) - .fmt(f), + [signature] => { + if matches!(self.kind, CallableTypeKind::ParamSpecValue) { + signature + .parameters() + .display_with(self.db, self.settings.clone()) + .fmt(f) + } else { + signature + .display_with(self.db, self.settings.clone()) + .fmt(f) + } + } signatures => { // TODO: How to display overloads? if !self.settings.multiline { @@ -1115,83 +1126,10 @@ impl DisplaySignature<'_> { /// Internal method to write signature with the signature writer fn write_signature(&self, writer: &mut SignatureWriter) -> fmt::Result { - let multiline = self.settings.multiline && self.parameters.len() > 1; - // Opening parenthesis - writer.write_char('(')?; - if multiline { - writer.write_str("\n ")?; - } - match self.parameters.kind() { - ParametersKind::Standard => { - let mut star_added = false; - let mut needs_slash = false; - let mut first = true; - let arg_separator = if multiline { ",\n " } else { ", " }; - - for parameter in self.parameters.as_slice() { - // Handle special separators - if !star_added && parameter.is_keyword_only() { - if !first { - writer.write_str(arg_separator)?; - } - writer.write_char('*')?; - star_added = true; - first = false; - } - if parameter.is_positional_only() { - needs_slash = true; - } else if needs_slash { - if !first { - writer.write_str(arg_separator)?; - } - writer.write_char('/')?; - needs_slash = false; - first = false; - } - - // Add comma before parameter if not first - if !first { - writer.write_str(arg_separator)?; - } - - // Write parameter with range tracking - let param_name = parameter.display_name(); - writer.write_parameter( - ¶meter.display_with(self.db, self.settings.singleline()), - param_name.as_deref(), - )?; - - first = false; - } - - if needs_slash { - if !first { - writer.write_str(arg_separator)?; - } - writer.write_char('/')?; - } - } - ParametersKind::Gradual => { - // We represent gradual form as `...` in the signature, internally the parameters still - // contain `(*args, **kwargs)` parameters. - writer.write_str("...")?; - } - ParametersKind::ParamSpec(origin) => { - writer.write_str(&format!("**{}", origin.name(self.db)))?; - if let Some(name) = origin - .binding_context(self.db) - .and_then(|binding_context| binding_context.name(self.db)) - { - writer.write_str(&format!("@{name}"))?; - } - } - } - - if multiline { - writer.write_char('\n')?; - } - // Closing parenthesis - writer.write_char(')')?; + // Parameters + self.parameters + .display_with(self.db, self.settings.clone()) + .write_parameters(writer)?; // Return type let return_ty = self.return_ty.unwrap_or_else(Type::unknown); @@ -1310,6 +1248,115 @@ pub(crate) struct SignatureDisplayDetails { pub parameter_names: Vec, } +impl<'db> Parameters<'db> { + fn display_with( + &'db self, + db: &'db dyn Db, + settings: DisplaySettings<'db>, + ) -> DisplayParameters<'db> { + DisplayParameters { + parameters: self, + db, + settings, + } + } +} + +struct DisplayParameters<'db> { + parameters: &'db Parameters<'db>, + db: &'db dyn Db, + settings: DisplaySettings<'db>, +} + +impl DisplayParameters<'_> { + fn write_parameters(&self, writer: &mut SignatureWriter) -> fmt::Result { + let multiline = self.settings.multiline && self.parameters.len() > 1; + // Opening parenthesis + writer.write_char('(')?; + if multiline { + writer.write_str("\n ")?; + } + match self.parameters.kind() { + ParametersKind::Standard => { + let mut star_added = false; + let mut needs_slash = false; + let mut first = true; + let arg_separator = if multiline { ",\n " } else { ", " }; + + for parameter in self.parameters.as_slice() { + // Handle special separators + if !star_added && parameter.is_keyword_only() { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('*')?; + star_added = true; + first = false; + } + if parameter.is_positional_only() { + needs_slash = true; + } else if needs_slash { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('/')?; + needs_slash = false; + first = false; + } + + // Add comma before parameter if not first + if !first { + writer.write_str(arg_separator)?; + } + + // Write parameter with range tracking + let param_name = parameter.display_name(); + writer.write_parameter( + ¶meter.display_with(self.db, self.settings.singleline()), + param_name.as_deref(), + )?; + + first = false; + } + + if needs_slash { + if !first { + writer.write_str(arg_separator)?; + } + writer.write_char('/')?; + } + } + ParametersKind::Gradual => { + // We represent gradual form as `...` in the signature, internally the parameters still + // contain `(*args, **kwargs)` parameters. + writer.write_str("...")?; + } + ParametersKind::ParamSpec(origin) => { + writer.write_str(&format!("**{}", origin.name(self.db)))?; + if let Some(name) = origin + .binding_context(self.db) + .and_then(|binding_context| binding_context.name(self.db)) + { + writer.write_str(&format!("@{name}"))?; + } + } + } + + if multiline { + writer.write_char('\n')?; + } + // Closing parenthesis + writer.write_char(')') + } +} + +impl Display for DisplayParameters<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let mut writer = SignatureWriter::Formatter(f); + self.write_parameters(&mut writer) + } +} + impl<'db> Parameter<'db> { fn display_with( &'db self, diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 737a5218e494f..b1aca0211aa6a 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -79,8 +79,8 @@ use crate::types::narrow::ClassInfoConstraintFunction; use crate::types::signatures::{CallableSignature, Signature}; use crate::types::visitor::any_over_type; use crate::types::{ - ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, - ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, + ApplyTypeMappingVisitor, BoundMethodType, BoundTypeVarInstance, CallableType, CallableTypeKind, + ClassBase, ClassLiteral, ClassType, DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, NormalizedVisitor, SpecialFormType, Truthiness, Type, TypeContext, TypeMapping, TypeRelation, UnionBuilder, binding_type, todo_type, walk_signature, @@ -954,7 +954,7 @@ impl<'db> FunctionType<'db> { /// Convert the `FunctionType` into a [`CallableType`]. pub(crate) fn into_callable_type(self, db: &'db dyn Db) -> CallableType<'db> { - CallableType::new(db, self.signature(db), true) + CallableType::new(db, self.signature(db), CallableTypeKind::FunctionLike) } /// Convert the `FunctionType` into a [`BoundMethodType`]. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ea656b2d76018..9ce4f5b3fef65 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -100,14 +100,15 @@ use crate::types::typed_dict::{ }; use crate::types::visitor::any_over_type; use crate::types::{ - CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, - DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, IntersectionType, - KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, - PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, Parameters, SpecialFormType, - SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, - TypeContext, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, - TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, - UnionType, binding_type, todo_type, + CallDunderError, CallableBinding, CallableType, CallableTypeKind, ClassLiteral, ClassType, + DataclassParams, DynamicType, InferredAs, InternedType, InternedTypes, IntersectionBuilder, + IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, + MetaclassCandidate, PEP695TypeAliasType, ParamSpecAttrKind, Parameter, ParameterForm, + Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, + TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers, + TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, + TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, + binding_type, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -2274,7 +2275,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::Callable(callable) => Some(Type::Callable(CallableType::new( db, callable.signatures(db), - true, + CallableTypeKind::FunctionLike, ))), Type::Union(union) => union .try_map(db, |element| into_function_like_callable(db, *element)), @@ -3324,9 +3325,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // for the subscript branch which is required for `Concatenate` but that cannot be // specified in this context. match default { - ast::Expr::EllipsisLiteral(_) => { - CallableType::single(self.db(), Signature::new(Parameters::gradual_form(), None)) - } + ast::Expr::EllipsisLiteral(_) => CallableType::paramspec_value( + self.db(), + Signature::new(Parameters::gradual_form(), None), + ), ast::Expr::List(ast::ExprList { elts, .. }) => { let mut parameter_types = Vec::with_capacity(elts.len()); @@ -3353,7 +3355,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { })) }; - CallableType::single(self.db(), Signature::new(parameters, None)) + CallableType::paramspec_value(self.db(), Signature::new(parameters, None)) } ast::Expr::Name(name) => { let name_ty = self.infer_name_load(name); From 0f1dddc7fa19153ddea8a8f73c1c0a570ae7b7ca Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 19 Nov 2025 09:30:40 +0530 Subject: [PATCH 05/59] Remove debug logs --- crates/ty_python_semantic/src/types/infer/builder.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index c2a643199e752..6988b1d9c0b9d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2671,7 +2671,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { if let Some(annotation) = parameter.annotation() { let annotated_type = self.file_expression_type(annotation); - tracing::debug!("annotated_type: {}", annotated_type.display(self.db())); let ty = if let Type::TypeVar(typevar) = annotated_type && typevar.kind(self.db()).is_paramspec() { @@ -8998,10 +8997,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let mut constraint_keys = vec![]; - tracing::debug!( - "value_type for attribute access: {}", - value_type.display(db) - ); if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_type && typevar.kind(db).is_paramspec() && let Some(bound_typevar) = bind_typevar( @@ -9013,7 +9008,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) { value_type = Type::TypeVar(bound_typevar); - tracing::debug!("updated value_type: {}", value_type.display(db)); } let mut assigned_type = None; @@ -9027,7 +9021,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { assigned_type = Some(ty); } } - tracing::debug!("assigned_type for attribute access: {:?}", assigned_type); let fallback_place = value_type.member(db, &attr.id); // Exclude non-definitely-bound places for purposes of reachability // analysis. We currently do not perform boundness analysis for implicit @@ -9126,11 +9119,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }) .inner_type(); - tracing::debug!( - "resolved_type for attribute access: {}", - resolved_type.display(db) - ); - self.check_deprecated(attr, resolved_type); // Even if we can obtain the attribute type based on the assignments, we still perform default type inference From e34532cc20a5234951f83c95a6211dc8a716f1e7 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 19 Nov 2025 21:36:05 +0530 Subject: [PATCH 06/59] Initial attempt to add to generic infra --- crates/ty_python_semantic/src/types.rs | 28 ++- .../ty_python_semantic/src/types/call/bind.rs | 234 +++++++++++------- .../ty_python_semantic/src/types/display.rs | 27 +- .../ty_python_semantic/src/types/generics.rs | 79 +++++- .../src/types/infer/builder.rs | 23 +- .../types/infer/builder/type_expression.rs | 2 +- .../src/types/signatures.rs | 69 +++++- 7 files changed, 354 insertions(+), 108 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4fda148cd0151..133072ad344a0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4579,8 +4579,7 @@ impl<'db> Type<'db> { } Type::TypeVar(typevar) - if typevar.kind(db).is_paramspec() - && matches!(name.as_str(), "args" | "kwargs") => + if typevar.is_paramspec(db) && matches!(name.as_str(), "args" | "kwargs") => { Place::bound(Type::TypeVar(match name.as_str() { "args" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), @@ -7881,6 +7880,7 @@ pub enum TypeMapping<'a, 'db> { /// Replace default types in parameters of callables with `Unknown`. This is used to avoid infinite /// recursion when the type of the default value of a parameter depends on the callable itself. ReplaceParameterDefaults, + // ReplaceParamSpecVars(Specialization<'db>), } impl<'db> TypeMapping<'_, 'db> { @@ -8113,7 +8113,7 @@ impl<'db> KnownInstanceType<'db> { fn class(self, db: &'db dyn Db) -> KnownClass { match self { Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, - Self::TypeVar(typevar_instance) if typevar_instance.kind(db).is_paramspec() => { + Self::TypeVar(typevar_instance) if typevar_instance.is_paramspec(db) => { KnownClass::ParamSpec } Self::TypeVar(_) => KnownClass::TypeVar, @@ -8187,7 +8187,7 @@ impl<'db> KnownInstanceType<'db> { // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. KnownInstanceType::TypeVar(typevar_instance) => { - if typevar_instance.kind(self.db).is_paramspec() { + if typevar_instance.is_paramspec(self.db) { f.write_str("typing.ParamSpec") } else { f.write_str("typing.TypeVar") @@ -8778,6 +8778,10 @@ impl<'db> TypeVarInstance<'db> { matches!(self.kind(db), TypeVarKind::TypingSelf) } + pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { + self.kind(db).is_paramspec() + } + pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { Some(ty) @@ -9129,6 +9133,10 @@ impl<'db> BoundTypeVarInstance<'db> { self.typevar(db).kind(db) } + pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool { + self.kind(db).is_paramspec() + } + pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { Self::new(db, self.typevar(db), self.binding_context(db), Some(kind)) } @@ -10884,6 +10892,18 @@ impl<'db> CallableType<'db> { )) } + /// Create a callable type which represents the value bound to a `ParamSpec` type variable. + pub(crate) fn overloaded_paramspec_value(db: &'db dyn Db, signatures: I) -> Type<'db> + where + I: IntoIterator>, + { + Type::Callable(CallableType::new( + db, + CallableSignature::from_overloads(signatures), + CallableTypeKind::ParamSpecValue, + )) + } + /// Create a callable type which accepts any parameters and returns an `Unknown` type. pub(crate) fn unknown(db: &'db dyn Db) -> Type<'db> { Self::single(db, Signature::unknown()) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index a99ac8b1ef8ad..b939f81737606 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3,6 +3,7 @@ //! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a //! union of types, each of which might contain multiple overloads. +use std::borrow::Cow; use std::collections::HashSet; use std::fmt; @@ -33,13 +34,14 @@ use crate::types::generics::{ InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError, }; use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; -use crate::types::tuple::{TupleLength, TupleType}; +use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, - MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, - TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, - WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type, + MemberLookupPolicy, NominalInstanceType, ParamSpecAttrKind, PropertyInstanceType, + SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, + UnionBuilder, UnionType, WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, + todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; @@ -2530,20 +2532,44 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { argument: Argument<'a>, argument_type: Option>, ) -> Result<(), ()> { - // TODO: `Type::iterate` internally handles unions, but in a lossy way. - // It might be superior here to manually map over the union and call `try_iterate` - // on each element, similar to the way that `unpacker.rs` does in the `unpack_inner` method. - // It might be a bit of a refactor, though. - // See - // for more details. --Alex - let tuple = argument_type.map(|ty| ty.iterate(db)); - let (mut argument_types, length, variable_element) = match tuple.as_ref() { - Some(tuple) => ( + enum VariadicArgumentType<'db> { + ParamSpecArgs(Type<'db>), + Other(Cow<'db, TupleSpec<'db>>), + None, + } + + let variadic_type = match argument_type { + Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { + if matches!(typevar.paramspec_attr(db), Some(ParamSpecAttrKind::Args)) { + VariadicArgumentType::ParamSpecArgs(paramspec) + } else { + VariadicArgumentType::None + } + } + Some(ty) => { + // TODO: `Type::iterate` internally handles unions, but in a lossy way. + // It might be superior here to manually map over the union and call `try_iterate` + // on each element, similar to the way that `unpacker.rs` does in the `unpack_inner` method. + // It might be a bit of a refactor, though. + // See + // for more details. --Alex + VariadicArgumentType::Other(ty.iterate(db)) + } + None => VariadicArgumentType::None, + }; + + let (mut argument_types, length, variable_element) = match &variadic_type { + VariadicArgumentType::ParamSpecArgs(paramspec_args) => ( + Either::Right(std::iter::empty()), + TupleLength::unknown(), + Some(*paramspec_args), + ), + VariadicArgumentType::Other(tuple) => ( Either::Left(tuple.all_elements().copied()), tuple.len(), tuple.variable_element().copied(), ), - None => ( + VariadicArgumentType::None => ( Either::Right(std::iter::empty()), TupleLength::unknown(), None, @@ -2618,19 +2644,32 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { ); } } else { - let value_type = match argument_type.map(|ty| { - ty.member_lookup_with_policy( - db, - Name::new_static("__getitem__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place - }) { - Some(Place::Defined(keys_method, _, Definedness::AlwaysDefined)) => keys_method - .try_call(db, &CallArguments::positional([Type::unknown()])) - .ok() - .map_or_else(Type::unknown, |bindings| bindings.return_type(db)), - _ => Type::unknown(), + let value_type = match argument_type { + // TODO: Is this correct? + Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { + if matches!(typevar.paramspec_attr(db), Some(ParamSpecAttrKind::Kwargs)) { + paramspec + } else { + Type::unknown() + } + } + Some(ty) => { + match ty + .member_lookup_with_policy( + db, + Name::new_static("__getitem__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method + .try_call(db, &CallArguments::positional([Type::unknown()])) + .ok() + .map_or_else(Type::unknown, |bindings| bindings.return_type(db)), + _ => Type::unknown(), + } + } + None => Type::unknown(), }; for (parameter_index, parameter) in self.parameters.iter().enumerate() { @@ -2772,15 +2811,22 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn infer_specialization(&mut self) { let Some(generic_context) = self.signature.generic_context else { + tracing::debug!("No generic context for signature, skipping specialization inference"); return; }; + tracing::debug!("generic_context: {}", generic_context.display(self.db)); + let return_with_tcx = self .constructor_instance_type .or(self.signature.return_ty) .zip(self.call_expression_tcx.annotation); self.inferable_typevars = generic_context.inferable_typevars(self.db); + tracing::debug!( + "inferable_typevars: {}", + self.inferable_typevars.display(self.db) + ); let mut builder = SpecializationBuilder::new(self.db, self.inferable_typevars); // Prefer the declared type of generic classes. @@ -2942,9 +2988,16 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let parameters = self.signature.parameters(); let parameter = ¶meters[parameter_index]; if let Some(mut expected_ty) = parameter.annotated_type() { + tracing::debug!("argument_type: {}", argument_type.display(self.db)); + tracing::debug!("expected_ty: {}", expected_ty.display(self.db)); if let Some(specialization) = self.specialization { argument_type = argument_type.apply_specialization(self.db, specialization); expected_ty = expected_ty.apply_specialization(self.db, specialization); + tracing::debug!( + "specialized argument_type: {}", + argument_type.display(self.db) + ); + tracing::debug!("specialized expected_ty: {}", expected_ty.display(self.db)); } // This is one of the few places where we want to check if there's _any_ specialization // where assignability holds; normally we want to check that assignability holds for @@ -3048,67 +3101,80 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ); } } else { - // TODO: Instead of calling the `keys` and `__getitem__` methods, we should instead - // get the constraints which satisfies the `SupportsKeysAndGetItem` protocol i.e., the - // key and value type. - let key_type = match argument_type - .member_lookup_with_policy( - self.db, - Name::new_static("keys"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place + let value_type = if let Type::TypeVar(typevar) = argument_type + && typevar.is_paramspec(self.db) { - Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method - .try_call(self.db, &CallArguments::none()) - .ok() - .and_then(|bindings| { - Some( - bindings - .return_type(self.db) - .try_iterate(self.db) - .ok()? - .homogeneous_element_type(self.db), - ) - }), - _ => None, - }; + if matches!( + typevar.paramspec_attr(self.db), + Some(ParamSpecAttrKind::Kwargs) + ) { + argument_type + } else { + Type::unknown() + } + } else { + // TODO: Instead of calling the `keys` and `__getitem__` methods, we should + // instead get the constraints which satisfies the `SupportsKeysAndGetItem` + // protocol i.e., the key and value type. + let key_type = match argument_type + .member_lookup_with_policy( + self.db, + Name::new_static("keys"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method + .try_call(self.db, &CallArguments::none()) + .ok() + .and_then(|bindings| { + Some( + bindings + .return_type(self.db) + .try_iterate(self.db) + .ok()? + .homogeneous_element_type(self.db), + ) + }), + _ => None, + }; - let Some(key_type) = key_type else { - self.errors.push(BindingError::KeywordsNotAMapping { - argument_index: adjusted_argument_index, - provided_ty: argument_type, - }); - return; - }; + let Some(key_type) = key_type else { + self.errors.push(BindingError::KeywordsNotAMapping { + argument_index: adjusted_argument_index, + provided_ty: argument_type, + }); + return; + }; - if !key_type - .when_assignable_to( - self.db, - KnownClass::Str.to_instance(self.db), - self.inferable_typevars, - ) - .is_always_satisfied(self.db) - { - self.errors.push(BindingError::InvalidKeyType { - argument_index: adjusted_argument_index, - provided_ty: key_type, - }); - } + if !key_type + .when_assignable_to( + self.db, + KnownClass::Str.to_instance(self.db), + self.inferable_typevars, + ) + .is_always_satisfied(self.db) + { + self.errors.push(BindingError::InvalidKeyType { + argument_index: adjusted_argument_index, + provided_ty: key_type, + }); + } - let value_type = match argument_type - .member_lookup_with_policy( - self.db, - Name::new_static("__getitem__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place - { - Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method - .try_call(self.db, &CallArguments::positional([Type::unknown()])) - .ok() - .map_or_else(Type::unknown, |bindings| bindings.return_type(self.db)), - _ => Type::unknown(), + match argument_type + .member_lookup_with_policy( + self.db, + Name::new_static("__getitem__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method + .try_call(self.db, &CallArguments::positional([Type::unknown()])) + .ok() + .map_or_else(Type::unknown, |bindings| bindings.return_type(self.db)), + _ => Type::unknown(), + } }; for (argument_type, parameter_index) in diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index ca3c196ce9f73..2a592c872192d 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -27,8 +27,8 @@ use crate::types::tuple::TupleSpec; use crate::types::visitor::TypeVisitor; use crate::types::{ BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, - KnownClass, MaterializationKind, Protocol, ProtocolInstanceType, StringLiteralType, - SubclassOfInner, Type, UnionType, WrapperDescriptorKind, visitor, + KnownClass, MaterializationKind, ParamSpecAttrKind, Protocol, ProtocolInstanceType, + StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind, visitor, }; use ruff_db::parsed::parsed_module; @@ -942,8 +942,29 @@ impl Display for DisplayGenericContext<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let variables = self.generic_context.variables(self.db); + let mut paramspec_args_identity = None; let non_implicit_variables: Vec<_> = variables - .filter(|bound_typevar| !bound_typevar.typevar(self.db).is_self(self.db)) + .filter(|bound_typevar| { + let typevar = bound_typevar.typevar(self.db); + // TODO: Should we instead display all the type variables? + // e.g., [P@foo.args, P@foo.kwargs] + if typevar.is_paramspec(self.db) { + match bound_typevar.paramspec_attr(self.db) { + Some(ParamSpecAttrKind::Args) => { + paramspec_args_identity = Some(typevar.identity(self.db)); + } + Some(ParamSpecAttrKind::Kwargs) => { + // If we have already seen `*args`, we can skip `**kwargs` to avoid + // displaying the same `ParamSpec` twice. + if paramspec_args_identity == Some(typevar.identity(self.db)) { + return false; + } + } + None => {} + } + } + !typevar.is_self(self.db) + }) .collect(); if non_implicit_variables.is_empty() { diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 74d0cbaca3c08..444671a78dfd4 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -13,15 +13,16 @@ use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSet; use crate::types::instance::{Protocol, ProtocolInstanceType}; -use crate::types::signatures::{Parameter, Parameters, Signature}; +use crate::types::signatures::{Parameter, ParameterKind, Parameters, Signature}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, ClassLiteral, - FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, - KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext, - TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type, + ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, CallableType, + ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, + IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, + Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, + TypeVarInstance, TypeVarKind, TypeVarVariance, UnionType, declaration_type, + walk_bound_type_var_type, }; use crate::{Db, FxOrderMap, FxOrderSet}; @@ -889,7 +890,13 @@ impl<'db> Specialization<'db> { .generic_context(db) .variables_inner(db) .get_index_of(&bound_typevar.identity(db))?; - self.types(db).get(index).copied() + let ty = self.types(db).get(index).copied(); + tracing::debug!( + "Specialization for typevar {} is {:?}", + Type::TypeVar(bound_typevar).display(db), + ty, + ); + ty } /// Applies a specialization to this specialization. This is used, for instance, when a generic @@ -1600,6 +1607,64 @@ impl<'db> SpecializationBuilder<'db> { } } + (Type::Callable(formal_callable), Type::Callable(actual_callable)) => { + // We're only interested in callable of the form `Callable[P, ...]` for now where + // `P` is a `ParamSpec`. + let [signature] = formal_callable.signatures(self.db).as_slice() else { + return Ok(()); + }; + let formal_parameters = signature.parameters(); + let Some((paramspec_args_typevar, paramspec_kwargs_typevar)) = + formal_parameters.paramspec_typevars() + else { + return Ok(()); + }; + let mut args_signatures = vec![]; + let mut kwargs_signatures = vec![]; + for signature in actual_callable.signatures(self.db) { + args_signatures.push(Signature::new( + Parameters::new( + signature + .parameters() + .iter() + .filter(|param| param.is_positional() || param.is_variadic()) + .cloned(), + ), + None, + )); + kwargs_signatures.push(Signature::new( + Parameters::new( + signature + .parameters() + .iter() + .filter(|param| { + param.is_keyword_only() || param.is_keyword_variadic() + }) + .cloned(), + ), + None, + )); + } + self.add_type_mapping( + paramspec_args_typevar, + CallableType::overloaded_paramspec_value(self.db, args_signatures), + polarity, + &mut f, + ); + self.add_type_mapping( + paramspec_kwargs_typevar, + CallableType::overloaded_paramspec_value(self.db, kwargs_signatures), + polarity, + &mut f, + ); + } + + (Type::Callable(_), _) => { + if let Some(actual_callable) = actual.try_upcast_to_callable(self.db) { + self.infer_map_impl(formal, actual_callable, polarity, &mut f)?; + } + } + // TODO: Add more forms that we can structurally induct into: type[C], callables _ => {} } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6988b1d9c0b9d..48fa5cf51178b 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2542,7 +2542,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } else { let annotated_type = self.file_expression_type(annotation); if let Type::TypeVar(typevar) = annotated_type - && typevar.kind(self.db()).is_paramspec() + && typevar.is_paramspec(self.db()) { match typevar.paramspec_attr(self.db()) { // `*args: P.args` @@ -2550,6 +2550,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `*args: P.kwargs` Some(ParamSpecAttrKind::Kwargs) => { + // TODO: Should this diagnostic be raised as part of + // `ArgumentTypeChecker`? if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) { @@ -2568,6 +2570,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `*args: P` None => { + // The diagnostic for this case is handled in `in_type_expression`. // TODO: Should this be `Unknown` instead? Type::homogeneous_tuple(self.db(), Type::unknown()) } @@ -2672,11 +2675,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Some(annotation) = parameter.annotation() { let annotated_type = self.file_expression_type(annotation); let ty = if let Type::TypeVar(typevar) = annotated_type - && typevar.kind(self.db()).is_paramspec() + && typevar.is_paramspec(self.db()) { match typevar.paramspec_attr(self.db()) { // `**kwargs: P.args` Some(ParamSpecAttrKind::Args) => { + // TODO: Should this diagnostic be raised as part of `ArgumentTypeChecker`? if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, annotation) { @@ -2698,11 +2702,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(ParamSpecAttrKind::Kwargs) => annotated_type, // `**kwargs: P` - // TODO: Should this be `Unknown` instead? - None => KnownClass::Dict.to_specialized_instance( - self.db(), - [KnownClass::Str.to_instance(self.db()), Type::unknown()], - ), + None => { + // The diagnostic for this case is handled in `in_type_expression`. + // TODO: Should this be `Unknown` instead? + KnownClass::Dict.to_specialized_instance( + self.db(), + [KnownClass::Str.to_instance(self.db()), Type::unknown()], + ) + } } } else { KnownClass::Dict.to_specialized_instance( @@ -8998,7 +9005,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut constraint_keys = vec![]; if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_type - && typevar.kind(db).is_paramspec() + && typevar.is_paramspec(db) && let Some(bound_typevar) = bind_typevar( db, self.index, diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index fb5c3ff16ad4c..1ba876f251009 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1551,7 +1551,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } let name_ty = self.infer_name_load(name); if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = name_ty - && typevar.kind(self.db()).is_paramspec() + && typevar.is_paramspec(self.db()) { let index = semantic_index(self.db(), self.scope().file(self.db())); let origin = bind_typevar( diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 1151f7e344477..2e444af6a1b94 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -29,7 +29,7 @@ use crate::types::generics::{ }; use crate::types::infer::nearest_enclosing_class; use crate::types::{ - ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, ClassLiteral, + ApplyTypeMappingVisitor, BindingContext, BoundTypeVarInstance, CallableType, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, TypeMapping, TypeRelation, TypeVarInstance, VarianceInferable, todo_type, @@ -151,6 +151,10 @@ impl<'db> CallableSignature<'db> { self.overloads.iter() } + pub(crate) fn as_slice(&self) -> &[Signature<'db>] { + &self.overloads + } + pub(crate) fn with_inherited_generic_context( &self, db: &'db dyn Db, @@ -182,6 +186,11 @@ impl<'db> CallableSignature<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { + // Check if `TypeMapping` is `ReplaceParamSpecVars` and take a special route which does the + // replacement in one pass for all overloads. I think in the case of overloads, the `self` + // is going to be `Callable[P, ...]` i.e., the one that actually contains the `ParamSpec` + // and not the actual overloaded function. That means that this needs to be converted into + // an overloaded callable. Self::from_overloads( self.overloads .iter() @@ -918,6 +927,52 @@ impl<'db> Signature<'db> { return ConstraintSet::from(relation.is_assignability()); } + if let Some((paramspec_args_typevar, paramspec_kwargs_typevar)) = + other.parameters.paramspec_typevars() + { + let args_type = CallableType::paramspec_value( + db, + Signature::new( + Parameters::new( + self.parameters + .iter() + .filter(|param| param.is_positional() || param.is_variadic()) + .cloned(), + ), + None, + ), + ); + let kwargs_type = CallableType::paramspec_value( + db, + Signature::new( + Parameters::new( + self.parameters + .iter() + .filter(|param| param.is_keyword_only() || param.is_keyword_variadic()) + .cloned(), + ), + None, + ), + ); + return ConstraintSet::constrain_typevar( + db, + paramspec_args_typevar, + args_type, + args_type, + relation, + ) + .union( + db, + ConstraintSet::constrain_typevar( + db, + paramspec_kwargs_typevar, + kwargs_type, + kwargs_type, + relation, + ), + ); + } + let mut parameters = ParametersZip { current_self: None, current_other: None, @@ -1354,6 +1409,18 @@ impl<'db> Parameters<'db> { matches!(self.kind, ParametersKind::Gradual) } + pub(crate) const fn is_paramspec(&self) -> bool { + matches!(self.kind, ParametersKind::ParamSpec(_)) + } + + pub(super) fn paramspec_typevars( + &self, + ) -> Option<(BoundTypeVarInstance<'db>, BoundTypeVarInstance<'db>)> { + let paramspec_args_typevar = self.variadic()?.1.annotated_type()?.as_typevar()?; + let paramspec_kwargs_typevar = self.keyword_variadic()?.1.annotated_type()?.as_typevar()?; + Some((paramspec_args_typevar, paramspec_kwargs_typevar)) + } + /// Return todo parameters: (*args: Todo, **kwargs: Todo) pub(crate) fn todo() -> Self { Self { From 46a45b79efb57ba2dd6f98cfd035b23a4d3b5a69 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 20 Nov 2025 00:28:07 +0530 Subject: [PATCH 07/59] simplify generic code --- crates/ty_python_semantic/src/types.rs | 26 +++- .../ty_python_semantic/src/types/display.rs | 36 +---- .../ty_python_semantic/src/types/generics.rs | 60 +++----- .../types/infer/builder/type_expression.rs | 25 ++-- .../src/types/signatures.rs | 140 ++++-------------- 5 files changed, 85 insertions(+), 202 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 133072ad344a0..57980e75213a5 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7343,16 +7343,22 @@ impl<'db> Type<'db> { visitor: &FindLegacyTypeVarsVisitor<'db>, ) { match self { - Type::TypeVar(bound_typevar) => { - if matches!( - bound_typevar.typevar(db).kind(db), - TypeVarKind::Legacy | TypeVarKind::TypingSelf | TypeVarKind::ParamSpec - ) && binding_context.is_none_or(|binding_context| { - bound_typevar.binding_context(db) == BindingContext::Definition(binding_context) - }) { + Type::TypeVar(bound_typevar) => match bound_typevar.typevar(db).kind(db) { + TypeVarKind::Legacy | TypeVarKind::TypingSelf + if binding_context.is_none_or(|binding_context| { + bound_typevar.binding_context(db) + == BindingContext::Definition(binding_context) + }) => + { typevars.insert(bound_typevar); } - } + TypeVarKind::ParamSpec => { + // For `ParamSpec`, we're only interested in `P` itself, not `P.args` or + // `P.kwargs`. + typevars.insert(bound_typevar.without_paramspec_attr(db)); + } + _ => {} + }, Type::FunctionLiteral(function) => { function.find_legacy_typevars_impl(db, binding_context, typevars, visitor); @@ -9141,6 +9147,10 @@ impl<'db> BoundTypeVarInstance<'db> { Self::new(db, self.typevar(db), self.binding_context(db), Some(kind)) } + pub(crate) fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { + Self::new(db, self.typevar(db), self.binding_context(db), None) + } + /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. /// differences in their bounds or constraints due to materialization. pub(crate) fn is_same_typevar_as(self, db: &'db dyn Db, other: Self) -> bool { diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 2a592c872192d..d5849d9460c17 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -27,8 +27,8 @@ use crate::types::tuple::TupleSpec; use crate::types::visitor::TypeVisitor; use crate::types::{ BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, - KnownClass, MaterializationKind, ParamSpecAttrKind, Protocol, ProtocolInstanceType, - StringLiteralType, SubclassOfInner, Type, UnionType, WrapperDescriptorKind, visitor, + KnownClass, MaterializationKind, Protocol, ProtocolInstanceType, StringLiteralType, + SubclassOfInner, Type, UnionType, WrapperDescriptorKind, visitor, }; use ruff_db::parsed::parsed_module; @@ -942,29 +942,8 @@ impl Display for DisplayGenericContext<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let variables = self.generic_context.variables(self.db); - let mut paramspec_args_identity = None; let non_implicit_variables: Vec<_> = variables - .filter(|bound_typevar| { - let typevar = bound_typevar.typevar(self.db); - // TODO: Should we instead display all the type variables? - // e.g., [P@foo.args, P@foo.kwargs] - if typevar.is_paramspec(self.db) { - match bound_typevar.paramspec_attr(self.db) { - Some(ParamSpecAttrKind::Args) => { - paramspec_args_identity = Some(typevar.identity(self.db)); - } - Some(ParamSpecAttrKind::Kwargs) => { - // If we have already seen `*args`, we can skip `**kwargs` to avoid - // displaying the same `ParamSpec` twice. - if paramspec_args_identity == Some(typevar.identity(self.db)) { - return false; - } - } - None => {} - } - } - !typevar.is_self(self.db) - }) + .filter(|bound_typevar| !bound_typevar.typevar(self.db).is_self(self.db)) .collect(); if non_implicit_variables.is_empty() { @@ -1352,12 +1331,9 @@ impl DisplayParameters<'_> { // contain `(*args, **kwargs)` parameters. writer.write_str("...")?; } - ParametersKind::ParamSpec(origin) => { - writer.write_str(&format!("**{}", origin.name(self.db)))?; - if let Some(name) = origin - .binding_context(self.db) - .and_then(|binding_context| binding_context.name(self.db)) - { + ParametersKind::ParamSpec(typevar) => { + writer.write_str(&format!("**{}", typevar.name(self.db)))?; + if let Some(name) = typevar.binding_context(self.db).name(self.db) { writer.write_str(&format!("@{name}"))?; } } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 444671a78dfd4..3c971870ec4fb 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -13,7 +13,7 @@ use crate::types::class::ClassType; use crate::types::class_base::ClassBase; use crate::types::constraints::ConstraintSet; use crate::types::instance::{Protocol, ProtocolInstanceType}; -use crate::types::signatures::{Parameter, ParameterKind, Parameters, Signature}; +use crate::types::signatures::{Parameter, Parameters, ParametersKind, Signature}; use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ @@ -335,8 +335,16 @@ impl<'db> GenericContext<'db> { }; Some(typevar.as_bound_type_var_instance(db, binding_context)) } - // TODO: Support these! - ast::TypeParam::ParamSpec(_) => None, + ast::TypeParam::ParamSpec(node) => { + let definition = index.expect_single_definition(node); + let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = + declaration_type(db, definition).inner_type() + else { + return None; + }; + Some(typevar.as_bound_type_var_instance(db, binding_context)) + } + // TODO: Support this! ast::TypeParam::TypeVarTuple(_) => None, } } @@ -1614,46 +1622,18 @@ impl<'db> SpecializationBuilder<'db> { return Ok(()); }; let formal_parameters = signature.parameters(); - let Some((paramspec_args_typevar, paramspec_kwargs_typevar)) = - formal_parameters.paramspec_typevars() - else { + let ParametersKind::ParamSpec(typevar) = formal_parameters.kind() else { return Ok(()); }; - let mut args_signatures = vec![]; - let mut kwargs_signatures = vec![]; - for signature in actual_callable.signatures(self.db) { - args_signatures.push(Signature::new( - Parameters::new( - signature - .parameters() - .iter() - .filter(|param| param.is_positional() || param.is_variadic()) - .cloned(), - ), - None, - )); - kwargs_signatures.push(Signature::new( - Parameters::new( - signature - .parameters() - .iter() - .filter(|param| { - param.is_keyword_only() || param.is_keyword_variadic() - }) - .cloned(), - ), - None, - )); - } - self.add_type_mapping( - paramspec_args_typevar, - CallableType::overloaded_paramspec_value(self.db, args_signatures), - polarity, - &mut f, - ); self.add_type_mapping( - paramspec_kwargs_typevar, - CallableType::overloaded_paramspec_value(self.db, kwargs_signatures), + typevar, + CallableType::overloaded_paramspec_value( + self.db, + actual_callable + .signatures(self.db) + .iter() + .map(|signature| Signature::new(signature.parameters().clone(), None)), + ), polarity, &mut f, ); diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 1ba876f251009..d46bc6395bc1f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -8,13 +8,14 @@ use crate::types::diagnostic::{ report_invalid_arguments_to_callable, }; use crate::types::generics::bind_typevar; -use crate::types::signatures::{ParamSpecOrigin, Signature}; +use crate::types::signatures::Signature; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::{ - CallableType, DynamicType, IntersectionBuilder, KnownClass, KnownInstanceType, - LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, Type, - TypeAliasType, TypeContext, TypeIsType, UnionBuilder, UnionType, todo_type, + BindingContext, BoundTypeVarInstance, CallableType, DynamicType, IntersectionBuilder, + KnownClass, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, + SubclassOfType, Type, TypeAliasType, TypeContext, TypeIsType, UnionBuilder, UnionType, + todo_type, }; /// Type expressions @@ -1554,18 +1555,22 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && typevar.is_paramspec(self.db()) { let index = semantic_index(self.db(), self.scope().file(self.db())); - let origin = bind_typevar( + let bound_typevar = bind_typevar( self.db(), index, self.scope().file_scope_id(self.db()), self.typevar_binding_context, typevar, ) - .map_or( - ParamSpecOrigin::Unbounded(typevar), - ParamSpecOrigin::Bounded, - ); - return Some(Parameters::paramspec(self.db(), origin)); + .unwrap_or_else(|| { + BoundTypeVarInstance::new( + self.db(), + typevar, + BindingContext::Synthetic, + None, + ) + }); + return Some(Parameters::paramspec(self.db(), bound_typevar)); } } _ => {} diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 2e444af6a1b94..052c88efa6cbb 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, CallableType, ClassLiteral, + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, - KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, - TypeContext, TypeMapping, TypeRelation, TypeVarInstance, VarianceInferable, todo_type, + KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, + TypeMapping, TypeRelation, VarianceInferable, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -927,50 +927,21 @@ impl<'db> Signature<'db> { return ConstraintSet::from(relation.is_assignability()); } - if let Some((paramspec_args_typevar, paramspec_kwargs_typevar)) = - other.parameters.paramspec_typevars() - { - let args_type = CallableType::paramspec_value( - db, - Signature::new( - Parameters::new( - self.parameters - .iter() - .filter(|param| param.is_positional() || param.is_variadic()) - .cloned(), - ), - None, - ), - ); - let kwargs_type = CallableType::paramspec_value( - db, - Signature::new( - Parameters::new( - self.parameters - .iter() - .filter(|param| param.is_keyword_only() || param.is_keyword_variadic()) - .cloned(), - ), - None, - ), - ); - return ConstraintSet::constrain_typevar( + if let ParametersKind::ParamSpec(typevar) = other.parameters.kind() { + let paramspec_value = + CallableType::paramspec_value(db, Signature::new(self.parameters().clone(), None)); + let constrained_typevar = ConstraintSet::constrain_typevar( db, - paramspec_args_typevar, - args_type, - args_type, + typevar, + paramspec_value, + paramspec_value, relation, - ) - .union( - db, - ConstraintSet::constrain_typevar( - db, - paramspec_kwargs_typevar, - kwargs_type, - kwargs_type, - relation, - ), ); + tracing::debug!( + "constrained paramspec typevar: {}", + constrained_typevar.display(db) + ); + return constrained_typevar; } let mut parameters = ParametersZip { @@ -1291,52 +1262,6 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { } } -#[derive( - Copy, Clone, Debug, Eq, Hash, PartialEq, salsa::Update, Ord, PartialOrd, get_size2::GetSize, -)] -pub(crate) enum ParamSpecOrigin<'db> { - Bounded(BoundTypeVarInstance<'db>), - Unbounded(TypeVarInstance<'db>), -} - -impl<'db> ParamSpecOrigin<'db> { - pub(crate) fn with_paramspec_attr( - self, - db: &'db dyn Db, - paramspec_attr: ParamSpecAttrKind, - ) -> Self { - match self { - ParamSpecOrigin::Bounded(typevar) => { - ParamSpecOrigin::Bounded(typevar.with_paramspec_attr(db, paramspec_attr)) - } - ParamSpecOrigin::Unbounded(_) => self, - } - } - - pub(crate) fn name(&self, db: &'db dyn Db) -> &ast::name::Name { - match self { - ParamSpecOrigin::Bounded(bound) => bound.typevar(db).name(db), - ParamSpecOrigin::Unbounded(unbound) => unbound.name(db), - } - } - - pub(crate) fn binding_context(&self, db: &'db dyn Db) -> Option> { - match self { - ParamSpecOrigin::Bounded(bound) => Some(bound.binding_context(db)), - ParamSpecOrigin::Unbounded(_) => None, - } - } - - pub(crate) fn into_type(self) -> Type<'db> { - match self { - ParamSpecOrigin::Bounded(bound) => Type::TypeVar(bound), - ParamSpecOrigin::Unbounded(unbound) => { - Type::KnownInstance(KnownInstanceType::TypeVar(unbound)) - } - } - } -} - /// The kind of parameter list represented. // TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number // of required positional parameters followed by a gradual form. Our representation will need @@ -1361,8 +1286,11 @@ pub(crate) enum ParametersKind<'db> { /// [the typing specification]: https://typing.python.org/en/latest/spec/callables.html#meaning-of-in-callable Gradual, - /// Represents a `ParamSpec` parameter list. - ParamSpec(ParamSpecOrigin<'db>), + /// Represents a parameter list containing a `ParamSpec` as the only parameter. + /// + /// Note that this is distinct from a parameter list _containing_ a `ParamSpec` which is + /// considered a standard parameter list that just contains a `ParamSpec`. + ParamSpec(BoundTypeVarInstance<'db>), } #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] @@ -1409,18 +1337,6 @@ impl<'db> Parameters<'db> { matches!(self.kind, ParametersKind::Gradual) } - pub(crate) const fn is_paramspec(&self) -> bool { - matches!(self.kind, ParametersKind::ParamSpec(_)) - } - - pub(super) fn paramspec_typevars( - &self, - ) -> Option<(BoundTypeVarInstance<'db>, BoundTypeVarInstance<'db>)> { - let paramspec_args_typevar = self.variadic()?.1.annotated_type()?.as_typevar()?; - let paramspec_kwargs_typevar = self.keyword_variadic()?.1.annotated_type()?.as_typevar()?; - Some((paramspec_args_typevar, paramspec_kwargs_typevar)) - } - /// Return todo parameters: (*args: Todo, **kwargs: Todo) pub(crate) fn todo() -> Self { Self { @@ -1451,21 +1367,17 @@ impl<'db> Parameters<'db> { } } - pub(crate) fn paramspec(db: &'db dyn Db, origin: ParamSpecOrigin<'db>) -> Self { + pub(crate) fn paramspec(db: &'db dyn Db, typevar: BoundTypeVarInstance<'db>) -> Self { Self { value: vec![ - Parameter::variadic(Name::new_static("args")).with_annotated_type( - origin - .with_paramspec_attr(db, ParamSpecAttrKind::Args) - .into_type(), - ), + Parameter::variadic(Name::new_static("args")).with_annotated_type(Type::TypeVar( + typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), + )), Parameter::keyword_variadic(Name::new_static("kwargs")).with_annotated_type( - origin - .with_paramspec_attr(db, ParamSpecAttrKind::Kwargs) - .into_type(), + Type::TypeVar(typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs)), ), ], - kind: ParametersKind::ParamSpec(origin), + kind: ParametersKind::ParamSpec(typevar), } } From 336edc4e17c8626123fd06ee74f029138ea0c408 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 20 Nov 2025 11:21:54 +0530 Subject: [PATCH 08/59] Naively apply type mapping --- crates/ty_python_semantic/src/types.rs | 4 ++-- .../ty_python_semantic/src/types/call/bind.rs | 14 ------------ .../ty_python_semantic/src/types/generics.rs | 12 +++------- .../src/types/signatures.rs | 22 +++++++++---------- 4 files changed, 16 insertions(+), 36 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index b456a3f53be3a..cb9e741db76be 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4587,9 +4587,9 @@ impl<'db> Type<'db> { } Type::TypeVar(typevar) - if typevar.is_paramspec(db) && matches!(name.as_str(), "args" | "kwargs") => + if typevar.is_paramspec(db) && matches!(name_str, "args" | "kwargs") => { - Place::bound(Type::TypeVar(match name.as_str() { + Place::bound(Type::TypeVar(match name_str { "args" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), "kwargs" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), _ => unreachable!(), diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 7ea415e3f9d2f..49c6f0601e4d1 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2839,22 +2839,15 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn infer_specialization(&mut self) { let Some(generic_context) = self.signature.generic_context else { - tracing::debug!("No generic context for signature, skipping specialization inference"); return; }; - tracing::debug!("generic_context: {}", generic_context.display(self.db)); - let return_with_tcx = self .constructor_instance_type .or(self.signature.return_ty) .zip(self.call_expression_tcx.annotation); self.inferable_typevars = generic_context.inferable_typevars(self.db); - tracing::debug!( - "inferable_typevars: {}", - self.inferable_typevars.display(self.db) - ); let mut builder = SpecializationBuilder::new(self.db, self.inferable_typevars); // Prefer the declared type of generic classes. @@ -3016,16 +3009,9 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let parameters = self.signature.parameters(); let parameter = ¶meters[parameter_index]; if let Some(mut expected_ty) = parameter.annotated_type() { - tracing::debug!("argument_type: {}", argument_type.display(self.db)); - tracing::debug!("expected_ty: {}", expected_ty.display(self.db)); if let Some(specialization) = self.specialization { argument_type = argument_type.apply_specialization(self.db, specialization); expected_ty = expected_ty.apply_specialization(self.db, specialization); - tracing::debug!( - "specialized argument_type: {}", - argument_type.display(self.db) - ); - tracing::debug!("specialized expected_ty: {}", expected_ty.display(self.db)); } // This is one of the few places where we want to check if there's _any_ specialization // where assignability holds; normally we want to check that assignability holds for diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 3cab66b96a95c..ac7c7b6348a48 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -940,13 +940,7 @@ impl<'db> Specialization<'db> { .generic_context(db) .variables_inner(db) .get_index_of(&bound_typevar.identity(db))?; - let ty = self.types(db).get(index).copied(); - tracing::debug!( - "Specialization for typevar {} is {:?}", - Type::TypeVar(bound_typevar).display(db), - ty, - ); - ty + self.types(db).get(index).copied() } /// Applies a specialization to this specialization. This is used, for instance, when a generic @@ -1663,8 +1657,8 @@ impl<'db> SpecializationBuilder<'db> { } (Type::Callable(formal_callable), Type::Callable(actual_callable)) => { - // We're only interested in callable of the form `Callable[P, ...]` for now where - // `P` is a `ParamSpec`. + // We're only interested in a formal callable of the form `Callable[P, ...]` for + // now where `P` is a `ParamSpec`. let [signature] = formal_callable.signatures(self.db).as_slice() else { return Ok(()); }; diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 052c88efa6cbb..fb4ee9fc1b7f3 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -186,11 +186,16 @@ impl<'db> CallableSignature<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { - // Check if `TypeMapping` is `ReplaceParamSpecVars` and take a special route which does the - // replacement in one pass for all overloads. I think in the case of overloads, the `self` - // is going to be `Callable[P, ...]` i.e., the one that actually contains the `ParamSpec` - // and not the actual overloaded function. That means that this needs to be converted into - // an overloaded callable. + if let TypeMapping::Specialization(specialization) = type_mapping + && let [self_signature] = self.overloads.as_slice() + && let ParametersKind::ParamSpec(typevar) = self_signature.parameters.kind + && let Some(Type::Callable(callable)) = specialization.get(db, typevar) + { + return Self::from_overloads(callable.signatures(db).iter().map(|signature| { + Signature::new(signature.parameters.clone(), self_signature.return_ty) + })); + } + Self::from_overloads( self.overloads .iter() @@ -930,18 +935,13 @@ impl<'db> Signature<'db> { if let ParametersKind::ParamSpec(typevar) = other.parameters.kind() { let paramspec_value = CallableType::paramspec_value(db, Signature::new(self.parameters().clone(), None)); - let constrained_typevar = ConstraintSet::constrain_typevar( + return ConstraintSet::constrain_typevar( db, typevar, paramspec_value, paramspec_value, relation, ); - tracing::debug!( - "constrained paramspec typevar: {}", - constrained_typevar.display(db) - ); - return constrained_typevar; } let mut parameters = ParametersZip { From 4697fcb160679cd59454e976c7c419e887506338 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 20 Nov 2025 12:10:15 +0530 Subject: [PATCH 09/59] Update mdtest --- .../resources/mdtest/annotations/callable.md | 11 ++++------- .../resources/mdtest/annotations/self.md | 2 +- .../mdtest/annotations/unsupported_special_forms.md | 8 ++++---- .../resources/mdtest/generics/pep695/aliases.md | 8 ++++---- .../resources/mdtest/generics/pep695/classes.md | 8 ++++---- .../resources/mdtest/implicit_type_aliases.md | 2 +- .../ty_python_semantic/resources/mdtest/with/async.md | 4 ++-- 7 files changed, 20 insertions(+), 23 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md index e7e55f7a447b9..728566d30e87b 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/callable.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/callable.md @@ -307,12 +307,10 @@ Using a `ParamSpec` in a `Callable` annotation: from typing_extensions import Callable def _[**P1](c: Callable[P1, int]): - # TODO: Should reveal `ParamSpecArgs` and `ParamSpecKwargs` - reveal_type(P1.args) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs) - reveal_type(P1.kwargs) # revealed: @Todo(ParamSpecArgs / ParamSpecKwargs) + reveal_type(P1.args) # revealed: P1@_.args + reveal_type(P1.kwargs) # revealed: P1@_.kwargs - # TODO: Signature should be (**P1) -> int - reveal_type(c) # revealed: (...) -> int + reveal_type(c) # revealed: (**P1@_) -> int ``` And, using the legacy syntax: @@ -322,9 +320,8 @@ from typing_extensions import ParamSpec P2 = ParamSpec("P2") -# TODO: argument list should not be `...` (requires `ParamSpec` support) def _(c: Callable[P2, int]): - reveal_type(c) # revealed: (...) -> int + reveal_type(c) # revealed: (**P2@_) -> int ``` ## Using `typing.Unpack` diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 8fc0802bac8c4..e4c4dfad352fc 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -189,7 +189,7 @@ reveal_type(B().name_does_not_matter()) # revealed: B reveal_type(B().positional_only(1)) # revealed: B reveal_type(B().keyword_only(x=1)) # revealed: B # TODO: This should deally be `B` -reveal_type(B().decorated_method()) # revealed: Unknown +reveal_type(B().decorated_method()) # revealed: R@some_decorator reveal_type(B().a_property) # revealed: B 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 c5d737d9eb0e9..e5be90a14495b 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 @@ -21,9 +21,8 @@ def f(*args: Unpack[Ts]) -> tuple[Unpack[Ts]]: def g() -> TypeGuard[int]: ... def i(callback: Callable[Concatenate[int, P], R_co], *args: P.args, **kwargs: P.kwargs) -> R_co: - # TODO: Should reveal a type representing `P.args` and `P.kwargs` - reveal_type(args) # revealed: tuple[@Todo(ParamSpecArgs / ParamSpecKwargs), ...] - reveal_type(kwargs) # revealed: dict[str, @Todo(ParamSpecArgs / ParamSpecKwargs)] + reveal_type(args) # revealed: P@i.args + reveal_type(kwargs) # revealed: P@i.kwargs return callback(42, *args, **kwargs) class Foo: @@ -68,8 +67,9 @@ def _( reveal_type(c) # revealed: Unknown reveal_type(d) # revealed: Unknown + # error: [invalid-type-form] "Variable of type `ParamSpec` is not allowed in a type expression" def foo(a_: e) -> None: - reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec`) + reveal_type(a_) # revealed: Unknown ``` ## Inheritance diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md index d32029311a175..072f392e883ea 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -25,11 +25,11 @@ reveal_type(generic_context(SingleTypevar)) # revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars] reveal_type(generic_context(MultipleTypevars)) -# TODO: support `ParamSpec`/`TypeVarTuple` properly -# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts) -# revealed: ty_extensions.GenericContext[] +# TODO: support `TypeVarTuple` properly +# (these should include the `TypeVarTuple`s in their generic contexts) +# revealed: ty_extensions.GenericContext[P@SingleParamSpec] reveal_type(generic_context(SingleParamSpec)) -# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec] +# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec, P@TypeVarAndParamSpec] reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: ty_extensions.GenericContext[] reveal_type(generic_context(SingleTypeVarTuple)) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index a701c0fcacc2b..694aa308b50f8 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -25,11 +25,11 @@ reveal_type(generic_context(SingleTypevar)) # revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars] reveal_type(generic_context(MultipleTypevars)) -# TODO: support `ParamSpec`/`TypeVarTuple` properly -# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts) -# revealed: ty_extensions.GenericContext[] +# TODO: support `TypeVarTuple` properly +# (these should include the `TypeVarTuple`s in their generic contexts) +# revealed: ty_extensions.GenericContext[P@SingleParamSpec] reveal_type(generic_context(SingleParamSpec)) -# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec] +# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec, P@TypeVarAndParamSpec] reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: ty_extensions.GenericContext[] reveal_type(generic_context(SingleTypeVarTuple)) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index f5a6292a9b1de..2a9cff0809a71 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -500,7 +500,7 @@ def _( # TODO: Should be `dict[Unknown, Unknown]` reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar] # TODO: Should be `(...) -> Unknown` - reveal_type(my_callable) # revealed: (...) -> typing.TypeVar + reveal_type(my_callable) # revealed: (**P) -> typing.TypeVar ``` (Generic) implicit type aliases can be used as base classes: diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index 6d556d438b6e1..6b3dd009258bd 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -213,12 +213,12 @@ async def connect() -> AsyncGenerator[Session]: yield Session() # TODO: this should be `() -> _AsyncGeneratorContextManager[Session, None]` -reveal_type(connect) # revealed: (...) -> _AsyncGeneratorContextManager[Unknown, None] +reveal_type(connect) # revealed: () -> _AsyncGeneratorContextManager[_T_co@asynccontextmanager, None] async def main(): async with connect() as session: # TODO: should be `Session` - reveal_type(session) # revealed: Unknown + reveal_type(session) # revealed: _T_co@asynccontextmanager ``` ## `asyncio.timeout` From d0b846aec87d55bdf6c34c662ea129c84fa3aaff Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 20 Nov 2025 12:54:16 +0530 Subject: [PATCH 10/59] Restrict callable upcast during specialization --- crates/ty_python_semantic/src/types/generics.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index ac7c7b6348a48..30e7a39ebb35d 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1680,7 +1680,10 @@ impl<'db> SpecializationBuilder<'db> { ); } - (Type::Callable(_), _) => { + ( + Type::Callable(_), + Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::KnownBoundMethod(_), + ) => { if let Some(actual_callable) = actual.try_upcast_to_callable(self.db) { self.infer_map_impl(formal, actual_callable, polarity, &mut f)?; } From 683e8b7004f46274b2b381458b3c82d4f2c2feee Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 20 Nov 2025 19:09:59 +0530 Subject: [PATCH 11/59] Consider `(*args: P.args, **kwargs: P.kwargs)` equivalent to `(**P)` --- crates/ty_python_semantic/src/place.rs | 1 + crates/ty_python_semantic/src/types.rs | 761 ++++++++++-------- crates/ty_python_semantic/src/types/class.rs | 268 +++--- .../ty_python_semantic/src/types/display.rs | 4 +- .../ty_python_semantic/src/types/generics.rs | 1 + .../src/types/infer/builder.rs | 10 +- .../types/infer/builder/type_expression.rs | 9 +- .../types/property_tests/type_generation.rs | 39 +- .../src/types/protocol_class.rs | 5 +- .../src/types/signatures.rs | 106 ++- 10 files changed, 707 insertions(+), 497 deletions(-) diff --git a/crates/ty_python_semantic/src/place.rs b/crates/ty_python_semantic/src/place.rs index 0f22048cccf82..67c243cd062ab 100644 --- a/crates/ty_python_semantic/src/place.rs +++ b/crates/ty_python_semantic/src/place.rs @@ -1452,6 +1452,7 @@ mod implicit_globals { "__annotate__" if Program::get(db).python_version(db) >= PythonVersion::PY314 => { let signature = Signature::new( Parameters::new( + db, [Parameter::positional_only(Some(Name::new_static("format"))) .with_annotated_type(KnownClass::Int.to_instance(db))], ), diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cb9e741db76be..5d0b28af9c2cd 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1590,8 +1590,11 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Some(CallableType::single( db, Signature::new( - Parameters::new([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), + Parameters::new( + db, + [Parameter::positional_only(None) + .with_annotated_type(newtype.base(db).instance_type(db))], + ), Some(Type::NewTypeInstance(newtype)), ), )), @@ -5140,8 +5143,11 @@ impl<'db> Type<'db> { Type::DataclassTransformer(_) => Binding::single( self, Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static("func"))) - .with_annotated_type(Type::object())]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("func"))) + .with_annotated_type(Type::object())], + ), None, ), ) @@ -5156,14 +5162,17 @@ impl<'db> Type<'db> { ) => Binding::single( self, Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("a"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("b"))) - .type_form() - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("a"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("b"))) + .type_form() + .with_annotated_type(Type::any()), + ], + ), Some(KnownClass::ConstraintSet.to_instance(db)), ), ) @@ -5173,11 +5182,12 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "a", - ))) - .type_form() - .with_annotated_type(Type::any())]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("a"))) + .type_form() + .with_annotated_type(Type::any())], + ), Some(KnownClass::Bool.to_instance(db)), ), ) @@ -5192,13 +5202,16 @@ impl<'db> Type<'db> { self, Signature::new_generic( Some(GenericContext::from_typevar_instances(db, [val_ty])), - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::TypeVar(val_ty)), - Parameter::positional_only(Some(Name::new_static("type"))) - .type_form() - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::TypeVar(val_ty)), + Parameter::positional_only(Some(Name::new_static("type"))) + .type_form() + .with_annotated_type(Type::any()), + ], + ), Some(Type::TypeVar(val_ty)), ), ) @@ -5209,14 +5222,15 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "arg", - ))) - // We need to set the type to `Any` here (instead of `Never`), - // in order for every `assert_never` call to pass the argument - // check. If we set it to `Never`, we'll get invalid-argument-type - // errors instead of `type-assertion-failure` errors. - .with_annotated_type(Type::any())]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("arg"))) + // We need to set the type to `Any` here (instead of `Never`), + // in order for every `assert_never` call to pass the argument + // check. If we set it to `Never`, we'll get invalid-argument-type + // errors instead of `type-assertion-failure` errors. + .with_annotated_type(Type::any())], + ), Some(Type::none(db)), ), ) @@ -5226,13 +5240,16 @@ impl<'db> Type<'db> { Some(KnownFunction::Cast) => Binding::single( self, Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("typ")) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_or_keyword(Name::new_static("val")) - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("typ")) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_or_keyword(Name::new_static("val")) + .with_annotated_type(Type::any()), + ], + ), Some(Type::any()), ), ) @@ -5244,18 +5261,20 @@ impl<'db> Type<'db> { [ // def dataclass(cls: None, /) -> Callable[[type[_T]], type[_T]]: ... Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("cls"), - )) - .with_annotated_type(Type::none(db))]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("cls"))) + .with_annotated_type(Type::none(db))], + ), None, ), // def dataclass(cls: type[_T], /) -> type[_T]: ... Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("cls"), - )) - .with_annotated_type(KnownClass::Type.to_instance(db))]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("cls"))) + .with_annotated_type(KnownClass::Type.to_instance(db))], + ), None, ), // TODO: make this overload Python-version-dependent @@ -5274,38 +5293,41 @@ impl<'db> Type<'db> { // weakref_slot: bool = False, // ) -> Callable[[type[_T]], type[_T]]: ... Signature::new( - Parameters::new([ - Parameter::keyword_only(Name::new_static("init")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("repr")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("eq")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("order")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("unsafe_hash")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("frozen")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("match_args")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - Parameter::keyword_only(Name::new_static("kw_only")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("slots")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - Parameter::keyword_only(Name::new_static("weakref_slot")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(false)), - ]), + Parameters::new( + db, + [ + Parameter::keyword_only(Name::new_static("init")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("repr")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("eq")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("order")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("unsafe_hash")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("frozen")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("match_args")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + Parameter::keyword_only(Name::new_static("kw_only")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("slots")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + Parameter::keyword_only(Name::new_static("weakref_slot")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(false)), + ], + ), None, ), ], @@ -5335,11 +5357,12 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "o", - ))) - .with_annotated_type(Type::any()) - .with_default_type(Type::BooleanLiteral(false))]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("o"))) + .with_annotated_type(Type::any()) + .with_default_type(Type::BooleanLiteral(false))], + ), Some(KnownClass::Bool.to_instance(db)), ), ) @@ -5358,16 +5381,21 @@ impl<'db> Type<'db> { self, [ Signature::new( - Parameters::new([Parameter::positional_or_keyword( - Name::new_static("object"), - ) - .with_annotated_type(Type::object()) - .with_default_type(Type::string_literal(db, ""))]), + Parameters::new( + db, + [Parameter::positional_or_keyword(Name::new_static("object")) + .with_annotated_type(Type::object()) + .with_default_type(Type::string_literal(db, ""))], + ), Some(KnownClass::Str.to_instance(db)), ), Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("object")) + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static( + "object", + )) // TODO: Should be `ReadableBuffer` instead of this union type: .with_annotated_type(UnionType::from_elements( db, @@ -5377,13 +5405,18 @@ impl<'db> Type<'db> { ], )) .with_default_type(Type::bytes_literal(db, b"")), - Parameter::positional_or_keyword(Name::new_static("encoding")) + Parameter::positional_or_keyword(Name::new_static( + "encoding", + )) .with_annotated_type(KnownClass::Str.to_instance(db)) .with_default_type(Type::string_literal(db, "utf-8")), - Parameter::positional_or_keyword(Name::new_static("errors")) + Parameter::positional_or_keyword(Name::new_static( + "errors", + )) .with_annotated_type(KnownClass::Str.to_instance(db)) .with_default_type(Type::string_literal(db, "strict")), - ]), + ], + ), Some(KnownClass::Str.to_instance(db)), ), ], @@ -5406,29 +5439,33 @@ impl<'db> Type<'db> { self, [ Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("o"), - )) - .with_annotated_type(Type::any())]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("o"))) + .with_annotated_type(Type::any())], + ), Some(type_instance), ), Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("name"))) - .with_annotated_type(str_instance), - Parameter::positional_only(Some(Name::new_static("bases"))) - .with_annotated_type(Type::homogeneous_tuple( - db, - type_instance, - )), - Parameter::positional_only(Some(Name::new_static("dict"))) - .with_annotated_type( - KnownClass::Dict.to_specialized_instance( + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("name"))) + .with_annotated_type(str_instance), + Parameter::positional_only(Some(Name::new_static("bases"))) + .with_annotated_type(Type::homogeneous_tuple( db, - [str_instance, Type::any()], + type_instance, + )), + Parameter::positional_only(Some(Name::new_static("dict"))) + .with_annotated_type( + KnownClass::Dict.to_specialized_instance( + db, + [str_instance, Type::any()], + ), ), - ), - ]), + ], + ), Some(type_instance), ), ], @@ -5467,19 +5504,23 @@ impl<'db> Type<'db> { self, [ Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("t"))) - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("obj"))) - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("t"))) + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("obj"))) + .with_annotated_type(Type::any()), + ], + ), Some(KnownClass::Super.to_instance(db)), ), Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("t"), - )) - .with_annotated_type(Type::any())]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("t"))) + .with_annotated_type(Type::any())], + ), Some(KnownClass::Super.to_instance(db)), ), Signature::new( @@ -5506,24 +5547,27 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("message"))) - .with_annotated_type(Type::LiteralString), - Parameter::keyword_only(Name::new_static("category")) - .with_annotated_type(UnionType::from_elements( - db, - [ - // TODO: should be `type[Warning]` - Type::any(), - KnownClass::NoneType.to_instance(db), - ], - )) - // TODO: should be `type[Warning]` - .with_default_type(Type::any()), - Parameter::keyword_only(Name::new_static("stacklevel")) - .with_annotated_type(KnownClass::Int.to_instance(db)) - .with_default_type(Type::IntLiteral(1)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("message"))) + .with_annotated_type(Type::LiteralString), + Parameter::keyword_only(Name::new_static("category")) + .with_annotated_type(UnionType::from_elements( + db, + [ + // TODO: should be `type[Warning]` + Type::any(), + KnownClass::NoneType.to_instance(db), + ], + )) + // TODO: should be `type[Warning]` + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("stacklevel")) + .with_annotated_type(KnownClass::Int.to_instance(db)) + .with_default_type(Type::IntLiteral(1)), + ], + ), Some(KnownClass::Deprecated.to_instance(db)), ), ) @@ -5543,26 +5587,29 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("name")) - .with_annotated_type(KnownClass::Str.to_instance(db)), - Parameter::positional_or_keyword(Name::new_static("value")) - .with_annotated_type(Type::any()) - .type_form(), - Parameter::keyword_only(Name::new_static("type_params")) - .with_annotated_type(Type::homogeneous_tuple( - db, - UnionType::from_elements( + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("name")) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_or_keyword(Name::new_static("value")) + .with_annotated_type(Type::any()) + .type_form(), + Parameter::keyword_only(Name::new_static("type_params")) + .with_annotated_type(Type::homogeneous_tuple( db, - [ - KnownClass::TypeVar.to_instance(db), - KnownClass::ParamSpec.to_instance(db), - KnownClass::TypeVarTuple.to_instance(db), - ], - ), - )) - .with_default_type(Type::empty_tuple(db)), - ]), + UnionType::from_elements( + db, + [ + KnownClass::TypeVar.to_instance(db), + KnownClass::ParamSpec.to_instance(db), + KnownClass::TypeVarTuple.to_instance(db), + ], + ), + )) + .with_default_type(Type::empty_tuple(db)), + ], + ), None, ), ) @@ -5571,63 +5618,71 @@ impl<'db> Type<'db> { Some(KnownClass::Property) => { let getter_signature = Signature::new( - Parameters::new([ - Parameter::positional_only(None).with_annotated_type(Type::any()) - ]), + Parameters::new( + db, + [Parameter::positional_only(None).with_annotated_type(Type::any())], + ), Some(Type::any()), ); let setter_signature = Signature::new( - Parameters::new([ - Parameter::positional_only(None).with_annotated_type(Type::any()), - Parameter::positional_only(None).with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(None).with_annotated_type(Type::any()), + Parameter::positional_only(None).with_annotated_type(Type::any()), + ], + ), Some(Type::none(db)), ); let deleter_signature = Signature::new( - Parameters::new([ - Parameter::positional_only(None).with_annotated_type(Type::any()) - ]), + Parameters::new( + db, + [Parameter::positional_only(None).with_annotated_type(Type::any())], + ), Some(Type::any()), ); Binding::single( self, Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("fget")) - .with_annotated_type(UnionType::from_elements( - db, - [ - CallableType::single(db, getter_signature), - Type::none(db), - ], - )) - .with_default_type(Type::none(db)), - Parameter::positional_or_keyword(Name::new_static("fset")) - .with_annotated_type(UnionType::from_elements( - db, - [ - CallableType::single(db, setter_signature), - Type::none(db), - ], - )) - .with_default_type(Type::none(db)), - Parameter::positional_or_keyword(Name::new_static("fdel")) - .with_annotated_type(UnionType::from_elements( - db, - [ - CallableType::single(db, deleter_signature), - Type::none(db), - ], - )) - .with_default_type(Type::none(db)), - Parameter::positional_or_keyword(Name::new_static("doc")) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::Str.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("fget")) + .with_annotated_type(UnionType::from_elements( + db, + [ + CallableType::single(db, getter_signature), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("fset")) + .with_annotated_type(UnionType::from_elements( + db, + [ + CallableType::single(db, setter_signature), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("fdel")) + .with_annotated_type(UnionType::from_elements( + db, + [ + CallableType::single(db, deleter_signature), + Type::none(db), + ], + )) + .with_default_type(Type::none(db)), + Parameter::positional_or_keyword(Name::new_static("doc")) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::Str.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ], + ), None, ), ) @@ -5649,12 +5704,15 @@ impl<'db> Type<'db> { [ Signature::new(Parameters::empty(), Some(Type::empty_tuple(db))), Signature::new( - Parameters::new([Parameter::positional_only(Some( - Name::new_static("iterable"), - )) - .with_annotated_type( - KnownClass::Iterable.to_specialized_instance(db, [object]), - )]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static( + "iterable", + ))) + .with_annotated_type( + KnownClass::Iterable.to_specialized_instance(db, [object]), + )], + ), Some(Type::homogeneous_tuple(db, object)), ), ], @@ -5682,19 +5740,22 @@ impl<'db> Type<'db> { Binding::single( self, Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("typename"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("fields"))) - .with_annotated_type(KnownClass::Dict.to_instance(db)) - .with_default_type(Type::any()), - Parameter::keyword_only(Name::new_static("total")) - .with_annotated_type(KnownClass::Bool.to_instance(db)) - .with_default_type(Type::BooleanLiteral(true)), - // Future compatibility, in case new keyword arguments will be added: - Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("typename"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("fields"))) + .with_annotated_type(KnownClass::Dict.to_instance(db)) + .with_default_type(Type::any()), + Parameter::keyword_only(Name::new_static("total")) + .with_annotated_type(KnownClass::Bool.to_instance(db)) + .with_default_type(Type::BooleanLiteral(true)), + // Future compatibility, in case new keyword arguments will be added: + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::any()), + ], + ), None, ), ) @@ -5781,8 +5842,11 @@ impl<'db> Type<'db> { Type::KnownInstance(KnownInstanceType::NewType(newtype)) => Binding::single( self, Signature::new( - Parameters::new([Parameter::positional_only(None) - .with_annotated_type(newtype.base(db).instance_type(db))]), + Parameters::new( + db, + [Parameter::positional_only(None) + .with_annotated_type(newtype.base(db).instance_type(db))], + ), Some(Type::NewTypeInstance(newtype)), ), ) @@ -11426,25 +11490,31 @@ impl<'db> KnownBoundMethodType<'db> { | KnownBoundMethodType::PropertyDunderGet(_) => Either::Left(Either::Left( [ Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(KnownClass::Type.to_instance(db)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(KnownClass::Type.to_instance(db)), + ], + ), None, ), Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object()), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::Type.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object()), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::Type.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ], + ), None, ), ] @@ -11455,39 +11525,48 @@ impl<'db> KnownBoundMethodType<'db> { )), KnownBoundMethodType::PropertyDunderSet(_) => { Either::Right(std::iter::once(Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object()), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::object()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object()), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::object()), + ], + ), None, ))) } KnownBoundMethodType::StrStartswith(_) => { Either::Right(std::iter::once(Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("prefix"))) - .with_annotated_type(UnionType::from_elements( - db, - [ - KnownClass::Str.to_instance(db), - Type::homogeneous_tuple(db, KnownClass::Str.to_instance(db)), - ], - )), - Parameter::positional_only(Some(Name::new_static("start"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - Parameter::positional_only(Some(Name::new_static("end"))) - .with_annotated_type(UnionType::from_elements( - db, - [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], - )) - .with_default_type(Type::none(db)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("prefix"))) + .with_annotated_type(UnionType::from_elements( + db, + [ + KnownClass::Str.to_instance(db), + Type::homogeneous_tuple( + db, + KnownClass::Str.to_instance(db), + ), + ], + )), + Parameter::positional_only(Some(Name::new_static("start"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + Parameter::positional_only(Some(Name::new_static("end"))) + .with_annotated_type(UnionType::from_elements( + db, + [KnownClass::SupportsIndex.to_instance(db), Type::none(db)], + )) + .with_default_type(Type::none(db)), + ], + ), Some(KnownClass::Bool.to_instance(db)), ))) } @@ -11497,17 +11576,20 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::ConstraintSetRange => { Either::Right(std::iter::once(Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("lower_bound"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("typevar"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("upper_bound"))) - .type_form() - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("lower_bound"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("typevar"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("upper_bound"))) + .type_form() + .with_annotated_type(Type::any()), + ], + ), Some(KnownClass::ConstraintSet.to_instance(db)), ))) } @@ -11522,45 +11604,57 @@ impl<'db> KnownBoundMethodType<'db> { KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => { Either::Right(std::iter::once(Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("ty"))) - .type_form() - .with_annotated_type(Type::any()), - Parameter::positional_only(Some(Name::new_static("of"))) - .type_form() - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("ty"))) + .type_form() + .with_annotated_type(Type::any()), + Parameter::positional_only(Some(Name::new_static("of"))) + .type_form() + .with_annotated_type(Type::any()), + ], + ), Some(KnownClass::ConstraintSet.to_instance(db)), ))) } KnownBoundMethodType::ConstraintSetSatisfies(_) => { Either::Right(std::iter::once(Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static("other"))) - .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("other"))) + .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))], + ), Some(KnownClass::ConstraintSet.to_instance(db)), ))) } KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => { Either::Right(std::iter::once(Signature::new( - Parameters::new([Parameter::keyword_only(Name::new_static("inferable")) - .type_form() - .with_annotated_type(UnionType::from_elements( - db, - [Type::homogeneous_tuple(db, Type::any()), Type::none(db)], - )) - .with_default_type(Type::none(db))]), + Parameters::new( + db, + [Parameter::keyword_only(Name::new_static("inferable")) + .type_form() + .with_annotated_type(UnionType::from_elements( + db, + [Type::homogeneous_tuple(db, Type::any()), Type::none(db)], + )) + .with_default_type(Type::none(db))], + ), Some(KnownClass::Bool.to_instance(db)), ))) } KnownBoundMethodType::GenericContextSpecializeConstrained(_) => { Either::Right(std::iter::once(Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static( - "constraints", - ))) - .with_annotated_type(KnownClass::ConstraintSet.to_instance(db))]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("constraints"))) + .with_annotated_type(KnownClass::ConstraintSet.to_instance(db)), + ], + ), Some(UnionType::from_elements( db, [KnownClass::Specialization.to_instance(db), Type::none(db)], @@ -11600,29 +11694,35 @@ impl WrapperDescriptorKind { let descriptor = class.to_instance(db); [ Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(descriptor), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(none), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(type_instance), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(descriptor), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(none), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(type_instance), + ], + ), None, ), Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(descriptor), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(Type::object()), - Parameter::positional_only(Some(Name::new_static("owner"))) - .with_annotated_type(UnionType::from_elements( - db, - [type_instance, none], - )) - .with_default_type(none), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(descriptor), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(Type::object()), + Parameter::positional_only(Some(Name::new_static("owner"))) + .with_annotated_type(UnionType::from_elements( + db, + [type_instance, none], + )) + .with_default_type(none), + ], + ), None, ), ] @@ -11638,14 +11738,17 @@ impl WrapperDescriptorKind { WrapperDescriptorKind::PropertyDunderSet => { let object = Type::object(); Either::Right(std::iter::once(Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(KnownClass::Property.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("instance"))) - .with_annotated_type(object), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(object), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(KnownClass::Property.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("instance"))) + .with_annotated_type(object), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(object), + ], + ), None, ))) } diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index be2e293c636c2..4b4c3ebcb9c0e 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -741,13 +741,14 @@ impl<'db> ClassType<'db> { name: &str, ) -> Member<'db> { fn synthesize_getitem_overload_signature<'db>( + db: &'db dyn Db, index_annotation: Type<'db>, return_annotation: Type<'db>, ) -> Signature<'db> { let self_parameter = Parameter::positional_only(Some(Name::new_static("self"))); let index_parameter = Parameter::positional_only(Some(Name::new_static("index"))) .with_annotated_type(index_annotation); - let parameters = Parameters::new([self_parameter, index_parameter]); + let parameters = Parameters::new(db, [self_parameter, index_parameter]); Signature::new(parameters, Some(return_annotation)) } @@ -778,9 +779,11 @@ impl<'db> ClassType<'db> { .map(Type::IntLiteral) .unwrap_or_else(|| KnownClass::Int.to_instance(db)); - let parameters = - Parameters::new([Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(Type::instance(db, self))]); + let parameters = Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(Type::instance(db, self))], + ); let synthesized_dunder_method = CallableType::function_like(db, Signature::new(parameters, Some(return_type))); @@ -908,6 +911,7 @@ impl<'db> ClassType<'db> { ); Some(synthesize_getitem_overload_signature( + db, index_annotation, return_type, )) @@ -925,11 +929,13 @@ impl<'db> ClassType<'db> { // __getitem__(self, index: slice[Any, Any, Any], /) -> tuple[str | float | bytes, ...] // overload_signatures.push(synthesize_getitem_overload_signature( + db, KnownClass::SupportsIndex.to_instance(db), all_elements_unioned, )); overload_signatures.push(synthesize_getitem_overload_signature( + db, KnownClass::Slice.to_instance(db), Type::homogeneous_tuple(db, all_elements_unioned), )); @@ -1002,11 +1008,14 @@ impl<'db> ClassType<'db> { iterable_parameter.with_default_type(Type::empty_tuple(db)); } - let parameters = Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(SubclassOfType::from(db, self)), - iterable_parameter, - ]); + let parameters = Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(SubclassOfType::from(db, self)), + iterable_parameter, + ], + ); let synthesized_dunder = CallableType::function_like( db, @@ -2150,7 +2159,10 @@ impl<'db> ClassLiteral<'db> { .get(name) { let property_getter_signature = Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), Some(field.declared_ty), ); let property_getter = CallableType::single(db, property_getter_signature); @@ -2362,10 +2374,10 @@ impl<'db> ClassLiteral<'db> { let signature = match name { "__new__" | "__init__" => Signature::new_generic( inherited_generic_context.or_else(|| self.inherited_generic_context(db)), - Parameters::new(parameters), + Parameters::new(db, parameters), return_ty, ), - _ => Signature::new(Parameters::new(parameters), return_ty), + _ => Signature::new(Parameters::new(db, parameters), return_ty), }; Some(CallableType::function_like(db, signature)) }; @@ -2392,14 +2404,17 @@ impl<'db> ClassLiteral<'db> { } let signature = Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("self")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("other")) - // TODO: could be `Self`. - .with_annotated_type(instance_ty), - ]), + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("other")) + // TODO: could be `Self`. + .with_annotated_type(instance_ty), + ], + ), Some(KnownClass::Bool.to_instance(db)), ); @@ -2412,10 +2427,11 @@ impl<'db> ClassLiteral<'db> { if unsafe_hash || (frozen && eq) { let signature = Signature::new( - Parameters::new([Parameter::positional_or_keyword(Name::new_static( - "self", - )) - .with_annotated_type(instance_ty)]), + Parameters::new( + db, + [Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty)], + ), Some(KnownClass::Int.to_instance(db)), ); @@ -2497,12 +2513,15 @@ impl<'db> ClassLiteral<'db> { (CodeGeneratorKind::DataclassLike(_), "__setattr__") => { if has_dataclass_param(DataclassFlags::FROZEN) { let signature = Signature::new( - Parameters::new([ - Parameter::positional_or_keyword(Name::new_static("self")) - .with_annotated_type(instance_ty), - Parameter::positional_or_keyword(Name::new_static("name")), - Parameter::positional_or_keyword(Name::new_static("value")), - ]), + Parameters::new( + db, + [ + Parameter::positional_or_keyword(Name::new_static("self")) + .with_annotated_type(instance_ty), + Parameter::positional_or_keyword(Name::new_static("name")), + Parameter::positional_or_keyword(Name::new_static("value")), + ], + ), Some(Type::Never), ); @@ -2537,14 +2556,17 @@ impl<'db> ClassLiteral<'db> { return Some(Type::Callable(CallableType::new( db, CallableSignature::single(Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(Type::Never), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(Type::any()), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(Type::Never), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(Type::any()), + ], + ), Some(Type::none(db)), )), CallableTypeKind::FunctionLike, @@ -2555,14 +2577,17 @@ impl<'db> ClassLiteral<'db> { let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str())); Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("value"))) - .with_annotated_type(field.declared_ty), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("value"))) + .with_annotated_type(field.declared_ty), + ], + ), Some(Type::none(db)), ) }); @@ -2581,12 +2606,15 @@ impl<'db> ClassLiteral<'db> { let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str())); Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), Some(field.declared_ty), ) }); @@ -2613,12 +2641,15 @@ impl<'db> ClassLiteral<'db> { // once the generics solver takes default arguments into account. let get_sig = Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), Some(if field.is_required() { field.declared_ty } else { @@ -2631,14 +2662,17 @@ impl<'db> ClassLiteral<'db> { let get_with_default_sig = Signature::new_generic( Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), Some(if field.is_required() { field.declared_ty } else { @@ -2654,12 +2688,15 @@ impl<'db> ClassLiteral<'db> { // Fallback overloads for unknown keys .chain(std::iter::once({ Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + ], + ), Some(UnionType::from_elements( db, [Type::unknown(), Type::none(db)], @@ -2672,14 +2709,17 @@ impl<'db> ClassLiteral<'db> { Signature::new_generic( Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(KnownClass::Str.to_instance(db)), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(KnownClass::Str.to_instance(db)), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), Some(UnionType::from_elements( db, [Type::unknown(), Type::TypeVar(t_default)], @@ -2709,12 +2749,15 @@ impl<'db> ClassLiteral<'db> { // `.pop()` without default let pop_sig = Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + ], + ), Some(field.declared_ty), ); @@ -2724,14 +2767,17 @@ impl<'db> ClassLiteral<'db> { let pop_with_default_sig = Signature::new_generic( Some(GenericContext::from_typevar_instances(db, [t_default])), - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(Type::TypeVar(t_default)), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(Type::TypeVar(t_default)), + ], + ), Some(UnionType::from_elements( db, [field.declared_ty, Type::TypeVar(t_default)], @@ -2754,14 +2800,17 @@ impl<'db> ClassLiteral<'db> { // `setdefault` always returns the field type Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::positional_only(Some(Name::new_static("key"))) - .with_annotated_type(key_type), - Parameter::positional_only(Some(Name::new_static("default"))) - .with_annotated_type(field.declared_ty), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::positional_only(Some(Name::new_static("key"))) + .with_annotated_type(key_type), + Parameter::positional_only(Some(Name::new_static("default"))) + .with_annotated_type(field.declared_ty), + ], + ), Some(field.declared_ty), ) }); @@ -2775,12 +2824,15 @@ impl<'db> ClassLiteral<'db> { (CodeGeneratorKind::TypedDict, "update") => { // TODO: synthesize a set of overloads with precise types let signature = Signature::new( - Parameters::new([ - Parameter::positional_only(Some(Name::new_static("self"))) - .with_annotated_type(instance_ty), - Parameter::variadic(Name::new_static("args")), - Parameter::keyword_variadic(Name::new_static("kwargs")), - ]), + Parameters::new( + db, + [ + Parameter::positional_only(Some(Name::new_static("self"))) + .with_annotated_type(instance_ty), + Parameter::variadic(Name::new_static("args")), + Parameter::keyword_variadic(Name::new_static("kwargs")), + ], + ), Some(Type::none(db)), ); diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 4703677bd2af6..4a4b97663ea98 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1941,7 +1941,7 @@ mod tests { parameters: impl IntoIterator>, return_ty: Option>, ) -> String { - Signature::new(Parameters::new(parameters), return_ty) + Signature::new(Parameters::new(db, parameters), return_ty) .display(db) .to_string() } @@ -1951,7 +1951,7 @@ mod tests { parameters: impl IntoIterator>, return_ty: Option>, ) -> String { - Signature::new(Parameters::new(parameters), return_ty) + Signature::new(Parameters::new(db, parameters), return_ty) .display_with(db, super::DisplaySettings::default().multiline()) .to_string() } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 30e7a39ebb35d..f163b834b3adb 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -422,6 +422,7 @@ impl<'db> GenericContext<'db> { pub(crate) fn signature(self, db: &'db dyn Db) -> Signature<'db> { let parameters = Parameters::new( + db, self.variables(db) .map(|typevar| Self::parameter_from_typevar(db, typevar)), ); diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index aefff001bb0fd..99aeae20fad13 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3375,9 +3375,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // TODO: `Unpack` Parameters::todo() } else { - Parameters::new(parameter_types.iter().map(|param_type| { - Parameter::positional_only(None).with_annotated_type(*param_type) - })) + Parameters::new( + self.db(), + parameter_types.iter().map(|param_type| { + Parameter::positional_only(None).with_annotated_type(*param_type) + }), + ) }; CallableType::paramspec_value(self.db(), Signature::new(parameters, None)) @@ -7935,6 +7938,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .map(|param| Parameter::keyword_variadic(param.name().id.clone())); Parameters::new( + self.db(), positional_only .into_iter() .chain(positional_or_keyword) diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 8a0f99ffc4090..3a5fb67480344 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -1566,9 +1566,12 @@ impl<'db> TypeInferenceBuilder<'db, '_> { // TODO: `Unpack` Parameters::todo() } else { - Parameters::new(parameter_types.iter().map(|param_type| { - Parameter::positional_only(None).with_annotated_type(*param_type) - })) + Parameters::new( + self.db(), + parameter_types.iter().map(|param_type| { + Parameter::positional_only(None).with_annotated_type(*param_type) + }), + ) }); } ast::Expr::Subscript(subscript) => { diff --git a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs index 52136e4046015..d6c3f9e959c47 100644 --- a/crates/ty_python_semantic/src/types/property_tests/type_generation.rs +++ b/crates/ty_python_semantic/src/types/property_tests/type_generation.rs @@ -76,24 +76,29 @@ impl CallableParams { pub(crate) fn into_parameters(self, db: &TestDb) -> Parameters<'_> { match self { CallableParams::GradualForm => Parameters::gradual_form(), - CallableParams::List(params) => Parameters::new(params.into_iter().map(|param| { - let mut parameter = match param.kind { - ParamKind::PositionalOnly => Parameter::positional_only(param.name), - ParamKind::PositionalOrKeyword => { - Parameter::positional_or_keyword(param.name.unwrap()) + CallableParams::List(params) => Parameters::new( + db, + params.into_iter().map(|param| { + let mut parameter = match param.kind { + ParamKind::PositionalOnly => Parameter::positional_only(param.name), + ParamKind::PositionalOrKeyword => { + Parameter::positional_or_keyword(param.name.unwrap()) + } + ParamKind::Variadic => Parameter::variadic(param.name.unwrap()), + ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()), + ParamKind::KeywordVariadic => { + Parameter::keyword_variadic(param.name.unwrap()) + } + }; + if let Some(annotated_ty) = param.annotated_ty { + parameter = parameter.with_annotated_type(annotated_ty.into_type(db)); } - ParamKind::Variadic => Parameter::variadic(param.name.unwrap()), - ParamKind::KeywordOnly => Parameter::keyword_only(param.name.unwrap()), - ParamKind::KeywordVariadic => Parameter::keyword_variadic(param.name.unwrap()), - }; - if let Some(annotated_ty) = param.annotated_ty { - parameter = parameter.with_annotated_type(annotated_ty.into_type(db)); - } - if let Some(default_ty) = param.default_ty { - parameter = parameter.with_default_type(default_ty.into_type(db)); - } - parameter - })), + if let Some(default_ty) = param.default_ty { + parameter = parameter.with_default_type(default_ty.into_type(db)); + } + parameter + }), + ), } } } diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 8e3835b386a76..b440f972cc64e 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -199,7 +199,10 @@ impl<'db> ProtocolInterface<'db> { // Synthesize a read-only property (one that has a getter but no setter) // which returns the specified type from its getter. let property_getter_signature = Signature::new( - Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))]), + Parameters::new( + db, + [Parameter::positional_only(Some(Name::new_static("self")))], + ), Some(ty.normalized(db)), ); let property_getter = CallableType::single(db, property_getter_signature); diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index fb4ee9fc1b7f3..b4ecedbd5e7e0 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -544,11 +544,12 @@ impl<'db> Signature<'db> { // Discard the definition when normalizing, so that two equivalent signatures // with different `Definition`s share the same Salsa ID when normalized definition: None, - parameters: self - .parameters - .iter() - .map(|param| param.normalized_impl(db, visitor)) - .collect(), + parameters: Parameters::new( + db, + self.parameters + .iter() + .map(|param| param.normalized_impl(db, visitor)), + ), return_ty: self .return_ty .map(|return_ty| return_ty.normalized_impl(db, visitor)), @@ -618,7 +619,7 @@ impl<'db> Signature<'db> { parameters.next(); } - let mut parameters = Parameters::new(parameters); + let mut parameters = Parameters::new(db, parameters); let mut return_ty = self.return_ty; if let Some(self_type) = self_type { parameters = parameters.apply_type_mapping_impl( @@ -932,16 +933,37 @@ impl<'db> Signature<'db> { return ConstraintSet::from(relation.is_assignability()); } - if let ParametersKind::ParamSpec(typevar) = other.parameters.kind() { - let paramspec_value = - CallableType::paramspec_value(db, Signature::new(self.parameters().clone(), None)); - return ConstraintSet::constrain_typevar( - db, - typevar, - paramspec_value, - paramspec_value, - relation, - ); + match (self.parameters.kind(), other.parameters.kind()) { + (ParametersKind::ParamSpec(self_typevar), ParametersKind::ParamSpec(other_typevar)) => { + return ConstraintSet::from(self_typevar.is_same_typevar_as(db, other_typevar)); + } + (ParametersKind::ParamSpec(typevar), _) => { + let paramspec_value = CallableType::paramspec_value( + db, + Signature::new(other.parameters.clone(), None), + ); + return ConstraintSet::constrain_typevar( + db, + typevar, + paramspec_value, + paramspec_value, + relation, + ); + } + (_, ParametersKind::ParamSpec(typevar)) => { + let paramspec_value = CallableType::paramspec_value( + db, + Signature::new(self.parameters.clone(), None), + ); + return ConstraintSet::constrain_typevar( + db, + typevar, + paramspec_value, + paramspec_value, + relation, + ); + } + _ => {} } let mut parameters = ParametersZip { @@ -1301,19 +1323,40 @@ pub(crate) struct Parameters<'db> { } impl<'db> Parameters<'db> { - pub(crate) fn new(parameters: impl IntoIterator>) -> Self { + /// Create a new parameter list from an iterator of parameters. + /// + /// The kind of the parameter list is determined based on the provided parameters. + /// Specifically, if the parameters is made up of `*args` and `**kwargs` only, it checks + /// their annotated types to determine if they represent a gradual form or a `ParamSpec`. + pub(crate) fn new( + db: &'db dyn Db, + parameters: impl IntoIterator>, + ) -> Self { let value: Vec> = parameters.into_iter().collect(); - let kind = if value.len() == 2 - && value - .iter() - .any(|p| p.is_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic())) - && value.iter().any(|p| { - p.is_keyword_variadic() && p.annotated_type().is_none_or(|ty| ty.is_dynamic()) - }) { - ParametersKind::Gradual - } else { - ParametersKind::Standard - }; + let mut kind = ParametersKind::Standard; + if let [p1, p2] = value.as_slice() + && p1.is_variadic() + && p2.is_keyword_variadic() + { + match (p1.annotated_type(), p2.annotated_type()) { + (None | Some(Type::Dynamic(_)), None | Some(Type::Dynamic(_))) => { + kind = ParametersKind::Gradual; + } + (Some(Type::TypeVar(args_typevar)), Some(Type::TypeVar(kwargs_typevar))) => { + if let (Some(ParamSpecAttrKind::Args), Some(ParamSpecAttrKind::Kwargs)) = ( + args_typevar.paramspec_attr(db), + kwargs_typevar.paramspec_attr(db), + ) { + let typevar = args_typevar.without_paramspec_attr(db); + if typevar.is_same_typevar_as(db, kwargs_typevar.without_paramspec_attr(db)) + { + kind = ParametersKind::ParamSpec(typevar); + } + } + } + _ => {} + } + } Self { value, kind } } @@ -1577,6 +1620,7 @@ impl<'db> Parameters<'db> { }); Self::new( + db, positional_only .into_iter() .chain(positional_or_keyword) @@ -1694,12 +1738,6 @@ impl<'db, 'a> IntoIterator for &'a Parameters<'db> { } } -impl<'db> FromIterator> for Parameters<'db> { - fn from_iter>>(iter: T) -> Self { - Self::new(iter) - } -} - impl<'db> std::ops::Index for Parameters<'db> { type Output = Parameter<'db>; From 87cb422150e931e15802f0e0fa07c9d498b5c721 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 20 Nov 2025 19:31:35 +0530 Subject: [PATCH 12/59] Fix constraint set to represent `P1 = P2` --- crates/ty_python_semantic/src/types/signatures.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b4ecedbd5e7e0..682ec6b4d018d 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -935,7 +935,17 @@ impl<'db> Signature<'db> { match (self.parameters.kind(), other.parameters.kind()) { (ParametersKind::ParamSpec(self_typevar), ParametersKind::ParamSpec(other_typevar)) => { - return ConstraintSet::from(self_typevar.is_same_typevar_as(db, other_typevar)); + return if self_typevar.is_same_typevar_as(db, other_typevar) { + ConstraintSet::from(true) + } else { + ConstraintSet::constrain_typevar( + db, + self_typevar, + Type::TypeVar(other_typevar), + Type::TypeVar(other_typevar), + relation, + ) + }; } (ParametersKind::ParamSpec(typevar), _) => { let paramspec_value = CallableType::paramspec_value( From 6270f8263073b45d08532bca23ab277650771c91 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 21 Nov 2025 12:14:07 +0530 Subject: [PATCH 13/59] Raise invalid argument type when `P.args` is matched against `P.kwargs` and vice versa --- .../ty_python_semantic/src/types/call/bind.rs | 37 +++++-------------- .../ty_python_semantic/src/types/display.rs | 11 ++++-- .../src/types/signatures.rs | 7 ++-- 3 files changed, 21 insertions(+), 34 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 49c6f0601e4d1..ac88df36d112f 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -38,10 +38,9 @@ use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, - MemberLookupPolicy, NominalInstanceType, ParamSpecAttrKind, PropertyInstanceType, - SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, - UnionBuilder, UnionType, WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, - todo_type, + MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, + TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, + WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; @@ -2561,18 +2560,14 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { argument_type: Option>, ) -> Result<(), ()> { enum VariadicArgumentType<'db> { - ParamSpecArgs(Type<'db>), + ParamSpec(Type<'db>), Other(Cow<'db, TupleSpec<'db>>), None, } let variadic_type = match argument_type { Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { - if matches!(typevar.paramspec_attr(db), Some(ParamSpecAttrKind::Args)) { - VariadicArgumentType::ParamSpecArgs(paramspec) - } else { - VariadicArgumentType::None - } + VariadicArgumentType::ParamSpec(paramspec) } Some(ty) => { // TODO: `Type::iterate` internally handles unions, but in a lossy way. @@ -2587,10 +2582,10 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }; let (mut argument_types, length, variable_element) = match &variadic_type { - VariadicArgumentType::ParamSpecArgs(paramspec_args) => ( + VariadicArgumentType::ParamSpec(paramspec) => ( Either::Right(std::iter::empty()), TupleLength::unknown(), - Some(*paramspec_args), + Some(*paramspec), ), VariadicArgumentType::Other(tuple) => ( Either::Left(tuple.all_elements().copied()), @@ -2673,14 +2668,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { } } else { let value_type = match argument_type { - // TODO: Is this correct? - Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { - if matches!(typevar.paramspec_attr(db), Some(ParamSpecAttrKind::Kwargs)) { - paramspec - } else { - Type::unknown() - } - } + Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => paramspec, Some(ty) => { match ty .member_lookup_with_policy( @@ -3118,14 +3106,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let value_type = if let Type::TypeVar(typevar) = argument_type && typevar.is_paramspec(self.db) { - if matches!( - typevar.paramspec_attr(self.db), - Some(ParamSpecAttrKind::Kwargs) - ) { - argument_type - } else { - Type::unknown() - } + argument_type } else { // TODO: Instead of calling the `keys` and `__getitem__` methods, we should // instead get the constraints which satisfies the `SupportsKeysAndGetItem` diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 4a4b97663ea98..a4bf6eb60764b 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -969,7 +969,12 @@ impl DisplayGenericContext<'_> { if idx > 0 { f.write_str(", ")?; } - f.write_str(bound_typevar.typevar(self.db).name(self.db))?; + let typevar = bound_typevar.typevar(self.db); + if typevar.is_paramspec(self.db) { + write!(f, "**{}", typevar.name(self.db))?; + } else { + f.write_str(typevar.name(self.db))?; + } } f.write_char(']') } @@ -1937,7 +1942,7 @@ mod tests { } fn display_signature<'db>( - db: &dyn Db, + db: &'db dyn Db, parameters: impl IntoIterator>, return_ty: Option>, ) -> String { @@ -1947,7 +1952,7 @@ mod tests { } fn display_signature_multiline<'db>( - db: &dyn Db, + db: &'db dyn Db, parameters: impl IntoIterator>, return_ty: Option>, ) -> String { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 682ec6b4d018d..9b0371bba6d4c 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1294,10 +1294,11 @@ impl<'db> VarianceInferable<'db> for &Signature<'db> { } } +// TODO: the spec also allows signatures like `Concatenate[int, ...]` or `Concatenate[int, P]`, +// which have some number of required positional-only parameters followed by a gradual form or a +// `ParamSpec`. Our representation will need some adjustments to represent that. + /// The kind of parameter list represented. -// TODO: the spec also allows signatures like `Concatenate[int, ...]`, which have some number -// of required positional parameters followed by a gradual form. Our representation will need -// some adjustments to represent that. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] pub(crate) enum ParametersKind<'db> { /// A standard parameter list. From 8af8194604658f817bc9881b02b1cf68f8c6d725 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 25 Nov 2025 10:30:40 +0530 Subject: [PATCH 14/59] Display paramspec parameter list on single line --- crates/ty_python_semantic/src/types/display.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 4c6ab44886c7c..6dd47cb86457c 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1646,7 +1646,11 @@ struct DisplayParameters<'a, 'db> { impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let multiline = self.settings.multiline && self.parameters.len() > 1; + // For `ParamSpec` kind, the parameters still contain `*args` and `**kwargs`, but we + // display them as `**P` instead, so avoid multiline in that case. + // TODO: This might change once we support `Concatenate` + let multiline = + self.settings.multiline && self.parameters.len() > 1 && !self.parameters.is_paramspec(); // Opening parenthesis f.write_char('(')?; if multiline { From ecb4b8427039f061d4007de526079a38c987b433 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 25 Nov 2025 10:31:04 +0530 Subject: [PATCH 15/59] Merge two branches of callable in specialization builder --- .../ty_python_semantic/src/types/generics.rs | 56 +++++++++---------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index f163b834b3adb..7bfd833cdb4ea 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1657,36 +1657,32 @@ impl<'db> SpecializationBuilder<'db> { } } - (Type::Callable(formal_callable), Type::Callable(actual_callable)) => { - // We're only interested in a formal callable of the form `Callable[P, ...]` for - // now where `P` is a `ParamSpec`. - let [signature] = formal_callable.signatures(self.db).as_slice() else { - return Ok(()); - }; - let formal_parameters = signature.parameters(); - let ParametersKind::ParamSpec(typevar) = formal_parameters.kind() else { - return Ok(()); - }; - self.add_type_mapping( - typevar, - CallableType::overloaded_paramspec_value( - self.db, - actual_callable - .signatures(self.db) - .iter() - .map(|signature| Signature::new(signature.parameters().clone(), None)), - ), - polarity, - &mut f, - ); - } - - ( - Type::Callable(_), - Type::FunctionLiteral(_) | Type::BoundMethod(_) | Type::KnownBoundMethod(_), - ) => { - if let Some(actual_callable) = actual.try_upcast_to_callable(self.db) { - self.infer_map_impl(formal, actual_callable, polarity, &mut f)?; + (Type::Callable(formal_callable), _) => { + if let Some(Type::Callable(actual_callable)) = + actual.try_upcast_to_callable(self.db) + { + // We're only interested in a formal callable of the form `Callable[P, ...]` for + // now where `P` is a `ParamSpec`. + // TODO: This would need to be updated once we support `Concatenate` + // TODO: What to do for overloaded callables? + let [signature] = formal_callable.signatures(self.db).as_slice() else { + return Ok(()); + }; + let formal_parameters = signature.parameters(); + let ParametersKind::ParamSpec(typevar) = formal_parameters.kind() else { + return Ok(()); + }; + self.add_type_mapping( + typevar, + CallableType::overloaded_paramspec_value( + self.db, + actual_callable.signatures(self.db).iter().map(|signature| { + Signature::new(signature.parameters().clone(), None) + }), + ), + polarity, + &mut f, + ); } } From 1dca873f8125b875551d292b928e40bb3a7dfe15 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 25 Nov 2025 10:31:36 +0530 Subject: [PATCH 16/59] wip --- .../resources/mdtest/paramspec.md | 22 +++++++++++++++++++ .../ty_python_semantic/src/types/generics.rs | 4 ++++ .../src/types/signatures.rs | 8 ++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/resources/mdtest/paramspec.md b/crates/ty_python_semantic/resources/mdtest/paramspec.md index 254cd9d0731dd..dcaf17122e8b8 100644 --- a/crates/ty_python_semantic/resources/mdtest/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/paramspec.md @@ -171,3 +171,25 @@ def foo[**P: int]() -> None: def foo[**P = int]() -> None: pass ``` + +## Validating `ParamSpec` usage + +`ParamSpec` is only valid as the first element to `Callable` or the final element to `Concatenate`. + +```py +from typing import ParamSpec, Callable + +P = ParamSpec("P") + +def f( + # error + a1: P, + # error + a2: list[P], + a3: Callable[P, int], + # error + a4: Callable[[P], int], + # error + a5: Callable[..., P], +) -> None: ... +``` diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 7bfd833cdb4ea..244ddb612556d 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -135,6 +135,10 @@ impl<'db> BoundTypeVarInstance<'db> { db: &'db dyn Db, inferable: InferableTypeVars<'_, 'db>, ) -> bool { + if self.is_paramspec(db) { + tracing::debug!("bound type var: {}", self.identity(db).display(db)); + tracing::debug!("inferrable type vars: {}", inferable.display(db)); + } match inferable { InferableTypeVars::None => false, InferableTypeVars::One(typevars) => typevars.contains(&self.identity(db)), diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 9b0371bba6d4c..babaf7310d52f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -29,7 +29,7 @@ use crate::types::generics::{ }; use crate::types::infer::nearest_enclosing_class; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, ClassLiteral, + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, CallableTypeKind, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, @@ -190,6 +190,7 @@ impl<'db> CallableSignature<'db> { && let [self_signature] = self.overloads.as_slice() && let ParametersKind::ParamSpec(typevar) = self_signature.parameters.kind && let Some(Type::Callable(callable)) = specialization.get(db, typevar) + && matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) { return Self::from_overloads(callable.signatures(db).iter().map(|signature| { Signature::new(signature.parameters.clone(), self_signature.return_ty) @@ -933,6 +934,7 @@ impl<'db> Signature<'db> { return ConstraintSet::from(relation.is_assignability()); } + // TODO: Handle `Concatenate` match (self.parameters.kind(), other.parameters.kind()) { (ParametersKind::ParamSpec(self_typevar), ParametersKind::ParamSpec(other_typevar)) => { return if self_typevar.is_same_typevar_as(db, other_typevar) { @@ -1391,6 +1393,10 @@ impl<'db> Parameters<'db> { matches!(self.kind, ParametersKind::Gradual) } + pub(crate) const fn is_paramspec(&self) -> bool { + matches!(self.kind, ParametersKind::ParamSpec(_)) + } + /// Return todo parameters: (*args: Todo, **kwargs: Todo) pub(crate) fn todo() -> Self { Self { From 31438c0eb88f6fa1acb2162caf5115165823bfe5 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 25 Nov 2025 11:01:08 +0530 Subject: [PATCH 17/59] Fix merge errors --- crates/ty_python_semantic/src/types/infer/builder.rs | 9 +++++---- crates/ty_python_semantic/src/types/protocol_class.rs | 8 ++++++-- crates/ty_python_semantic/src/types/signatures.rs | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index b50a26e7376da..9e6fd18e1b112 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -62,10 +62,10 @@ use crate::types::diagnostic::{ INVALID_METACLASS, INVALID_NAMED_TUPLE, INVALID_NEWTYPE, INVALID_OVERLOAD, INVALID_PARAMETER_DEFAULT, INVALID_PARAMSPEC, INVALID_PROTOCOL, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, - NON_SUBSCRIPTABLE, POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_IMPORT, - SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, - UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, USELESS_OVERLOAD_BODY, - hint_if_stdlib_attribute_exists_on_other_versions, + NON_SUBSCRIPTABLE, POSSIBLY_MISSING_ATTRIBUTE, POSSIBLY_MISSING_IMPLICIT_CALL, + POSSIBLY_MISSING_IMPORT, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, + UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, + USELESS_OVERLOAD_BODY, hint_if_stdlib_attribute_exists_on_other_versions, hint_if_stdlib_submodule_exists_on_other_versions, report_attempted_protocol_instantiation, report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, report_duplicate_bases, report_implicit_return_type, report_index_out_of_bounds, @@ -110,6 +110,7 @@ use crate::types::{ TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, binding_type, todo_type, }; +use crate::types::{CallableTypes, liskov}; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; use crate::{Db, FxIndexSet, FxOrderSet, Program}; diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 23f637d7339a6..931280a1a4b63 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; -use crate::types::TypeContext; +use crate::types::{CallableTypeKind, TypeContext}; use crate::{ Db, FxOrderSet, place::{Definedness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, @@ -926,5 +926,9 @@ fn protocol_bind_self<'db>( callable: CallableType<'db>, self_type: Option>, ) -> CallableType<'db> { - CallableType::new(db, callable.signatures(db).bind_self(db, self_type), false) + CallableType::new( + db, + callable.signatures(db).bind_self(db, self_type), + CallableTypeKind::Regular, + ) } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 5a58519e37a21..4ad91112e2133 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -29,7 +29,7 @@ use crate::types::generics::{ }; use crate::types::infer::nearest_enclosing_class; use crate::types::{ - ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableType, CallableTypeKind, ClassLiteral, + ApplyTypeMappingVisitor, BoundTypeVarInstance, CallableTypeKind, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, NormalizedVisitor, ParamSpecAttrKind, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, From 3680d94c07a2bbd07320f1e1203049614177938f Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 27 Nov 2025 14:11:06 +0530 Subject: [PATCH 18/59] Try using a sub-call to evaluate paramspec --- .../src/types/call/arguments.rs | 8 ++ .../ty_python_semantic/src/types/call/bind.rs | 109 +++++++++++++++++- .../ty_python_semantic/src/types/generics.rs | 19 ++- .../src/types/signatures.rs | 40 +++++++ 4 files changed, 160 insertions(+), 16 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index 6da85184eae83..cc8f3772715b1 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -150,6 +150,14 @@ impl<'a, 'db> CallArguments<'a, 'db> { (self.arguments.iter().copied()).zip(self.types.iter_mut()) } + /// Create a new [`CallArguments`] starting from the specified index. + pub(super) fn start_from(&self, index: usize) -> Self { + Self { + arguments: self.arguments[index..].to_vec(), + types: self.types[index..].to_vec(), + } + } + /// Returns an iterator on performing [argument type expansion]. /// /// Each element of the iterator represents a set of argument lists, where each argument list diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 35fa85387cc3d..41d7036463d65 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -36,11 +36,11 @@ use crate::types::generics::{ use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters}; use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use crate::types::{ - BoundMethodType, BoundTypeVarIdentity, ClassLiteral, DATACLASS_FLAGS, DataclassFlags, - DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType, - MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType, - TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType, - WrapperDescriptorKind, enums, ide_support, todo_type, + BoundMethodType, BoundTypeVarIdentity, BoundTypeVarInstance, CallableTypeKind, ClassLiteral, + DATACLASS_FLAGS, DataclassFlags, DataclassParams, FieldInstance, KnownBoundMethodType, + KnownClass, KnownInstanceType, MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, + SpecialFormType, TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, + UnionBuilder, UnionType, WrapperDescriptorKind, enums, ide_support, todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion}; @@ -2757,6 +2757,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, + signature_type: Type<'db>, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, argument_matches: &'a [MatchedArgument<'db>], @@ -2774,6 +2775,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { #[expect(clippy::too_many_arguments)] fn new( db: &'db dyn Db, + signature_type: Type<'db>, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, argument_matches: &'a [MatchedArgument<'db>], @@ -2785,6 +2787,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ) -> Self { Self { db, + signature_type, signature, arguments, argument_matches, @@ -3033,9 +3036,23 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } fn check_argument_types(&mut self) { + let paramspec = self + .signature + .parameters() + .find_paramspec_from_args_kwargs(self.db); + for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { + if let Some(paramspec) = paramspec { + if self.try_paramspec_evaluation_at(argument_index, paramspec) { + // Once we find an argument that matches the `ParamSpec`, we can stop checking + // the remaining arguments since `ParamSpec` should always be the last + // parameter. + return; + } + } + match argument { Argument::Variadic => self.check_variadic_argument_type( argument_index, @@ -3061,6 +3078,87 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } } + + if let Some(paramspec) = paramspec { + // If we reach here, none of the arguments matched the `ParamSpec` parameter, but the + // `ParamSpec` could specialize to a parameter list containing some parameters. + self.evaluate_paramspec_sub_call(None, paramspec); + } + } + + /// Try to evaluate a `ParamSpec` sub-call at the given argument index. + /// + /// If the argument at the given index matches a parameter which is a `ParamSpec`, invoke + /// a sub-call starting from that argument index and return `true`. Otherwise, return `false`. + fn try_paramspec_evaluation_at( + &mut self, + argument_index: usize, + paramspec: BoundTypeVarInstance<'db>, + ) -> bool { + let [parameter_index] = self.argument_matches[argument_index].parameters.as_slice() else { + return false; + }; + + if !self.signature.parameters()[*parameter_index] + .annotated_type() + .is_some_and(|ty| matches!(ty, Type::TypeVar(typevar) if typevar.is_paramspec(self.db))) + { + return false; + } + + self.evaluate_paramspec_sub_call(Some(argument_index), paramspec) + } + + /// Invoke a sub-call for the given `ParamSpec` type variable, using the remaining arguments. + /// + /// The remaining arguments start from `argument_index` if provided, otherwise no arguments + /// are passed. + /// + /// Returns `false` if the specialization does not contain a mapping for the given `paramspec`. + fn evaluate_paramspec_sub_call( + &mut self, + argument_index: Option, + paramspec: BoundTypeVarInstance<'db>, + ) -> bool { + let Some(Type::Callable(callable)) = self + .specialization + .and_then(|specialization| specialization.get(self.db, paramspec)) + else { + return false; + }; + + if !matches!(callable.kind(self.db), CallableTypeKind::ParamSpecValue) { + return false; + } + + // TODO: Support overloads? + let [signature] = callable.signatures(self.db).overloads.as_slice() else { + return false; + }; + + let sub_arguments = if let Some(argument_index) = argument_index { + self.arguments.start_from(argument_index) + } else { + CallArguments::none() + }; + + // TODO: What should be the `signature_type` here? + let bindings = match Bindings::from(Binding::single(self.signature_type, signature.clone())) + .match_parameters(self.db, &sub_arguments) + .check_types(self.db, &sub_arguments, self.call_expression_tcx, &[]) + { + Ok(bindings) => Box::new(bindings), + Err(CallError(_, bindings)) => bindings, + }; + + // SAFETY: `bindings` was created from a single binding above. + let [binding] = bindings.single_element().unwrap().overloads.as_slice() else { + unreachable!("ParamSpec sub-call should only contain a single binding"); + }; + + self.errors.extend(binding.errors.iter().cloned()); + + true } fn check_variadic_argument_type( @@ -3349,6 +3447,7 @@ impl<'db> Binding<'db> { ) { let mut checker = ArgumentTypeChecker::new( db, + self.signature_type, &self.signature, arguments, &self.argument_matches, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4bb5e548cbe52..eccd50a730a9e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -18,11 +18,11 @@ use crate::types::tuple::{TupleSpec, TupleType, walk_tuple_type}; use crate::types::visitor::{TypeCollector, TypeVisitor, walk_type_with_recursion_guard}; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarIdentity, BoundTypeVarInstance, CallableSignature, - CallableType, CallableTypeKind, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, - IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, - NormalizedVisitor, Signature, Type, TypeContext, TypeMapping, TypeRelation, - TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, - UnionType, declaration_type, walk_bound_type_var_type, + CallableType, CallableTypeKind, CallableTypes, ClassLiteral, FindLegacyTypeVarsVisitor, + HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, + MaterializationKind, NormalizedVisitor, Signature, Type, TypeContext, TypeMapping, + TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, TypeVarKind, + TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type, }; use crate::{Db, FxOrderMap, FxOrderSet}; @@ -135,10 +135,6 @@ impl<'db> BoundTypeVarInstance<'db> { db: &'db dyn Db, inferable: InferableTypeVars<'_, 'db>, ) -> bool { - if self.is_paramspec(db) { - tracing::debug!("bound type var: {}", self.identity(db).display(db)); - tracing::debug!("inferrable type vars: {}", inferable.display(db)); - } match inferable { InferableTypeVars::None => false, InferableTypeVars::One(typevars) => typevars.contains(&self.identity(db)), @@ -1648,8 +1644,9 @@ impl<'db> SpecializationBuilder<'db> { } (Type::Callable(formal_callable), _) => { - if let Some(callable_types) = actual.try_upcast_to_callable(self.db) - && let Some(actual_callable) = callable_types.exactly_one() + if let Some(actual_callable) = actual + .try_upcast_to_callable(self.db) + .and_then(CallableTypes::exactly_one) { // We're only interested in a formal callable of the form `Callable[P, ...]` for // now where `P` is a `ParamSpec`. diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 11f749508dec1..b76dbcb22aa2f 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1409,6 +1409,8 @@ pub(crate) enum ParametersKind<'db> { /// /// Note that this is distinct from a parameter list _containing_ a `ParamSpec` which is /// considered a standard parameter list that just contains a `ParamSpec`. + // TODO: Maybe we should use `find_paramspec_from_args_kwargs` instead of storing the typevar + // here? ParamSpec(BoundTypeVarInstance<'db>), } @@ -1555,6 +1557,44 @@ impl<'db> Parameters<'db> { } } + /// Returns the bound `ParamSpec` type variable if the parameters contain a `ParamSpec`. + pub(crate) fn find_paramspec_from_args_kwargs( + &self, + db: &'db dyn Db, + ) -> Option> { + let [.., maybe_args, maybe_kwargs] = self.value.as_slice() else { + return None; + }; + + if !maybe_args.is_variadic() || !maybe_kwargs.is_keyword_variadic() { + return None; + } + + let (Type::TypeVar(args_typevar), Type::TypeVar(kwargs_typevar)) = + (maybe_args.annotated_type()?, maybe_kwargs.annotated_type()?) + else { + return None; + }; + + if matches!( + ( + args_typevar.paramspec_attr(db), + kwargs_typevar.paramspec_attr(db) + ), + ( + Some(ParamSpecAttrKind::Args), + Some(ParamSpecAttrKind::Kwargs) + ) + ) { + let typevar = args_typevar.without_paramspec_attr(db); + if typevar.is_same_typevar_as(db, kwargs_typevar.without_paramspec_attr(db)) { + return Some(typevar); + } + } + + None + } + fn from_parameters( db: &'db dyn Db, definition: Definition<'db>, From 4984259e7c1974c694095b2b415ae25edaa827f6 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 27 Nov 2025 20:01:31 +0530 Subject: [PATCH 19/59] Support `ParamSpec` in explicit specialization --- .../src/types/infer/builder.rs | 136 ++++++++++++------ .../types/infer/builder/type_expression.rs | 2 +- 2 files changed, 96 insertions(+), 42 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 7f38fac07f9e0..a28923c9638d8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -20,6 +20,7 @@ use super::{ infer_unpack_types, }; use crate::diagnostic::format_enumeration; +use crate::lint::LintMetadata; use crate::module_name::{ModuleName, ModuleNameResolutionError}; use crate::module_resolver::{ KnownModule, ModuleResolveMode, file_to_module, resolve_module, search_paths, @@ -3403,20 +3404,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let previous_deferred_state = std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); - let default_ty = self.infer_paramspec_default(default); + let default_ty = self + .infer_paramspec_value(default, ParamSpecValuePosition::Default) + .unwrap_or_else(Type::unknown); self.store_expression_type(default, default_ty); self.deferred_state = previous_deferred_state; } - fn infer_paramspec_default(&mut self, default: &ast::Expr) -> Type<'db> { - // This is the same logic as `TypeInferenceBuilder::infer_callable_parameter_types` except - // for the subscript branch which is required for `Concatenate` but that cannot be - // specified in this context. - match default { - ast::Expr::EllipsisLiteral(_) => Type::paramspec_value_callable( + fn infer_paramspec_value( + &mut self, + expr: &ast::Expr, + position: ParamSpecValuePosition, + ) -> Option> { + match expr { + ast::Expr::EllipsisLiteral(_) => Some(Type::paramspec_value_callable( self.db(), Signature::new(Parameters::gradual_form(), None), - ), + )), ast::Expr::List(ast::ExprList { elts, .. }) => { let mut parameter_types = Vec::with_capacity(elts.len()); @@ -3446,11 +3450,26 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) }; - Type::paramspec_value_callable(self.db(), Signature::new(parameters, None)) + Some(Type::paramspec_value_callable( + self.db(), + Signature::new(parameters, None), + )) + } + ast::Expr::Subscript(subscript) + if matches!(position, ParamSpecValuePosition::ExplicitSpecialization) => + { + let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); + self.infer_subscript_type_expression(subscript, value_ty); + // TODO: Support `Concatenate[...]` + Some(Type::paramspec_value_callable( + self.db(), + Signature::new(Parameters::todo(), None), + )) } ast::Expr::Name(name) => { let name_ty = self.infer_name_load(name); let is_paramspec = match name_ty { + Type::TypeVar(typevar) => typevar.is_paramspec(self.db()), Type::KnownInstance(known_instance) => { known_instance.class(self.db()) == KnownClass::ParamSpec } @@ -3460,25 +3479,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { _ => false, }; if is_paramspec { - name_ty + Some(name_ty) } else { - if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default) { - builder.into_diagnostic( - "The default value to `ParamSpec` must be either a list of types, \ - `ParamSpec`, or `...`", - ); + if let Some(builder) = + self.context.report_lint(position.diagnostic_code(), expr) + { + builder.into_diagnostic(position.diagnostic_message()); } - Type::unknown() + None } } _ => { - if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default) { - builder.into_diagnostic( - "The default value to `ParamSpec` must be either a list of types, \ - `ParamSpec`, or `...`", - ); + if let Some(builder) = self.context.report_lint(position.diagnostic_code(), expr) { + builder.into_diagnostic(position.diagnostic_message()); } - Type::unknown() + None } } } @@ -5454,7 +5469,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(default) = arguments.find_keyword("default") { if let Some(KnownClass::ParamSpec) = known_class { - self.infer_paramspec_default(&default.value); + self.infer_paramspec_value(&default.value, ParamSpecValuePosition::Default) + .unwrap_or_else(Type::unknown); } else { self.infer_type_expression(&default.value); } @@ -11318,22 +11334,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let slice_node = subscript.slice.as_ref(); - // Extract type arguments from the subscript - let type_arguments: Vec> = match slice_node { - ast::Expr::Tuple(tuple) => { - let types: Vec<_> = tuple - .elts - .iter() - .map(|elt| self.infer_type_expression(elt)) - .collect(); - self.store_expression_type( - slice_node, - Type::heterogeneous_tuple(db, types.iter().copied()), - ); - types - } - _ => vec![self.infer_type_expression(slice_node)], + let type_arguments = match slice_node { + ast::Expr::Tuple(tuple) => tuple.elts.as_slice(), + _ => std::slice::from_ref(slice_node), }; + let mut inferred_type_arguments = Vec::with_capacity(type_arguments.len()); let typevars = generic_context.variables(db); let typevars_len = typevars.len(); @@ -11358,10 +11363,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { for (index, item) in typevars.zip_longest(type_arguments.iter()).enumerate() { match item { - EitherOrBoth::Both(typevar, &provided_type) => { + EitherOrBoth::Both(typevar, expr) => { if typevar.default_type(db).is_some() { typevar_with_defaults += 1; } + + // TODO: Update this once `TypeVarTuple` support is added. + let provided_type = if typevar.is_paramspec(db) { + if let Some(paramspec_value) = self.infer_paramspec_value( + expr, + ParamSpecValuePosition::ExplicitSpecialization, + ) { + paramspec_value + } else { + has_error = true; + Type::unknown() + } + } else { + self.infer_type_expression(expr) + }; + + inferred_type_arguments.push(provided_type); + match typevar.typevar(db).bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { if provided_type @@ -11415,17 +11438,20 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } None => {} } + specialization_types.push(Some(provided_type)); } EitherOrBoth::Left(typevar) => { if typevar.default_type(db).is_none() { + // This is an error case, so no need to push into the specialization types. missing_typevars.push(typevar); } else { typevar_with_defaults += 1; + specialization_types.push(None); } - specialization_types.push(None); } - EitherOrBoth::Right(_) => { + EitherOrBoth::Right(expr) => { + inferred_type_arguments.push(self.infer_type_expression(expr)); first_excess_type_argument_index.get_or_insert(index); } } @@ -12175,6 +12201,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ParamSpecValuePosition { + Default, + ExplicitSpecialization, +} + +impl ParamSpecValuePosition { + fn diagnostic_code(self) -> &'static LintMetadata { + match self { + ParamSpecValuePosition::Default => &INVALID_PARAMSPEC, + ParamSpecValuePosition::ExplicitSpecialization => &INVALID_TYPE_ARGUMENTS, + } + } + + fn diagnostic_message(self) -> &'static str { + match self { + ParamSpecValuePosition::Default => { + "The default value to `ParamSpec` must be either \ + a list of types, `ParamSpec`, or `...`" + } + ParamSpecValuePosition::ExplicitSpecialization => { + "Type argument for `ParamSpec` must be either \ + a list of types, `ParamSpec`, `Concatenate`, or `...`" + } + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum GenericContextError { /// It's invalid to subscript `Generic` or `Protocol` with this type diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index dc6587a5999c4..056d5bfbdb8bb 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -755,7 +755,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { } } - fn infer_subscript_type_expression( + pub(super) fn infer_subscript_type_expression( &mut self, subscript: &ast::ExprSubscript, value_ty: Type<'db>, From 9032d07c112fb647a9807b04d3d0d3e61051236c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 27 Nov 2025 21:03:38 +0530 Subject: [PATCH 20/59] Update `paramspec_value` to take parameters instead of signature --- crates/ty_python_semantic/src/types.rs | 11 +++++++---- crates/ty_python_semantic/src/types/infer/builder.rs | 9 +++------ crates/ty_python_semantic/src/types/signatures.rs | 10 ++-------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 1315cf9d44fe5..3480d56e68520 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -11503,9 +11503,9 @@ impl<'db> Type<'db> { /// type variable. pub(crate) fn paramspec_value_callable( db: &'db dyn Db, - signature: Signature<'db>, + parameters: Parameters<'db>, ) -> Type<'db> { - Type::Callable(CallableType::paramspec_value(db, signature)) + Type::Callable(CallableType::paramspec_value(db, parameters)) } } @@ -11526,10 +11526,13 @@ impl<'db> CallableType<'db> { ) } - pub(crate) fn paramspec_value(db: &'db dyn Db, signature: Signature<'db>) -> CallableType<'db> { + pub(crate) fn paramspec_value( + db: &'db dyn Db, + parameters: Parameters<'db>, + ) -> CallableType<'db> { CallableType::new( db, - CallableSignature::single(signature), + CallableSignature::single(Signature::new(parameters, None)), CallableTypeKind::ParamSpecValue, ) } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index a28923c9638d8..540c0f6d441e7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3419,7 +3419,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match expr { ast::Expr::EllipsisLiteral(_) => Some(Type::paramspec_value_callable( self.db(), - Signature::new(Parameters::gradual_form(), None), + Parameters::gradual_form(), )), ast::Expr::List(ast::ExprList { elts, .. }) => { let mut parameter_types = Vec::with_capacity(elts.len()); @@ -3450,10 +3450,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) }; - Some(Type::paramspec_value_callable( - self.db(), - Signature::new(parameters, None), - )) + Some(Type::paramspec_value_callable(self.db(), parameters)) } ast::Expr::Subscript(subscript) if matches!(position, ParamSpecValuePosition::ExplicitSpecialization) => @@ -3463,7 +3460,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // TODO: Support `Concatenate[...]` Some(Type::paramspec_value_callable( self.db(), - Signature::new(Parameters::todo(), None), + Parameters::todo(), )) } ast::Expr::Name(name) => { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index b76dbcb22aa2f..13eff30490b2c 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1034,10 +1034,7 @@ impl<'db> Signature<'db> { }; } (ParametersKind::ParamSpec(typevar), _) => { - let paramspec_value = Type::paramspec_value_callable( - db, - Signature::new(other.parameters.clone(), None), - ); + let paramspec_value = Type::paramspec_value_callable(db, other.parameters.clone()); return ConstraintSet::constrain_typevar( db, typevar, @@ -1047,10 +1044,7 @@ impl<'db> Signature<'db> { ); } (_, ParametersKind::ParamSpec(typevar)) => { - let paramspec_value = Type::paramspec_value_callable( - db, - Signature::new(self.parameters.clone(), None), - ); + let paramspec_value = Type::paramspec_value_callable(db, self.parameters.clone()); return ConstraintSet::constrain_typevar( db, typevar, From b37f5ce212578203217e0e18ae353008db44ed3e Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 28 Nov 2025 15:14:15 +0530 Subject: [PATCH 21/59] Correctly implement the paramspec type / value inference --- crates/ty_python_semantic/src/types.rs | 64 +++-- .../ty_python_semantic/src/types/generics.rs | 7 + .../src/types/infer/builder.rs | 264 ++++++++++++------ 3 files changed, 226 insertions(+), 109 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3480d56e68520..e655d2221c657 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7030,14 +7030,6 @@ impl<'db> Type<'db> { // TODO: A `ParamSpec` type variable cannot be used in type expressions. This // requires storing additional context as it's allowed in some places // (`Concatenate`, `Callable`) but not others. - // if typevar.kind(db).is_paramspec() { - // return Err(InvalidTypeExpressionError { - // invalid_expressions: smallvec::smallvec_inline![ - // InvalidTypeExpression::InvalidType(*self, scope_id) - // ], - // fallback_type: Type::unknown(), - // }); - // } let index = semantic_index(db, scope_id.file(db)); Ok(bind_typevar( db, @@ -7264,12 +7256,6 @@ impl<'db> Type<'db> { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), - Some(KnownClass::ParamSpecArgs) => { - Ok(todo_type!("Support for `typing.ParamSpecArgs`")) - } - Some(KnownClass::ParamSpecKwargs) => { - Ok(todo_type!("Support for `typing.ParamSpecKwargs`")) - } Some(KnownClass::TypeVarTuple) => Ok(todo_type!( "Support for `typing.TypeVarTuple` instances in type expressions" )), @@ -9444,6 +9430,32 @@ impl<'db> TypeVarInstance<'db> { #[salsa::tracked(cycle_fn=lazy_default_cycle_recover, cycle_initial=lazy_default_cycle_initial, heap_size=ruff_memory_usage::heap_size)] fn lazy_default(self, db: &'db dyn Db) -> Option> { + fn convert_type_to_paramspec_value<'db>(db: &'db dyn Db, ty: Type<'db>) -> Type<'db> { + let parameters = match ty { + Type::NominalInstance(nominal_instance) + if nominal_instance.has_known_class(db, KnownClass::EllipsisType) => + { + Parameters::gradual_form() + } + Type::NominalInstance(nominal_instance) => nominal_instance + .own_tuple_spec(db) + .map_or_else(Parameters::unknown, |tuple_spec| { + Parameters::new( + db, + tuple_spec.all_elements().map(|ty| { + Parameter::positional_only(None).with_annotated_type(*ty) + }), + ) + }), + Type::Dynamic(DynamicType::Todo(_)) => Parameters::todo(), + Type::TypeVar(typevar) if typevar.is_paramspec(db) => { + Parameters::paramspec(db, typevar) + } + _ => Parameters::unknown(), + }; + Type::paramspec_value_callable(db, parameters) + } + let definition = self.definition(db)?; let module = parsed_module(db, definition.file(db)).load(db); match definition.kind(db) { @@ -9456,27 +9468,35 @@ impl<'db> TypeVarInstance<'db> { typevar_node.default.as_ref()?, )) } - // legacy typevar + // legacy typevar / ParamSpec DefinitionKind::Assignment(assignment) => { let call_expr = assignment.value(&module).as_call_expr()?; + let func_ty = definition_expression_type(db, definition, &call_expr.func); + let known_class = func_ty.as_class_literal().and_then(|cls| cls.known(db)); let expr = &call_expr.arguments.find_keyword("default")?.value; - Some(definition_expression_type(db, definition, expr)) + let default_type = definition_expression_type(db, definition, expr); + if matches!(known_class, Some(KnownClass::ParamSpec)) { + Some(convert_type_to_paramspec_value(db, default_type)) + } else { + Some(default_type) + } } // PEP 695 ParamSpec DefinitionKind::ParamSpec(paramspec) => { let paramspec_node = paramspec.node(&module); - Some(definition_expression_type( - db, - definition, - paramspec_node.default.as_ref()?, - )) + let default_ty = + definition_expression_type(db, definition, paramspec_node.default.as_ref()?); + Some(convert_type_to_paramspec_value(db, default_ty)) } _ => None, } } pub fn bind_pep695(self, db: &'db dyn Db) -> Option> { - if self.identity(db).kind(db) != TypeVarKind::Pep695 { + if !matches!( + self.identity(db).kind(db), + TypeVarKind::Pep695 | TypeVarKind::Pep695ParamSpec + ) { return None; } let typevar_definition = self.definition(db)?; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index eccd50a730a9e..246eaf5b33028 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -319,6 +319,13 @@ impl<'db> GenericContext<'db> { self.variables_inner(db).values().copied() } + /// Returns `true` if this generic context contains exactly one `ParamSpec` type variable. + pub(crate) fn exactly_one_paramspec(self, db: &'db dyn Db) -> bool { + self.variables(db) + .exactly_one() + .is_ok_and(|bound_typevar| bound_typevar.is_paramspec(db)) + } + fn variable_from_type_param( db: &'db dyn Db, index: &'db SemanticIndex<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 540c0f6d441e7..09cf140e1e45a 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -20,7 +20,6 @@ use super::{ infer_unpack_types, }; use crate::diagnostic::format_enumeration; -use crate::lint::LintMetadata; use crate::module_name::{ModuleName, ModuleNameResolutionError}; use crate::module_resolver::{ KnownModule, ModuleResolveMode, file_to_module, resolve_module, search_paths, @@ -3404,24 +3403,81 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }; let previous_deferred_state = std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred); - let default_ty = self - .infer_paramspec_value(default, ParamSpecValuePosition::Default) - .unwrap_or_else(Type::unknown); - self.store_expression_type(default, default_ty); + self.infer_paramspec_default(default); self.deferred_state = previous_deferred_state; } - fn infer_paramspec_value( + fn infer_paramspec_default(&mut self, default_expr: &ast::Expr) { + match default_expr { + ast::Expr::EllipsisLiteral(ellipsis) => { + // TODO: This should use `infer_type_expression` but that uses a `todo` type for + // inferring ellipsis literals in type expressions, which is not what we want here. + let ty = self.infer_ellipsis_literal_expression(ellipsis); + self.store_expression_type(default_expr, ty); + return; + } + ast::Expr::List(ast::ExprList { elts, .. }) => { + let types = elts + .iter() + .map(|elt| self.infer_type_expression(elt)) + .collect::>(); + // N.B. We cannot represent a hetrogeneous list of types in our type system, so we + // use a heterogeneous tuple type to represent the list of types instead. + self.store_expression_type( + default_expr, + Type::heterogeneous_tuple(self.db(), types), + ); + return; + } + ast::Expr::Name(_) => { + let ty = self.infer_type_expression(default_expr); + let is_paramspec = match ty { + Type::TypeVar(typevar) => typevar.is_paramspec(self.db()), + Type::KnownInstance(known_instance) => { + known_instance.class(self.db()) == KnownClass::ParamSpec + } + _ => false, + }; + if is_paramspec { + return; + } + } + _ => {} + } + if let Some(builder) = self.context.report_lint(&INVALID_PARAMSPEC, default_expr) { + builder.into_diagnostic( + "The default value to `ParamSpec` must be either \ + a list of types, `ParamSpec`, or `...`", + ); + } + } + + fn infer_paramspec_explicit_specialization_value( &mut self, expr: &ast::Expr, - position: ParamSpecValuePosition, - ) -> Option> { + exactly_one_paramspec: bool, + ) -> Result, ()> { + let db = self.db(); + match expr { - ast::Expr::EllipsisLiteral(_) => Some(Type::paramspec_value_callable( - self.db(), - Parameters::gradual_form(), - )), - ast::Expr::List(ast::ExprList { elts, .. }) => { + ast::Expr::EllipsisLiteral(ellipsis) => { + let ty = self.infer_ellipsis_literal_expression(ellipsis); + self.store_expression_type(expr, ty); + return Ok(Type::paramspec_value_callable( + db, + Parameters::gradual_form(), + )); + } + + ast::Expr::Tuple(ast::ExprTuple { elts, .. }) + | ast::Expr::List(ast::ExprList { elts, .. }) => { + // This should be taken care by the caller. + debug_assert!( + expr.is_tuple_expr() && exactly_one_paramspec, + "Inferring ParamSpec value during explicit specialization for a \ + tuple expression should only happen when it contains exactly one ParamSpec" + ); + let mut parameter_types = Vec::with_capacity(elts.len()); // Whether to infer `Todo` for the parameters @@ -3438,6 +3494,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter_types.push(param_type); } + // N.B. We cannot represent a hetrogeneous list of types in our type system, so we + // use a heterogeneous tuple type to represent the list of types instead. + self.store_expression_type( + expr, + Type::heterogeneous_tuple(db, parameter_types.iter().copied()), + ); + let parameters = if return_todo { // TODO: `Unpack` Parameters::todo() @@ -3450,49 +3513,85 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) }; - Some(Type::paramspec_value_callable(self.db(), parameters)) + return Ok(Type::paramspec_value_callable(db, parameters)); } - ast::Expr::Subscript(subscript) - if matches!(position, ParamSpecValuePosition::ExplicitSpecialization) => - { - let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); - self.infer_subscript_type_expression(subscript, value_ty); + + ast::Expr::Subscript(_) => { + self.infer_type_expression(expr); // TODO: Support `Concatenate[...]` - Some(Type::paramspec_value_callable( - self.db(), - Parameters::todo(), - )) + return Ok(Type::paramspec_value_callable(db, Parameters::todo())); } - ast::Expr::Name(name) => { - let name_ty = self.infer_name_load(name); - let is_paramspec = match name_ty { - Type::TypeVar(typevar) => typevar.is_paramspec(self.db()), - Type::KnownInstance(known_instance) => { - known_instance.class(self.db()) == KnownClass::ParamSpec + + ast::Expr::Name(_) => { + let param_type = self.infer_type_expression(expr); + + match param_type { + Type::TypeVar(typevar) if typevar.is_paramspec(db) => { + return Ok(Type::paramspec_value_callable( + db, + Parameters::paramspec(db, typevar), + )); } - Type::NominalInstance(nominal) => { - nominal.has_known_class(self.db(), KnownClass::ParamSpec) + + Type::KnownInstance(known_instance) + if known_instance.class(self.db()) == KnownClass::ParamSpec => + { + // TODO: Raise diagnostic: "ParamSpec "P" is unbound" + return Err(()); } - _ => false, - }; - if is_paramspec { - Some(name_ty) - } else { - if let Some(builder) = - self.context.report_lint(position.diagnostic_code(), expr) + + Type::NominalInstance(nominal) + if nominal.has_known_class(self.db(), KnownClass::ParamSpec) => { - builder.into_diagnostic(position.diagnostic_message()); + return Ok(Type::paramspec_value_callable( + db, + Parameters::new( + self.db(), + [ + Parameter::positional_only(None) + .with_annotated_type(param_type), + ], + ), + )); + } + + _ => { + // Square brackets are optional when `ParamSpec` is the only type variable + // being specialized. This means that a single name expression represents a + // parameter list with a single parameter. For example, + // + // ```python + // class OnlyParamSpec[**P]: ... + // + // OnlyParamSpec[int] # P: (int, /) + // ``` + if exactly_one_paramspec { + let parameters = if param_type.is_todo() { + Parameters::todo() + } else { + Parameters::new( + self.db(), + [Parameter::positional_only(None) + .with_annotated_type(param_type)], + ) + }; + return Ok(Type::paramspec_value_callable(db, parameters)); + } } - None - } - } - _ => { - if let Some(builder) = self.context.report_lint(position.diagnostic_code(), expr) { - builder.into_diagnostic(position.diagnostic_message()); } - None } + + _ => self.store_expression_type(expr, Type::unknown()), } + + if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) { + builder.into_diagnostic( + "Type argument for `ParamSpec` must be either \ + a list of types, `ParamSpec`, `Concatenate`, or `...`", + ); + } + + Err(()) } fn infer_typevartuple_definition( @@ -5466,8 +5565,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } if let Some(default) = arguments.find_keyword("default") { if let Some(KnownClass::ParamSpec) = known_class { - self.infer_paramspec_value(&default.value, ParamSpecValuePosition::Default) - .unwrap_or_else(Type::unknown); + self.infer_paramspec_default(&default.value); } else { self.infer_type_expression(&default.value); } @@ -11331,9 +11429,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); let slice_node = subscript.slice.as_ref(); - let type_arguments = match slice_node { - ast::Expr::Tuple(tuple) => tuple.elts.as_slice(), - _ => std::slice::from_ref(slice_node), + let exactly_one_paramspec = generic_context.exactly_one_paramspec(db); + let (type_arguments, store_inferred_type_arguments) = match slice_node { + ast::Expr::Tuple(tuple) => { + if exactly_one_paramspec { + (std::slice::from_ref(slice_node), false) + } else { + (tuple.elts.as_slice(), true) + } + } + _ => (std::slice::from_ref(slice_node), false), }; let mut inferred_type_arguments = Vec::with_capacity(type_arguments.len()); @@ -11365,16 +11470,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { typevar_with_defaults += 1; } - // TODO: Update this once `TypeVarTuple` support is added. let provided_type = if typevar.is_paramspec(db) { - if let Some(paramspec_value) = self.infer_paramspec_value( + match self.infer_paramspec_explicit_specialization_value( expr, - ParamSpecValuePosition::ExplicitSpecialization, + exactly_one_paramspec, ) { - paramspec_value - } else { - has_error = true; - Type::unknown() + Ok(paramspec_value) => paramspec_value, + Err(()) => { + has_error = true; + Type::unknown() + } } } else { self.infer_type_expression(expr) @@ -11500,10 +11605,23 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { has_error = true; } + if store_inferred_type_arguments { + self.store_expression_type( + slice_node, + Type::heterogeneous_tuple(db, inferred_type_arguments), + ); + } + if has_error { let unknowns = generic_context .variables(self.db()) - .map(|_| Some(Type::unknown())) + .map(|typevar| { + Some(if typevar.is_paramspec(db) { + Type::paramspec_value_callable(db, Parameters::unknown()) + } else { + Type::unknown() + }) + }) .collect::>(); return specialize(&unknowns); } @@ -12198,34 +12316,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum ParamSpecValuePosition { - Default, - ExplicitSpecialization, -} - -impl ParamSpecValuePosition { - fn diagnostic_code(self) -> &'static LintMetadata { - match self { - ParamSpecValuePosition::Default => &INVALID_PARAMSPEC, - ParamSpecValuePosition::ExplicitSpecialization => &INVALID_TYPE_ARGUMENTS, - } - } - - fn diagnostic_message(self) -> &'static str { - match self { - ParamSpecValuePosition::Default => { - "The default value to `ParamSpec` must be either \ - a list of types, `ParamSpec`, or `...`" - } - ParamSpecValuePosition::ExplicitSpecialization => { - "Type argument for `ParamSpec` must be either \ - a list of types, `ParamSpec`, `Concatenate`, or `...`" - } - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum GenericContextError { /// It's invalid to subscript `Generic` or `Protocol` with this type From b0511976d135dd1441211e55a2daf9d12b96efc4 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 28 Nov 2025 17:28:00 +0530 Subject: [PATCH 22/59] Avoid replacing paramspec variable itself for default type --- crates/ty_python_semantic/src/types.rs | 7 +++++- .../ty_python_semantic/src/types/generics.rs | 24 ++++++++----------- .../src/types/infer/builder.rs | 10 ++++---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e655d2221c657..effc706070988 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9449,7 +9449,12 @@ impl<'db> TypeVarInstance<'db> { }), Type::Dynamic(DynamicType::Todo(_)) => Parameters::todo(), Type::TypeVar(typevar) if typevar.is_paramspec(db) => { - Parameters::paramspec(db, typevar) + return ty; + } + Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) + if typevar.is_paramspec(db) => + { + return ty; } _ => Parameters::unknown(), }; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 246eaf5b33028..7efef0f1e6a7e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1666,20 +1666,16 @@ impl<'db> SpecializationBuilder<'db> { let ParametersKind::ParamSpec(typevar) = formal_parameters.kind() else { return Ok(()); }; - self.add_type_mapping( - typevar, - Type::Callable(CallableType::new( - self.db, - CallableSignature::from_overloads( - actual_callable.signatures(self.db).iter().map(|signature| { - Signature::new(signature.parameters().clone(), None) - }), - ), - CallableTypeKind::ParamSpecValue, - )), - polarity, - &mut f, - ); + let paramspec_value = Type::Callable(CallableType::new( + self.db, + CallableSignature::from_overloads( + actual_callable.signatures(self.db).iter().map(|signature| { + Signature::new(signature.parameters().clone(), None) + }), + ), + CallableTypeKind::ParamSpecValue, + )); + self.add_type_mapping(typevar, paramspec_value, polarity, &mut f); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 09cf140e1e45a..5acbef257fc70 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3472,11 +3472,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::Tuple(ast::ExprTuple { elts, .. }) | ast::Expr::List(ast::ExprList { elts, .. }) => { // This should be taken care by the caller. - debug_assert!( - expr.is_tuple_expr() && exactly_one_paramspec, - "Inferring ParamSpec value during explicit specialization for a \ + if expr.is_tuple_expr() { + debug_assert!( + exactly_one_paramspec, + "Inferring ParamSpec value during explicit specialization for a \ tuple expression should only happen when it contains exactly one ParamSpec" - ); + ); + } let mut parameter_types = Vec::with_capacity(elts.len()); From b11331ffee5b9adb9846c70a96bc32225a609dd5 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 28 Nov 2025 17:32:05 +0530 Subject: [PATCH 23/59] Run pre-commit --- crates/ty_python_semantic/src/types/infer/builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 5acbef257fc70..58fb0fd43dce2 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3421,7 +3421,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .iter() .map(|elt| self.infer_type_expression(elt)) .collect::>(); - // N.B. We cannot represent a hetrogeneous list of types in our type system, so we + // N.B. We cannot represent a heterogeneous list of types in our type system, so we // use a heterogeneous tuple type to represent the list of types instead. self.store_expression_type( default_expr, @@ -3496,7 +3496,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter_types.push(param_type); } - // N.B. We cannot represent a hetrogeneous list of types in our type system, so we + // N.B. We cannot represent a heterogeneous list of types in our type system, so we // use a heterogeneous tuple type to represent the list of types instead. self.store_expression_type( expr, From 6fd1c21e47de188ced58507c5a8554b97ee96ba0 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 28 Nov 2025 17:35:04 +0530 Subject: [PATCH 24/59] Fix after merging latest main --- crates/ty_python_semantic/src/types.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 57e5edd8dac5f..65199e8a76233 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9811,7 +9811,12 @@ impl<'db> BoundTypeVarInstance<'db> { self.typevar(db)._default(db), ); - Self::new(db, typevar, self.binding_context(db)) + Self::new( + db, + typevar, + self.binding_context(db), + self.paramspec_attr(db), + ) } pub(crate) fn variance_with_polarity( From c5c2e70d9f9c4c1ddf727e5b3ab1b7817c2f75ae Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sat, 29 Nov 2025 09:27:47 +0530 Subject: [PATCH 25/59] Remove implicit type alias TODO related to ParamSpec --- .../resources/mdtest/implicit_type_aliases.md | 10 +++---- .../src/types/infer/builder.rs | 29 ------------------- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index dbc5610e18991..7dd930f0da291 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -401,7 +401,7 @@ reveal_type(Pair) # revealed: reveal_type(Sum) # revealed: reveal_type(ListOrTuple) # revealed: types.UnionType reveal_type(ListOrTupleLegacy) # revealed: types.UnionType -reveal_type(MyCallable) # revealed: @Todo(Callable[..] specialized with ParamSpec) +reveal_type(MyCallable) # revealed: GenericAlias reveal_type(AnnotatedType) # revealed: reveal_type(TransparentAlias) # revealed: typing.TypeVar reveal_type(MyOptional) # revealed: types.UnionType @@ -429,7 +429,7 @@ def _( reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...] reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] # TODO: This should be `(str, bytes) -> int` - reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec) + reveal_type(my_callable) # revealed: (str, bytes, /) -> T@MyCallable reveal_type(annotated_int) # revealed: int reveal_type(transparent_alias) # revealed: int reveal_type(optional_int) # revealed: int | None @@ -466,7 +466,7 @@ reveal_type(ListOfPairs) # revealed: reveal_type(ListOrTupleOfInts) # revealed: types.UnionType reveal_type(AnnotatedInt) # revealed: reveal_type(SubclassOfInt) # revealed: GenericAlias -reveal_type(CallableIntToStr) # revealed: @Todo(Callable[..] specialized with ParamSpec) +reveal_type(CallableIntToStr) # revealed: GenericAlias def _( ints_or_none: IntsOrNone, @@ -484,7 +484,7 @@ def _( reveal_type(annotated_int) # revealed: int reveal_type(subclass_of_int) # revealed: type[int] # TODO: This should be `(int, /) -> str` - reveal_type(callable_int_to_str) # revealed: @Todo(Callable[..] specialized with ParamSpec) + reveal_type(callable_int_to_str) # revealed: (int, /) -> T@MyCallable ``` A generic implicit type alias can also be used in another generic implicit type alias: @@ -545,7 +545,7 @@ def _( reveal_type(list_or_tuple) # revealed: list[T@ListOrTuple] | tuple[T@ListOrTuple, ...] # TODO: Should be `list[Unknown] | tuple[Unknown, ...]` reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...] - reveal_type(my_callable) # revealed: (**P) -> T@MyCallable + reveal_type(my_callable) # revealed: (**P@MyCallable) -> T@MyCallable # TODO: Should be `Unknown` reveal_type(annotated_unknown) # revealed: T@AnnotatedType # TODO: Should be `Unknown | None` diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ef6c9307fd257..1c6423a0920ce 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11291,35 +11291,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); } Type::SpecialForm(SpecialFormType::Callable) => { - // TODO: Remove this once we support ParamSpec properly. This is necessary to avoid - // a lot of false positives downstream, because we can't represent the specialized - // `Callable[P, _]` type yet. - if let Some(first_arg) = subscript - .slice - .as_ref() - .as_tuple_expr() - .and_then(|args| args.elts.first()) - && first_arg.is_name_expr() - { - let first_arg_ty = self.infer_expression(first_arg, TypeContext::default()); - - if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = first_arg_ty - && typevar.kind(self.db()).is_paramspec() - { - return todo_type!("Callable[..] specialized with ParamSpec"); - } - - if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) { - builder.into_diagnostic(format_args!( - "The first argument to `Callable` must be either a list of types, \ - ParamSpec, Concatenate, or `...`", - )); - } - return Type::KnownInstance(KnownInstanceType::Callable( - CallableType::unknown(self.db()), - )); - } - let callable = self .infer_callable_type(subscript) .as_callable() From 12d2fa4102f065a9d24b9885bd35bf404c04cccf Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:03:29 +0530 Subject: [PATCH 26/59] Fix default specialization for paramspec --- crates/ty_python_semantic/src/types/generics.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index a0bd9b54d8a60..0654752268e32 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -565,7 +565,15 @@ impl<'db> GenericContext<'db> { // // If there is a mapping for `T`, we want to map `U` to that type, not to `T`. To handle // this, we repeatedly apply the specialization to itself, until we reach a fixed point. - let mut expanded = vec![Type::unknown(); types.len()]; + let mut expanded = Vec::with_capacity(types.len()); + for typevar in variables.clone() { + if typevar.is_paramspec(db) { + expanded.push(Type::paramspec_value_callable(db, Parameters::unknown())); + } else { + expanded.push(Type::unknown()); + } + } + for (idx, (ty, typevar)) in types.zip(variables).enumerate() { if let Some(ty) = ty { expanded[idx] = ty; From 3056776f4d827bb917f94fd921cdffde4065948d Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:03:40 +0530 Subject: [PATCH 27/59] Apply type mapping for return type while specializing paramspec --- crates/ty_python_semantic/src/types/signatures.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index a7aa10e490050..f06d8b1e86194 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -208,7 +208,12 @@ impl<'db> CallableSignature<'db> { && matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) { return Self::from_overloads(callable.signatures(db).iter().map(|signature| { - Signature::new(signature.parameters.clone(), self_signature.return_ty) + Signature::new( + signature.parameters.clone(), + self_signature + .return_ty + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + ) })); } From f1f79bad5be18d91f7921c71b8c0371616155a82 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:04:07 +0530 Subject: [PATCH 28/59] Avoid creating union when `P` is mapped multiple times --- crates/ty_python_semantic/src/types/generics.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 0654752268e32..e05dc2653bbc0 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1404,6 +1404,10 @@ impl<'db> SpecializationBuilder<'db> { match self.types.entry(identity) { Entry::Occupied(mut entry) => { + // TODO: mypy and Pyright does this, should we keep doing it? + if bound_typevar.is_paramspec(self.db) { + return; + } *entry.get_mut() = UnionType::from_elements(self.db, [*entry.get(), ty]); } Entry::Vacant(entry) => { From bf9083335e63ab1605ec253547d45eced15c3707 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:04:36 +0530 Subject: [PATCH 29/59] Remove paramspec special casing --- crates/ty_python_semantic/src/types/infer/builder.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index f5d3beb21a893..76e6423fc9502 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -12074,10 +12074,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { *typevar, &|ty| match ty { Type::Dynamic(DynamicType::TodoUnpack) => true, - Type::NominalInstance(nominal) => matches!( - nominal.known_class(self.db()), - Some(KnownClass::TypeVarTuple | KnownClass::ParamSpec) - ), + Type::NominalInstance(nominal) => { + nominal.has_known_class(self.db(), KnownClass::TypeVarTuple) + } _ => false, }, true, From 427966a7e2fafdcbe46580bded8b83534936ceb1 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:05:14 +0530 Subject: [PATCH 30/59] Update existing mdtest --- .../ty_python_semantic/resources/mdtest/annotations/self.md | 2 +- .../resources/mdtest/implicit_type_aliases.md | 6 ++---- crates/ty_python_semantic/resources/mdtest/with/async.md | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/self.md b/crates/ty_python_semantic/resources/mdtest/annotations/self.md index 9329473a83936..4be59a5a63193 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/self.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/self.md @@ -189,7 +189,7 @@ reveal_type(B().name_does_not_matter()) # revealed: B reveal_type(B().positional_only(1)) # revealed: B reveal_type(B().keyword_only(x=1)) # revealed: B # TODO: This should deally be `B` -reveal_type(B().decorated_method()) # revealed: R@some_decorator +reveal_type(B().decorated_method()) # revealed: Unknown reveal_type(B().a_property) # revealed: B diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 7dd930f0da291..c44d84eb50367 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -428,8 +428,7 @@ def _( reveal_type(int_and_bytes) # revealed: tuple[int, bytes] reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...] reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...] - # TODO: This should be `(str, bytes) -> int` - reveal_type(my_callable) # revealed: (str, bytes, /) -> T@MyCallable + reveal_type(my_callable) # revealed: (str, bytes, /) -> int reveal_type(annotated_int) # revealed: int reveal_type(transparent_alias) # revealed: int reveal_type(optional_int) # revealed: int | None @@ -483,8 +482,7 @@ def _( reveal_type(list_or_tuple_of_ints) # revealed: list[int] | tuple[int, ...] reveal_type(annotated_int) # revealed: int reveal_type(subclass_of_int) # revealed: type[int] - # TODO: This should be `(int, /) -> str` - reveal_type(callable_int_to_str) # revealed: (int, /) -> T@MyCallable + reveal_type(callable_int_to_str) # revealed: (int, /) -> str ``` A generic implicit type alias can also be used in another generic implicit type alias: diff --git a/crates/ty_python_semantic/resources/mdtest/with/async.md b/crates/ty_python_semantic/resources/mdtest/with/async.md index 6b3dd009258bd..2a0d7165de71b 100644 --- a/crates/ty_python_semantic/resources/mdtest/with/async.md +++ b/crates/ty_python_semantic/resources/mdtest/with/async.md @@ -213,12 +213,12 @@ async def connect() -> AsyncGenerator[Session]: yield Session() # TODO: this should be `() -> _AsyncGeneratorContextManager[Session, None]` -reveal_type(connect) # revealed: () -> _AsyncGeneratorContextManager[_T_co@asynccontextmanager, None] +reveal_type(connect) # revealed: () -> _AsyncGeneratorContextManager[Unknown, None] async def main(): async with connect() as session: # TODO: should be `Session` - reveal_type(session) # revealed: _T_co@asynccontextmanager + reveal_type(session) # revealed: Unknown ``` ## `asyncio.timeout` From b5efe3c633ae100769aab9d1d5cfb03e186e236b Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:05:22 +0530 Subject: [PATCH 31/59] Add paramspec test cases --- .../mdtest/generics/legacy/paramspec.md | 230 +++++++++++ .../mdtest/generics/pep695/paramspec.md | 391 ++++++++++++++++++ 2 files changed, 621 insertions(+) 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 2ce9f148524f7..2af98e98cf280 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -115,3 +115,233 @@ P = ParamSpec("P", default=[A, B]) class A: ... class B: ... ``` + +## Validating `ParamSpec` usage + +`ParamSpec` is only valid as the first element to `Callable` or the final element to `Concatenate`. + +```py +from typing import ParamSpec, Callable, Concatenate + +P = ParamSpec("P") + +def valid( + a1: Callable[P, int], + a2: Callable[Concatenate[int, P], int], +) -> None: ... + +def invalid( + # TODO: error + a1: P, + # TODO: error + a2: list[P], + # TODO: error + a3: Callable[[P], int], + # TODO: error + a4: Callable[..., P], + # TODO: error + a5: Callable[Concatenate[P, ...], int], +) -> None: ... +``` + +## Validating `P.args` and `P.kwargs` usage + +The components of `ParamSpec` i.e., `P.args` and `P.kwargs` are only valid when used as the +annotated types of `*args` and `**kwargs` respectively. + +```py +from typing import Callable, ParamSpec + +P = ParamSpec("P") + +def foo(c: Callable[P, int]) -> None: + def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ... + + # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" + # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" + def nested2(*args: P.kwargs, **kwargs: P.args) -> None: ... + + # TODO: error + def nested3(*args: P.args) -> None: ... + + # TODO: error + def nested4(**kwargs: P.kwargs) -> None: ... + +# TODO: error +def bar(*args: P.args, **kwargs: P.kwargs) -> None: + pass + +class Foo: + # TODO: error + def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... +``` + +And, they need to be used together. + +```py +def foo(c: Callable[P, int]) -> None: + # TODO: error + def nested1(*args: P.args) -> None: ... + + # TODO: error + def nested2(**kwargs: P.kwargs) -> None: ... + + +class Foo: + # TODO: error + args: P.args + + # TODO: error + kwargs: P.kwargs +``` + +## Specializing generic classes explicitly + +```py +from typing import Any, Generic, ParamSpec, Callable, TypeVar + +P1 = ParamSpec("P1") +P2 = ParamSpec("P2") +T1 = TypeVar("T1") + +class OnlyParamSpec(Generic[P1]): + attr: Callable[P1, None] + +class TwoParamSpec(Generic[P1, P2]): + attr1: Callable[P1, None] + attr2: Callable[P2, None] + +class TypeVarAndParamSpec(Generic[T1, P1]): + attr: Callable[P1, T1] +``` + +Explicit specialization of a generic class involving `ParamSpec` is done by providing either a list +of types, `...`, or another in-scope `ParamSpec`. + +```py +reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None +reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None + +def func(c: Callable[P2, None]): + reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None + +# TODO: error: paramspec is unbound +reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None +``` + +The square brackets can be omitted when `ParamSpec` is the only type variable + +```py +reveal_type(OnlyParamSpec[int, str]().attr) # revealed: (int, str, /) -> None +reveal_type(OnlyParamSpec[int,]().attr) # revealed: (int, /) -> None + +# Even when there is only one element +reveal_type(OnlyParamSpec[Any]().attr) # revealed: (Any, /) -> None +reveal_type(OnlyParamSpec[object]().attr) # revealed: (object, /) -> None +reveal_type(OnlyParamSpec[int]().attr) # revealed: (int, /) -> None +``` + +But, they cannot be omitted when there are multiple type variables. + +```py +reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int +reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int +reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int + +# TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int` +# TODO: error: paramspec is unbound +reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown +# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown +``` + +Nor can they be omitted when there are more than one `ParamSpec`. + +```py +p = TwoParamSpec[[int, str], [int]]() +reveal_type(p.attr1) # revealed: (int, str, /) -> None +reveal_type(p.attr2) # revealed: (int, /) -> None + +# error: [invalid-type-arguments] +# error: [invalid-type-arguments] +TwoParamSpec[int, str] +``` + +## Specialization when defaults are involved + +```toml +[environment] +python-version = "3.13" +``` + +```py +from typing import Any, Generic, ParamSpec, Callable, TypeVar + +P = ParamSpec("P") +PList = ParamSpec("PList", default=[int, str]) +PEllipsis = ParamSpec("PEllipsis", default=...) +PAnother = ParamSpec("PAnother", default=P) +PAnotherWithDefault = ParamSpec("PAnotherWithDefault", default=PList) +``` + +```py +class ParamSpecWithDefault1(Generic[PList]): + attr: Callable[PList, None] + +reveal_type(ParamSpecWithDefault1().attr) # revealed: (int, str, /) -> None +reveal_type(ParamSpecWithDefault1[[int]]().attr) # revealed: (int, /) -> None +``` + +```py +class ParamSpecWithDefault2(Generic[PEllipsis]): + attr: Callable[PEllipsis, None] + +reveal_type(ParamSpecWithDefault2().attr) # revealed: (...) -> None +reveal_type(ParamSpecWithDefault2[[int, str]]().attr) # revealed: (int, str, /) -> None +``` + +```py +class ParamSpecWithDefault3(Generic[P, PAnother]): + attr1: Callable[P, None] + attr2: Callable[PAnother, None] + +# `P` hasn't been specialized, so it defaults to `Unknown` gradual form +p1 = ParamSpecWithDefault3() +reveal_type(p1.attr1) # revealed: (...) -> None +reveal_type(p1.attr2) # revealed: (...) -> None + +p2 = ParamSpecWithDefault3[[int, str]]() +reveal_type(p2.attr1) # revealed: (int, str, /) -> None +reveal_type(p2.attr2) # revealed: (int, str, /) -> None + +p3 = ParamSpecWithDefault3[[int], [str]]() +reveal_type(p3.attr1) # revealed: (int, /) -> None +reveal_type(p3.attr2) # revealed: (str, /) -> None + +class ParamSpecWithDefault4(Generic[PList, PAnotherWithDefault]): + attr1: Callable[PList, None] + attr2: Callable[PAnotherWithDefault, None] + +p1 = ParamSpecWithDefault4() +reveal_type(p1.attr1) # revealed: (int, str, /) -> None +reveal_type(p1.attr2) # revealed: (int, str, /) -> None + +p2 = ParamSpecWithDefault4[[int]]() +reveal_type(p2.attr1) # revealed: (int, /) -> None +reveal_type(p2.attr2) # revealed: (int, /) -> None + +p3 = ParamSpecWithDefault4[[int], [str]]() +reveal_type(p3.attr1) # revealed: (int, /) -> None +reveal_type(p3.attr2) # revealed: (str, /) -> None + +# TODO: error +# Un-ordered type variables as the default of `PAnother` is `P` +class ParamSpecWithDefault5(Generic[PAnother, P]): + attr: Callable[PAnother, None] +``` + +## Semantics + +The semantics of `ParamSpec` are described in [the PEP 695 `ParamSpec` +document](./../pep695/paramspec.md) to avoid duplication unless there are any behavior specific to +the legacy `ParamSpec` implementation. 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 62b50b05efbd9..f26d54bf327d4 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -62,3 +62,394 @@ Other values are invalid. def foo[**P = int]() -> None: pass ``` + +## Validating `ParamSpec` usage + +`ParamSpec` is only valid as the first element to `Callable` or the final element to `Concatenate`. + +```py +from typing import ParamSpec, Callable, Concatenate + +def valid[**P]( + a1: Callable[P, int], + a2: Callable[Concatenate[int, P], int], +) -> None: ... + +def invalid[**P]( + # TODO: error + a1: P, + # TODO: error + a2: list[P], + # TODO: error + a3: Callable[[P], int], + # TODO: error + a4: Callable[..., P], + # TODO: error + a5: Callable[Concatenate[P, ...], int], +) -> None: ... +``` + +## Validating `P.args` and `P.kwargs` usage + +The components of `ParamSpec` i.e., `P.args` and `P.kwargs` are only valid when used as the +annotated types of `*args` and `**kwargs` respectively. + +```py +from typing import Callable + +def foo[**P](c: Callable[P, int]) -> None: + def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ... + + # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" + # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" + def nested2(*args: P.kwargs, **kwargs: P.args) -> None: ... + + # TODO: error + def nested3(*args: P.args) -> None: ... + + # TODO: error + def nested4(**kwargs: P.kwargs) -> None: ... + + # TODO: error + def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ... +``` + +And, they need to be used together. + +```py +def foo[**P](c: Callable[P, int]) -> None: + # TODO: error + def nested1(*args: P.args) -> None: ... + + # TODO: error + def nested2(**kwargs: P.kwargs) -> None: ... + + +class Foo[**P]: + # TODO: error + args: P.args + + # TODO: error + kwargs: P.kwargs +``` + +## Semantics of `P.args` and `P.kwargs` + +The type of `args` and `kwargs` inside the function is `P.args` and `P.kwargs` respectively instead +of `tuple[P.args, ...]` and `dict[str, P.kwargs]`. + +```py +from typing import Callable + +def f[**P](func: Callable[P, int]) -> Callable[P, None]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: + reveal_type(args) # revealed: P@f.args + reveal_type(kwargs) # revealed: P@f.kwargs + reveal_type(func(*args, **kwargs)) # revealed: int + + # error: [invalid-argument-type] "Argument is incorrect: Expected `P@f.args`, found `P@f.kwargs`" + # error: [invalid-argument-type] "Argument is incorrect: Expected `P@f.kwargs`, found `P@f.args`" + reveal_type(func(*kwargs, **args)) # revealed: int + + # error: [invalid-argument-type] "Argument is incorrect: Expected `P@f.args`, found `P@f.kwargs`" + reveal_type(func(args, kwargs)) # revealed: int + + # Both parameters are required + # TODO: error and reveal `Unknown` + reveal_type(func()) # revealed: int + reveal_type(func(*args)) # revealed: int + reveal_type(func(**kwargs)) # revealed: int + + return wrapper +``` + +## Specializing generic classes explicitly + +```py +from typing import Any, Callable, ParamSpec + +class OnlyParamSpec[**P1]: + attr: Callable[P1, None] + +class TwoParamSpec[**P1, **P2]: + attr1: Callable[P1, None] + attr2: Callable[P2, None] + +class TypeVarAndParamSpec[T1, **P1]: + attr: Callable[P1, T1] +``` + +Explicit specialization of a generic class involving `ParamSpec` is done by providing either a list +of types, `...`, or another in-scope `ParamSpec`. + +```py +reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None +reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None + +def func[**P2](c: Callable[P2, None]): + reveal_type(OnlyParamSpec[P2]().attr) # revealed: (**P2@func) -> None + +P2 = ParamSpec("P2") + +# TODO: error: paramspec is unbound +reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None +``` + +The square brackets can be omitted when `ParamSpec` is the only type variable + +```py +reveal_type(OnlyParamSpec[int, str]().attr) # revealed: (int, str, /) -> None +reveal_type(OnlyParamSpec[int,]().attr) # revealed: (int, /) -> None + +# Even when there is only one element +reveal_type(OnlyParamSpec[Any]().attr) # revealed: (Any, /) -> None +reveal_type(OnlyParamSpec[object]().attr) # revealed: (object, /) -> None +reveal_type(OnlyParamSpec[int]().attr) # revealed: (int, /) -> None +``` + +But, they cannot be omitted when there are multiple type variables. + +```py +reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int +reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int +reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int + +# TODO: error: paramspec is unbound +reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown +# error: [invalid-type-arguments] +reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown +``` + +Nor can they be omitted when there are more than one `ParamSpec`. + +```py +p = TwoParamSpec[[int, str], [int]]() +reveal_type(p.attr1) # revealed: (int, str, /) -> None +reveal_type(p.attr2) # revealed: (int, /) -> None + +# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`" +TwoParamSpec[int, str] +``` + +## Specialization when defaults are involved + +```py +from typing import Callable, ParamSpec + +class ParamSpecWithDefault1[**P1 = [int, str]]: + attr: Callable[P1, None] + +reveal_type(ParamSpecWithDefault1().attr) # revealed: (int, str, /) -> None +reveal_type(ParamSpecWithDefault1[int]().attr) # revealed: (int, /) -> None +``` + +```py +class ParamSpecWithDefault2[**P1 = ...]: + attr: Callable[P1, None] + +reveal_type(ParamSpecWithDefault2().attr) # revealed: (...) -> None +reveal_type(ParamSpecWithDefault2[int, str]().attr) # revealed: (int, str, /) -> None +``` + +```py +class ParamSpecWithDefault3[**P1, **P2 = P1]: + attr1: Callable[P1, None] + attr2: Callable[P2, None] + +# `P1` hasn't been specialized, so it defaults to `Unknown` gradual form +p1 = ParamSpecWithDefault3() +reveal_type(p1.attr1) # revealed: (...) -> None +reveal_type(p1.attr2) # revealed: (...) -> None + +p2 = ParamSpecWithDefault3[[int, str]]() +reveal_type(p2.attr1) # revealed: (int, str, /) -> None +reveal_type(p2.attr2) # revealed: (int, str, /) -> None + +p3 = ParamSpecWithDefault3[[int], [str]]() +reveal_type(p3.attr1) # revealed: (int, /) -> None +reveal_type(p3.attr2) # revealed: (str, /) -> None + +class ParamSpecWithDefault4[**P1 = [int, str], **P2 = P1]: + attr1: Callable[P1, None] + attr2: Callable[P2, None] + +p1 = ParamSpecWithDefault4() +reveal_type(p1.attr1) # revealed: (int, str, /) -> None +reveal_type(p1.attr2) # revealed: (int, str, /) -> None + +p2 = ParamSpecWithDefault4[[int]]() +reveal_type(p2.attr1) # revealed: (int, /) -> None +reveal_type(p2.attr2) # revealed: (int, /) -> None + +p3 = ParamSpecWithDefault4[[int], [str]]() +reveal_type(p3.attr1) # revealed: (int, /) -> None +reveal_type(p3.attr2) # revealed: (str, /) -> None + +P2 = ParamSpec("P2") + +# TODO: error: paramspec is out of scope +class ParamSpecWithDefault5[**P1 = P2]: + attr: Callable[P1, None] +``` + +## Semantics + +Most of these test cases are adopted from the [typing documentation on `ParamSpec` +semantics](https://typing.python.org/en/latest/spec/generics.html#semantics). + +### Return type change using `ParamSpec` once + +```py +from typing import Callable + +def converter[**P](func: Callable[P, int]) -> Callable[P, bool]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool: + func(*args, **kwargs) + return True + return wrapper + +def f1(x: int, y: str) -> int: + return 1 + +# This should preserve all the information about the parameters of `f1` +f2 = converter(f1) + +reveal_type(f2) # revealed: (x: int, y: str) -> bool + +reveal_type(f1(1, "a")) # revealed: int +reveal_type(f2(1, "a")) # revealed: bool + +# As it preserves the parameter kinds, the following should work as well +reveal_type(f2(1, y="a")) # revealed: bool +reveal_type(f2(x=1, y="a")) # revealed: bool +reveal_type(f2(y="a", x=1)) # revealed: bool + +# error: [missing-argument] "No argument provided for required parameter `y`" +f2(1) +# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `Literal["a"]`" +f2("a", "b") +``` + +### Return type change using the same `ParamSpec` multiple times + +```py +from typing import Callable + +def multiple[**P](func1: Callable[P, int], func2: Callable[P, int]) -> Callable[P, bool]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool: + func1(*args, **kwargs) + func2(*args, **kwargs) + return True + return wrapper +``` + +As per the spec, + +> A user may include the same `ParamSpec` multiple times in the arguments of the same function, to +> indicate a dependency between multiple arguments. In these cases a type checker may choose to +> solve to a common behavioral supertype (i.e. a set of parameters for which all of the valid calls +> are valid in both of the subtypes), but is not obligated to do so. + +TODO: Currently, we don't do this + +```py +def xy(x: int, y: str) -> int: + return 1 + +def yx(y: int, x: str) -> int: + return 2 + +reveal_type(multiple(xy, xy)) # revealed: (x: int, y: str) -> bool + +# The common supertype is `(int, str, /)` which is converting the positional-or-keyword parameters +# into positional-only parameters because the position of the types are the same. +# TODO: This shouldn't error +# error: [invalid-argument-type] +reveal_type(multiple(xy, yx)) # revealed: (x: int, y: str) -> bool + +def keyword_only1(*, x: int) -> int: + return 1 + +def keyword_only2(*, y: int) -> int: + return 2 + +# On the other hand, combining two functions with only keyword-only parameters does not have a +# common supertype, so it should result in an error. +# error: [invalid-argument-type] "Argument to function `multiple` is incorrect: Expected `(*, x: int) -> int`, found `def keyword_only2(*, y: int) -> int`" +reveal_type(multiple(keyword_only1, keyword_only2)) # revealed: (*, x: int) -> bool +``` + +### Constructors of user-defined generic class on `ParamSpec` + +```py +from typing import Callable + +class C[**P]: + f: Callable[P, int] + + def __init__(self, f: Callable[P, int]) -> None: + self.f = f + +def f(x: int, y: str) -> bool: + return True + +c = C(f) +reveal_type(c.f) # revealed: (x: int, y: str) -> int +``` + +### `ParamSpec` in prepended positional parameters + +> If one of these prepended positional parameters contains a free `ParamSpec`, we consider that +> variable in scope for the purposes of extracting the components of that `ParamSpec`. + +```py +from typing import Callable + +def foo1[**P1](func: Callable[P1, int], *args: P1.args, **kwargs: P1.kwargs) -> int: + return func(*args, **kwargs) + +def foo1_with_extra_arg[**P1]( + func: Callable[P1, int], extra: str, *args: P1.args, **kwargs: P1.kwargs +) -> int: + return func(*args, **kwargs) + +def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> None: + foo1(func, *args, **kwargs) + + # error: [invalid-argument-type] "Argument to function `foo1` is incorrect: Expected `P2@foo2.args`, found `Literal[1]`" + foo1(func, 1, *args, **kwargs) + + # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `P1@foo1_with_extra_arg.args`, found `P2@foo2.args`" + # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `str`, found `P2@foo2.args`" + foo1_with_extra_arg(func, *args, **kwargs) + +``` + +Here, the first argument to `f` can specialize `P` to the parameters of the callable passed to it +which is then used to type the `ParamSpec` components used in `*args` and `**kwargs`. + +```py +def f1(x: int, y: str) -> int: + return 1 + +foo1(f1, 1, "a") +foo1(f1, x=1, y="a") +foo1(f1, 1, y="a") + +# error: [missing-argument] "No arguments provided for required parameters `x`, `y` of function `foo1`" +foo1(f1) + +# error: [missing-argument] "No argument provided for required parameter `y` of function `foo1`" +foo1(f1, 1) + +# error: [invalid-argument-type] "Argument to function `foo1` is incorrect: Expected `str`, found `Literal[2]`" +foo1(f1, 1, 2) + +# error: [too-many-positional-arguments] "Too many positional arguments to function `foo1`: expected 2, got 3" +foo1(f1, 1, "a", "b") + +# error: [missing-argument] "No argument provided for required parameter `y` of function `foo1`" +# error: [unknown-argument] "Argument `z` does not match any known parameter of function `foo1`" +foo1(f1, x=1, z="a") +``` From e11d8d7250d278d0d36730c391c9798f18b1b248 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 21:28:08 +0530 Subject: [PATCH 32/59] Relation checks for `P.args`/`P.kwargs` --- .../type_properties/is_assignable_to.md | 29 +++++++++++++++++++ crates/ty_python_semantic/src/types.rs | 27 +++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 885d3c2e4f194..8e99687df6714 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1344,6 +1344,35 @@ static_assert(not is_assignable_to(TypeGuard[Unknown], str)) # error: [static-a static_assert(not is_assignable_to(TypeIs[Any], str)) ``` +## `ParamSpec` + +```py +from ty_extensions import TypeOf, static_assert, is_assignable_to, Unknown +from typing import ParamSpec, Mapping, Callable, Any + +P = ParamSpec("P") + +def f(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: + static_assert(is_assignable_to(TypeOf[args], tuple[Any, ...])) + static_assert(is_assignable_to(TypeOf[args], tuple[object, ...])) + static_assert(is_assignable_to(TypeOf[args], tuple[Unknown, ...])) + + static_assert(not is_assignable_to(tuple[Any, ...], TypeOf[args])) + static_assert(not is_assignable_to(tuple[object, ...], TypeOf[args])) + static_assert(not is_assignable_to(tuple[Unknown, ...], TypeOf[args])) + + static_assert(is_assignable_to(TypeOf[kwargs], dict[str, Any])) + static_assert(is_assignable_to(TypeOf[kwargs], dict[str, object])) + static_assert(is_assignable_to(TypeOf[kwargs], dict[str, Unknown])) + static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, Any])) + static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, object])) + static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, Unknown])) + + static_assert(not is_assignable_to(dict[str, Any], TypeOf[kwargs])) + static_assert(not is_assignable_to(dict[str, object], TypeOf[kwargs])) + static_assert(not is_assignable_to(dict[str, Unknown], TypeOf[kwargs])) +``` + [gradual form]: https://typing.python.org/en/latest/spec/glossary.html#term-gradual-form [gradual tuple]: https://typing.python.org/en/latest/spec/tuples.html#tuple-type-form [typing documentation]: https://typing.python.org/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 8940901b14cf2..392a09729babd 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2339,6 +2339,33 @@ impl<'db> Type<'db> { (_, Type::TypeVar(bound_typevar)) if bound_typevar.is_inferable(db, inferable) => { ConstraintSet::from(false) } + (Type::TypeVar(bound_typevar), _) + if bound_typevar.paramspec_attr(db) == Some(ParamSpecAttrKind::Args) => + { + Type::homogeneous_tuple(db, Type::object()).has_relation_to_impl( + db, + target, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + } + (Type::TypeVar(bound_typevar), _) + if bound_typevar.paramspec_attr(db) == Some(ParamSpecAttrKind::Kwargs) => + { + KnownClass::Dict + .to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]) + .top_materialization(db) + .has_relation_to_impl( + db, + target, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + } (Type::TypeVar(bound_typevar), _) => { // All inferable cases should have been handled above assert!(!bound_typevar.is_inferable(db, inferable)); From dfeec73c9f6c0df91fdb9f63479ff10fa1728862 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 22:48:23 +0530 Subject: [PATCH 33/59] Add more assignability check --- .../resources/mdtest/type_properties/is_assignable_to.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index 8e99687df6714..b572d3937b2cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1356,6 +1356,8 @@ def f(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: static_assert(is_assignable_to(TypeOf[args], tuple[Any, ...])) static_assert(is_assignable_to(TypeOf[args], tuple[object, ...])) static_assert(is_assignable_to(TypeOf[args], tuple[Unknown, ...])) + static_assert(not is_assignable_to(TypeOf[args], tuple[int, ...])) + static_assert(not is_assignable_to(TypeOf[args], tuple[int, str])) static_assert(not is_assignable_to(tuple[Any, ...], TypeOf[args])) static_assert(not is_assignable_to(tuple[object, ...], TypeOf[args])) @@ -1364,6 +1366,7 @@ def f(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: static_assert(is_assignable_to(TypeOf[kwargs], dict[str, Any])) static_assert(is_assignable_to(TypeOf[kwargs], dict[str, object])) static_assert(is_assignable_to(TypeOf[kwargs], dict[str, Unknown])) + static_assert(not is_assignable_to(TypeOf[kwargs], dict[str, int])) static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, Any])) static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, object])) static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, Unknown])) From 6c5702801b63a8d5e7d85688f32d676768c3ee9e Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 23:24:07 +0530 Subject: [PATCH 34/59] Avoid bound typevar for an unbounded paramspec --- .../types/infer/builder/type_expression.rs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 21246f6a4cf2c..42436d17c9775 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -14,10 +14,10 @@ use crate::types::signatures::Signature; use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::{ - BindingContext, BoundTypeVarInstance, CallableType, DynamicType, GenericContext, - IntersectionBuilder, KnownClass, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, - SpecialFormType, SubclassOfType, Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, - UnionBuilder, UnionType, any_over_type, todo_type, + BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass, + KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, + Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, + any_over_type, todo_type, }; /// Type expressions @@ -1742,21 +1742,16 @@ impl<'db> TypeInferenceBuilder<'db, '_> { && typevar.is_paramspec(self.db()) { let index = semantic_index(self.db(), self.scope().file(self.db())); - let bound_typevar = bind_typevar( + let Some(bound_typevar) = bind_typevar( self.db(), index, self.scope().file_scope_id(self.db()), self.typevar_binding_context, typevar, - ) - .unwrap_or_else(|| { - BoundTypeVarInstance::new( - self.db(), - typevar, - BindingContext::Synthetic, - None, - ) - }); + ) else { + // TODO: What to do here? + return None; + }; return Some(Parameters::paramspec(self.db(), bound_typevar)); } } From ed9b4c2d4443eed13dea73bcea6a496871e80cfe Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Mon, 1 Dec 2025 23:24:23 +0530 Subject: [PATCH 35/59] Update mdtest --- .../resources/mdtest/decorators.md | 2 ++ .../resources/mdtest/generics/legacy/paramspec.md | 8 +++----- .../resources/mdtest/generics/pep695/paramspec.md | 12 +++--------- .../mdtest/type_properties/is_assignable_to.md | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index f92eca800360e..4fd24a07be5f4 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -126,6 +126,8 @@ def custom_decorator(f) -> Callable[[int], str]: def wrapper(*args, **kwargs): print("Calling decorated function") return f(*args, **kwargs) + # TODO: This shouldn't be an error + # error: [invalid-return-type] return wrapper @custom_decorator 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 2af98e98cf280..fecd95967ec19 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -129,7 +129,6 @@ def valid( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], ) -> None: ... - def invalid( # TODO: error a1: P, @@ -186,7 +185,6 @@ def foo(c: Callable[P, int]) -> None: # TODO: error def nested2(**kwargs: P.kwargs) -> None: ... - class Foo: # TODO: error args: P.args @@ -342,6 +340,6 @@ class ParamSpecWithDefault5(Generic[PAnother, P]): ## Semantics -The semantics of `ParamSpec` are described in [the PEP 695 `ParamSpec` -document](./../pep695/paramspec.md) to avoid duplication unless there are any behavior specific to -the legacy `ParamSpec` implementation. +The semantics of `ParamSpec` are described in +[the PEP 695 `ParamSpec` document](./../pep695/paramspec.md) to avoid duplication unless there are +any behavior specific to the legacy `ParamSpec` implementation. 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 f26d54bf327d4..7c76b6bb91275 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -74,7 +74,6 @@ def valid[**P]( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], ) -> None: ... - def invalid[**P]( # TODO: error a1: P, @@ -124,7 +123,6 @@ def foo[**P](c: Callable[P, int]) -> None: # TODO: error def nested2(**kwargs: P.kwargs) -> None: ... - class Foo[**P]: # TODO: error args: P.args @@ -159,7 +157,6 @@ def f[**P](func: Callable[P, int]) -> Callable[P, None]: reveal_type(func()) # revealed: int reveal_type(func(*args)) # revealed: int reveal_type(func(**kwargs)) # revealed: int - return wrapper ``` @@ -295,8 +292,8 @@ class ParamSpecWithDefault5[**P1 = P2]: ## Semantics -Most of these test cases are adopted from the [typing documentation on `ParamSpec` -semantics](https://typing.python.org/en/latest/spec/generics.html#semantics). +Most of these test cases are adopted from the +[typing documentation on `ParamSpec` semantics](https://typing.python.org/en/latest/spec/generics.html#semantics). ### Return type change using `ParamSpec` once @@ -409,9 +406,7 @@ from typing import Callable def foo1[**P1](func: Callable[P1, int], *args: P1.args, **kwargs: P1.kwargs) -> int: return func(*args, **kwargs) -def foo1_with_extra_arg[**P1]( - func: Callable[P1, int], extra: str, *args: P1.args, **kwargs: P1.kwargs -) -> int: +def foo1_with_extra_arg[**P1](func: Callable[P1, int], extra: str, *args: P1.args, **kwargs: P1.kwargs) -> int: return func(*args, **kwargs) def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> None: @@ -423,7 +418,6 @@ def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `P1@foo1_with_extra_arg.args`, found `P2@foo2.args`" # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `str`, found `P2@foo2.args`" foo1_with_extra_arg(func, *args, **kwargs) - ``` Here, the first argument to `f` can specialize `P` to the parameters of the callable passed to it diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index b572d3937b2cd..3bd3cd1f097d6 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1364,8 +1364,8 @@ def f(func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: static_assert(not is_assignable_to(tuple[Unknown, ...], TypeOf[args])) static_assert(is_assignable_to(TypeOf[kwargs], dict[str, Any])) - static_assert(is_assignable_to(TypeOf[kwargs], dict[str, object])) static_assert(is_assignable_to(TypeOf[kwargs], dict[str, Unknown])) + static_assert(not is_assignable_to(TypeOf[kwargs], dict[str, object])) static_assert(not is_assignable_to(TypeOf[kwargs], dict[str, int])) static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, Any])) static_assert(is_assignable_to(TypeOf[kwargs], Mapping[str, object])) From 75e18a48c4c9ad28d50194708a15a8bfd7318be4 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 2 Dec 2025 10:46:59 +0530 Subject: [PATCH 36/59] Revert method rename --- crates/ty_python_semantic/src/types.rs | 2 +- crates/ty_python_semantic/src/types/generics.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 164a95ad9f0b8..fd16db45788ff 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9398,7 +9398,7 @@ fn walk_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( #[salsa::tracked] impl<'db> TypeVarInstance<'db> { - pub(crate) fn as_bound_type_var_instance( + pub(crate) fn with_binding_context( self, db: &'db dyn Db, binding_context: Definition<'db>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index e05dc2653bbc0..4cd64808e09ca 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -65,7 +65,7 @@ pub(crate) fn bind_typevar<'db>( if outer.kind().is_class() { if let NodeWithScopeKind::Function(function) = inner.node() { let definition = index.expect_single_definition(function); - return Some(typevar.as_bound_type_var_instance(db, definition)); + return Some(typevar.with_binding_context(db, definition)); } } } @@ -74,7 +74,7 @@ pub(crate) fn bind_typevar<'db>( .find_map(|enclosing_context| enclosing_context.binds_typevar(db, typevar)) .or_else(|| { typevar_binding_context.map(|typevar_binding_context| { - typevar.as_bound_type_var_instance(db, typevar_binding_context) + typevar.with_binding_context(db, typevar_binding_context) }) }) } @@ -340,7 +340,7 @@ impl<'db> GenericContext<'db> { else { return None; }; - Some(typevar.as_bound_type_var_instance(db, binding_context)) + Some(typevar.with_binding_context(db, binding_context)) } ast::TypeParam::ParamSpec(node) => { let definition = index.expect_single_definition(node); @@ -349,7 +349,7 @@ impl<'db> GenericContext<'db> { else { return None; }; - Some(typevar.as_bound_type_var_instance(db, binding_context)) + Some(typevar.with_binding_context(db, binding_context)) } // TODO: Support this! ast::TypeParam::TypeVarTuple(_) => None, From 198099a1f9bf49db66ee7934ca1237e72418bcdb Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 2 Dec 2025 10:47:11 +0530 Subject: [PATCH 37/59] Fix fuzzer panics --- .../src/types/infer/builder.rs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 3f4fd2f19fe49..eff44c9d6063f 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3470,9 +3470,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let db = self.db(); match expr { - ast::Expr::EllipsisLiteral(ellipsis) => { - let ty = self.infer_ellipsis_literal_expression(ellipsis); - self.store_expression_type(expr, ty); + ast::Expr::EllipsisLiteral(_) => { return Ok(Type::paramspec_value_callable( db, Parameters::gradual_form(), @@ -3506,13 +3504,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { parameter_types.push(param_type); } - // N.B. We cannot represent a heterogeneous list of types in our type system, so we - // use a heterogeneous tuple type to represent the list of types instead. - self.store_expression_type( - expr, - Type::heterogeneous_tuple(db, parameter_types.iter().copied()), - ); - let parameters = if return_todo { // TODO: `Unpack` Parameters::todo() @@ -3529,7 +3520,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } ast::Expr::Subscript(_) => { - self.infer_type_expression(expr); // TODO: Support `Concatenate[...]` return Ok(Type::paramspec_value_callable(db, Parameters::todo())); } @@ -3552,6 +3542,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Err(()); } + // This is to handle the following case: + // + // ```python + // from typing import ParamSpec + // + // class Foo[**P]: ... + // + // Foo[ParamSpec] # P: (ParamSpec, /) + // ``` Type::NominalInstance(nominal) if nominal.has_known_class(self.db(), KnownClass::ParamSpec) => { @@ -3593,7 +3592,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - _ => self.store_expression_type(expr, Type::unknown()), + _ => {} } if let Some(builder) = self.context.report_lint(&INVALID_TYPE_ARGUMENTS, expr) { From 106253f9da712dbb00a0970d2038416422867731 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 2 Dec 2025 11:15:00 +0530 Subject: [PATCH 38/59] Fix ide tests --- crates/ty_ide/src/hover.rs | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 3b4b463ee2f21..e95ab56233579 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1654,12 +1654,12 @@ def ab(a: int, *, c: int): r#" def outer(): x = "outer_value" - + def inner(): nonlocal x x = "modified" return x # Should find the nonlocal x declaration in outer scope - + return inner "#, ); @@ -1693,12 +1693,12 @@ def outer(): r#" def outer(): xy = "outer_value" - + def inner(): nonlocal xy xy = "modified" return x # Should find the nonlocal x declaration in outer scope - + return inner "#, ); @@ -1906,7 +1906,7 @@ def function(): def __init__(self, pos, btn): self.position: int = pos self.button: str = btn - + def my_func(event: Click): match event: case Click(x, button=ab): @@ -1926,7 +1926,7 @@ def function(): def __init__(self, pos, btn): self.position: int = pos self.button: str = btn - + def my_func(event: Click): match event: case Click(x, button=ab): @@ -1964,7 +1964,7 @@ def function(): def __init__(self, pos, btn): self.position: int = pos self.button: str = btn - + def my_func(event: Click): match event: case Click(x, button=ab): @@ -2003,7 +2003,7 @@ def function(): def __init__(self, pos, btn): self.position: int = pos self.button: str = btn - + def my_func(event: Click): match event: case Click(x, button=ab): @@ -2089,15 +2089,13 @@ def function(): "#, ); + // TODO: This should just be `**AB@Alias2 ()` + // https://github.com/astral-sh/ty/issues/1581 assert_snapshot!(test.hover(), @r" - ( - ... - ) -> tuple[typing.ParamSpec] + (**AB@Alias2) -> tuple[AB@Alias2] --------------------------------------------- ```python - ( - ... - ) -> tuple[typing.ParamSpec] + (**AB@Alias2) -> tuple[AB@Alias2] ``` --------------------------------------------- info[hover]: Hovered content is @@ -2238,12 +2236,11 @@ def function(): "#, ); - // TODO: This should be `P@Alias ()` assert_snapshot!(test.hover(), @r" - typing.ParamSpec + P@Alias (bivariant) --------------------------------------------- ```python - typing.ParamSpec + P@Alias (bivariant) ``` --------------------------------------------- info[hover]: Hovered content is From a67e53f600c0cd48fb48279a7b62c35260f1b551 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 2 Dec 2025 11:15:32 +0530 Subject: [PATCH 39/59] Display gradual parameters on single line --- crates/ty_python_semantic/src/types/display.rs | 8 ++++++-- crates/ty_python_semantic/src/types/signatures.rs | 4 ---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 243443c14a075..6bea7d6ea076e 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1629,8 +1629,12 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { // For `ParamSpec` kind, the parameters still contain `*args` and `**kwargs`, but we // display them as `**P` instead, so avoid multiline in that case. // TODO: This might change once we support `Concatenate` - let multiline = - self.settings.multiline && self.parameters.len() > 1 && !self.parameters.is_paramspec(); + let multiline = self.settings.multiline + && self.parameters.len() > 1 + && !matches!( + self.parameters.kind(), + ParametersKind::Gradual | ParametersKind::ParamSpec(_) + ); // Opening parenthesis f.write_char('(')?; if multiline { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index f06d8b1e86194..8803de7c90e77 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1476,10 +1476,6 @@ impl<'db> Parameters<'db> { matches!(self.kind, ParametersKind::Gradual) } - pub(crate) const fn is_paramspec(&self) -> bool { - matches!(self.kind, ParametersKind::ParamSpec(_)) - } - /// Return todo parameters: (*args: Todo, **kwargs: Todo) pub(crate) fn todo() -> Self { Self { From b358231686abed80e64c6c85dd68433a9a3f0693 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 2 Dec 2025 18:44:33 +0530 Subject: [PATCH 40/59] Correctly pass around the ParamSpec type variable --- .../resources/mdtest/decorators.md | 2 - .../mdtest/generics/pep695/paramspec.md | 14 +++- crates/ty_python_semantic/src/types.rs | 30 ++++++-- .../ty_python_semantic/src/types/call/bind.rs | 4 +- .../ty_python_semantic/src/types/generics.rs | 30 +++++--- .../src/types/infer/builder.rs | 5 +- .../src/types/signatures.rs | 71 ++++++++++++++----- 7 files changed, 116 insertions(+), 40 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/decorators.md b/crates/ty_python_semantic/resources/mdtest/decorators.md index 4fd24a07be5f4..f92eca800360e 100644 --- a/crates/ty_python_semantic/resources/mdtest/decorators.md +++ b/crates/ty_python_semantic/resources/mdtest/decorators.md @@ -126,8 +126,6 @@ def custom_decorator(f) -> Callable[[int], str]: def wrapper(*args, **kwargs): print("Calling decorated function") return f(*args, **kwargs) - # TODO: This shouldn't be an error - # error: [invalid-return-type] return wrapper @custom_decorator 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 7c76b6bb91275..a72bb5eb1499a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -415,7 +415,6 @@ def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> # error: [invalid-argument-type] "Argument to function `foo1` is incorrect: Expected `P2@foo2.args`, found `Literal[1]`" foo1(func, 1, *args, **kwargs) - # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `P1@foo1_with_extra_arg.args`, found `P2@foo2.args`" # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `str`, found `P2@foo2.args`" foo1_with_extra_arg(func, *args, **kwargs) ``` @@ -447,3 +446,16 @@ foo1(f1, 1, "a", "b") # error: [unknown-argument] "Argument `z` does not match any known parameter of function `foo1`" foo1(f1, x=1, z="a") ``` + +### Specializing `ParamSpec` with another `ParamSpec` + +```py +class Foo[**P]: + def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: + self.args = args + self.kwargs = kwargs + +def bar[**P](foo: Foo[P]) -> None: + reveal_type(foo.args) # revealed: Unknown | P@bar.args + reveal_type(foo.kwargs) # revealed: Unknown | P@bar.kwargs +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index d87e76869d338..94da8b6def296 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9956,12 +9956,30 @@ impl<'db> BoundTypeVarInstance<'db> { visitor: &ApplyTypeMappingVisitor<'db>, ) -> Type<'db> { match type_mapping { - TypeMapping::Specialization(specialization) => { - specialization.get(db, self).unwrap_or(Type::TypeVar(self)) - } - TypeMapping::PartialSpecialization(partial) => { - partial.get(db, self).unwrap_or(Type::TypeVar(self)) - } + TypeMapping::Specialization(specialization) => specialization + .get(db, self.without_paramspec_attr(db)) + .map(|ty| { + if let Some(attr) = self.paramspec_attr(db) + && let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + { + return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); + } + ty + }) + .unwrap_or(Type::TypeVar(self)), + TypeMapping::PartialSpecialization(partial) => partial + .get(db, self.without_paramspec_attr(db)) + .map(|ty| { + if let Some(attr) = self.paramspec_attr(db) + && let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + { + return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); + } + ty + }) + .unwrap_or(Type::TypeVar(self)), TypeMapping::BindSelf(self_type) => { if self.typevar(db).is_self(db) { *self_type diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 7c02ba4ef98f2..96b400ebcf383 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3049,7 +3049,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { - if let Some(paramspec) = paramspec { + if let Some((_, paramspec)) = paramspec { if self.try_paramspec_evaluation_at(argument_index, paramspec) { // Once we find an argument that matches the `ParamSpec`, we can stop checking // the remaining arguments since `ParamSpec` should always be the last @@ -3084,7 +3084,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } - if let Some(paramspec) = paramspec { + if let Some((_, paramspec)) = paramspec { // If we reach here, none of the arguments matched the `ParamSpec` parameter, but the // `ParamSpec` could specialize to a parameter list containing some parameters. self.evaluate_paramspec_sub_call(None, paramspec); diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4cd64808e09ca..c051492d0b16a 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1684,15 +1684,27 @@ impl<'db> SpecializationBuilder<'db> { let ParametersKind::ParamSpec(typevar) = formal_parameters.kind() else { return Ok(()); }; - let paramspec_value = Type::Callable(CallableType::new( - self.db, - CallableSignature::from_overloads( - actual_callable.signatures(self.db).iter().map(|signature| { - Signature::new(signature.parameters().clone(), None) - }), - ), - CallableTypeKind::ParamSpecValue, - )); + let paramspec_value = match actual_callable.signatures(self.db).as_slice() { + [] => return Ok(()), + [actual_signature] => match actual_signature.parameters().kind() { + ParametersKind::ParamSpec(typevar) => Type::TypeVar(typevar), + _ => Type::Callable(CallableType::new( + self.db, + CallableSignature::single(Signature::new( + actual_signature.parameters().clone(), + None, + )), + CallableTypeKind::ParamSpecValue, + )), + }, + actual_signatures => Type::Callable(CallableType::new( + self.db, + CallableSignature::from_overloads(actual_signatures.iter().map( + |signature| Signature::new(signature.parameters().clone(), None), + )), + CallableTypeKind::ParamSpecValue, + )), + }; self.add_type_mapping(typevar, paramspec_value, polarity, &mut f); } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6ca3a94888f3c..74d3e6b4dc7fe 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3529,10 +3529,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { match param_type { Type::TypeVar(typevar) if typevar.is_paramspec(db) => { - return Ok(Type::paramspec_value_callable( - db, - Parameters::paramspec(db, typevar), - )); + return Ok(param_type); } Type::KnownInstance(known_instance) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 8803de7c90e77..5855182e68497 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -203,18 +203,57 @@ impl<'db> CallableSignature<'db> { ) -> Self { if let TypeMapping::Specialization(specialization) = type_mapping && let [self_signature] = self.overloads.as_slice() - && let ParametersKind::ParamSpec(typevar) = self_signature.parameters.kind - && let Some(Type::Callable(callable)) = specialization.get(db, typevar) - && matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) + && let Some((prefix_parameters, typevar)) = self_signature + .parameters + .find_paramspec_from_args_kwargs(db) { - return Self::from_overloads(callable.signatures(db).iter().map(|signature| { - Signature::new( - signature.parameters.clone(), - self_signature - .return_ty - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), - ) - })); + let prefix_parameters = prefix_parameters + .iter() + .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) + .collect::>(); + + let return_ty = self_signature + .return_ty + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); + + match specialization.get(db, typevar) { + Some(Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { + return Self::single(Signature::new( + Parameters::new( + db, + prefix_parameters.into_iter().chain([ + Parameter::variadic(Name::new_static("args")).with_annotated_type( + Type::TypeVar( + typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), + ), + ), + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::TypeVar( + typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), + )), + ]), + ), + return_ty, + )); + } + Some(Type::Callable(callable)) + if matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) => + { + return Self::from_overloads(callable.signatures(db).iter().map(|signature| { + Signature::new( + Parameters::new( + db, + prefix_parameters + .iter() + .cloned() + .chain(signature.parameters().iter().cloned()), + ), + return_ty, + ) + })); + } + _ => {} + } } Self::from_overloads( @@ -1551,11 +1590,11 @@ impl<'db> Parameters<'db> { } /// Returns the bound `ParamSpec` type variable if the parameters contain a `ParamSpec`. - pub(crate) fn find_paramspec_from_args_kwargs( - &self, + pub(crate) fn find_paramspec_from_args_kwargs<'a>( + &'a self, db: &'db dyn Db, - ) -> Option> { - let [.., maybe_args, maybe_kwargs] = self.value.as_slice() else { + ) -> Option<(&'a [Parameter<'db>], BoundTypeVarInstance<'db>)> { + let [prefix @ .., maybe_args, maybe_kwargs] = self.value.as_slice() else { return None; }; @@ -1581,7 +1620,7 @@ impl<'db> Parameters<'db> { ) { let typevar = args_typevar.without_paramspec_attr(db); if typevar.is_same_typevar_as(db, kwargs_typevar.without_paramspec_attr(db)) { - return Some(typevar); + return Some((prefix, typevar)); } } From 0db8bd65e418953ade77a321254610a716e53e0e Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 3 Dec 2025 14:27:40 +0530 Subject: [PATCH 41/59] Pass generic context during paramspec specialization --- .../mdtest/generics/pep695/paramspec.md | 14 +++++++++++++ crates/ty_python_semantic/src/types.rs | 1 + .../src/types/signatures.rs | 20 +++++++++++++------ 3 files changed, 29 insertions(+), 6 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 a72bb5eb1499a..90ed876e2d29b 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -459,3 +459,17 @@ def bar[**P](foo: Foo[P]) -> None: reveal_type(foo.args) # revealed: Unknown | P@bar.args reveal_type(foo.kwargs) # revealed: Unknown | P@bar.kwargs ``` + +### Specializing `Self` when `ParamSpec` is involved + +```py +class Foo[**P]: + def method(self, *args: P.args, **kwargs: P.kwargs) -> str: + return "hello" + +foo = Foo[int, str]() + +reveal_type(foo) # revealed: Foo[(int, str, /)] +reveal_type(foo.method) # revealed: bound method Foo[(int, str, /)].method(int, str, /) -> str +reveal_type(foo.method(1, "a")) # revealed: str +``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index baa63d6dd1466..0207b6eb7b979 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -9870,6 +9870,7 @@ impl<'db> BoundTypeVarInstance<'db> { } pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { + debug_assert!(self.is_paramspec(db)); Self::new(db, self.typevar(db), self.binding_context(db), Some(kind)) } diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 88beac9a163f9..69a3e918208c3 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -212,14 +212,20 @@ impl<'db> CallableSignature<'db> { .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) .collect::>(); + let generic_context = self_signature + .generic_context + .map(|context| type_mapping.update_signature_generic_context(db, context)); + let return_ty = self_signature .return_ty .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); match specialization.get(db, typevar) { Some(Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { - return Self::single(Signature::new( - Parameters::new( + return Self::single(Signature { + generic_context, + definition: self_signature.definition, + parameters: Parameters::new( db, prefix_parameters.into_iter().chain([ Parameter::variadic(Name::new_static("args")).with_annotated_type( @@ -234,14 +240,16 @@ impl<'db> CallableSignature<'db> { ]), ), return_ty, - )); + }); } Some(Type::Callable(callable)) if matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) => { return Self::from_overloads(callable.signatures(db).iter().map(|signature| { - Signature::new( - Parameters::new( + Signature { + generic_context, + definition: signature.definition, + parameters: Parameters::new( db, prefix_parameters .iter() @@ -249,7 +257,7 @@ impl<'db> CallableSignature<'db> { .chain(signature.parameters().iter().cloned()), ), return_ty, - ) + } })); } _ => {} From cb4aa2d767d00f8d0d4c8fa0187f42f1aed60b3f Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 3 Dec 2025 22:01:47 +0530 Subject: [PATCH 42/59] Add upper bound for `P.args` and `P.kwargs` --- .../mdtest/generics/pep695/paramspec.md | 11 ++ crates/ty_python_semantic/src/types.rs | 146 +++++++++++------- .../ty_python_semantic/src/types/call/bind.rs | 125 ++++++++++----- 3 files changed, 189 insertions(+), 93 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 90ed876e2d29b..3226db87ae265 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -456,10 +456,21 @@ class Foo[**P]: self.kwargs = kwargs def bar[**P](foo: Foo[P]) -> None: + reveal_type(foo) # revealed: Foo[P@bar] reveal_type(foo.args) # revealed: Unknown | P@bar.args reveal_type(foo.kwargs) # revealed: Unknown | P@bar.kwargs ``` +ty will check whether the argument after `**` is a mapping type but as instance attribute are +unioned with `Unknown`, it shouldn't error here. + +```py +from typing import Callable + +def baz[**P](fn: Callable[P, None], foo: Foo[P]) -> None: + fn(*foo.args, **foo.kwargs) +``` + ### Specializing `Self` when `ParamSpec` is involved ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index cce938bff0a89..6b42595374441 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2378,33 +2378,6 @@ impl<'db> Type<'db> { (_, Type::TypeVar(bound_typevar)) if bound_typevar.is_inferable(db, inferable) => { ConstraintSet::from(false) } - (Type::TypeVar(bound_typevar), _) - if bound_typevar.paramspec_attr(db) == Some(ParamSpecAttrKind::Args) => - { - Type::homogeneous_tuple(db, Type::object()).has_relation_to_impl( - db, - target, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } - (Type::TypeVar(bound_typevar), _) - if bound_typevar.paramspec_attr(db) == Some(ParamSpecAttrKind::Kwargs) => - { - KnownClass::Dict - .to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]) - .top_materialization(db) - .has_relation_to_impl( - db, - target, - inferable, - relation, - relation_visitor, - disjointness_visitor, - ) - } (Type::TypeVar(bound_typevar), _) => { // All inferable cases should have been handled above assert!(!bound_typevar.is_inferable(db, inferable)); @@ -5015,7 +4988,7 @@ impl<'db> Type<'db> { Type::TypeVar(typevar) if typevar.is_paramspec(db) && matches!(name_str, "args" | "kwargs") => { - Place::bound(Type::TypeVar(match name_str { + Place::declared(Type::TypeVar(match name_str { "args" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), "kwargs" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), _ => unreachable!(), @@ -7888,8 +7861,8 @@ impl<'db> Type<'db> { typevars: &mut FxOrderSet>, visitor: &FindLegacyTypeVarsVisitor<'db>, ) { - let matching_typevar = - |bound_typevar: &BoundTypeVarInstance<'db>| match bound_typevar.typevar(db).kind(db) { + let matching_typevar = |bound_typevar: &BoundTypeVarInstance<'db>| { + match bound_typevar.typevar(db).kind(db) { TypeVarKind::Legacy | TypeVarKind::TypingSelf if binding_context.is_none_or(|binding_context| { bound_typevar.binding_context(db) @@ -7904,7 +7877,8 @@ impl<'db> Type<'db> { Some(bound_typevar.without_paramspec_attr(db)) } _ => None, - }; + } + }; match self { Type::TypeVar(bound_typevar) => { @@ -9941,13 +9915,57 @@ impl<'db> BoundTypeVarInstance<'db> { self.kind(db).is_paramspec() } + /// Returns a new bound typevar instance with the given `ParamSpec` attribute set. + /// + /// This method will also set an appropriate upper bound on the typevar, based on the + /// attribute kind. For `P.args`, the upper bound will be `tuple[object, ...]`, and for + /// `P.kwargs`, the upper bound will be `Top[dict[str, Any]]`. + /// + /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` + /// type variable. pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { debug_assert!(self.is_paramspec(db)); - Self::new(db, self.typevar(db), self.binding_context(db), Some(kind)) + + let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind { + ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()), + ParamSpecAttrKind::Kwargs => KnownClass::Dict + .to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()]) + .top_materialization(db), + }); + + let typevar = TypeVarInstance::new( + db, + self.typevar(db).identity(db), + Some(TypeVarBoundOrConstraintsEvaluation::Eager(upper_bound)), + None, // explicit_variance + None, // _default + ); + + Self::new(db, typevar, self.binding_context(db), Some(kind)) } + /// Returns a new bound typevar instance without any `ParamSpec` attribute set. + /// + /// This method will also remove any upper bound that was set by `with_paramspec_attr`. This + /// means that the returned typevar will have no upper bound or constraints. + /// + /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` + /// type variable. pub(crate) fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { - Self::new(db, self.typevar(db), self.binding_context(db), None) + debug_assert!(self.is_paramspec(db)); + + Self::new( + db, + TypeVarInstance::new( + db, + self.typevar(db).identity(db), + None, // Remove the upper bound set by `with_paramspec_attr` + None, // explicit_variance + None, // _default + ), + self.binding_context(db), + None, + ) } /// Returns whether two bound typevars represent the same logical typevar, regardless of e.g. @@ -10053,30 +10071,44 @@ impl<'db> BoundTypeVarInstance<'db> { visitor: &ApplyTypeMappingVisitor<'db>, ) -> Type<'db> { match type_mapping { - TypeMapping::Specialization(specialization) => specialization - .get(db, self.without_paramspec_attr(db)) - .map(|ty| { - if let Some(attr) = self.paramspec_attr(db) - && let Type::TypeVar(typevar) = ty - && typevar.is_paramspec(db) - { - return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); - } - ty - }) - .unwrap_or(Type::TypeVar(self)), - TypeMapping::PartialSpecialization(partial) => partial - .get(db, self.without_paramspec_attr(db)) - .map(|ty| { - if let Some(attr) = self.paramspec_attr(db) - && let Type::TypeVar(typevar) = ty - && typevar.is_paramspec(db) - { - return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); - } - ty - }) - .unwrap_or(Type::TypeVar(self)), + TypeMapping::Specialization(specialization) => { + let typevar = if self.is_paramspec(db) { + self.without_paramspec_attr(db) + } else { + self + }; + specialization + .get(db, typevar) + .map(|ty| { + if let Some(attr) = self.paramspec_attr(db) + && let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + { + return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); + } + ty + }) + .unwrap_or(Type::TypeVar(self)) + } + TypeMapping::PartialSpecialization(partial) => { + let typevar = if self.is_paramspec(db) { + self.without_paramspec_attr(db) + } else { + self + }; + partial + .get(db, typevar) + .map(|ty| { + if let Some(attr) = self.paramspec_attr(db) + && let Type::TypeVar(typevar) = ty + && typevar.is_paramspec(db) + { + return Type::TypeVar(typevar.with_paramspec_attr(db, attr)); + } + ty + }) + .unwrap_or(Type::TypeVar(self)) + } TypeMapping::BindSelf { self_type, binding_context, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index b6bb0f97e477b..05d365bc39898 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2571,17 +2571,39 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { } let variadic_type = match argument_type { + Some(argument_type @ Type::Union(union)) => { + // When accessing an instance attribute that is a `P.args`, the type we infer is + // `Unknown | P.args`. This needs to be special cased here to avoid calling + // `iterate` on it which will lose the `ParamSpec` information as it will return + // `object` that comes from the upper bound of `P.args`. What we want is to always + // use the `P.args` type to perform type checking against the parameter type. This + // will allow us to error when `*args: P.args` is matched against, for example, + // `n: int` and correctly type check when `*args: P.args` is matched against + // `*args: P.args`. + match union.elements(db) { + [paramspec @ Type::TypeVar(typevar), other] + | [other, paramspec @ Type::TypeVar(typevar)] + if typevar.is_paramspec(db) && other.is_unknown() => + { + VariadicArgumentType::ParamSpec(*paramspec) + } + _ => { + // TODO: Same todo comment as in the non-paramspec case below + VariadicArgumentType::Other(argument_type.iterate(db)) + } + } + } Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { VariadicArgumentType::ParamSpec(paramspec) } - Some(ty) => { + Some(argument_type) => { // TODO: `Type::iterate` internally handles unions, but in a lossy way. // It might be superior here to manually map over the union and call `try_iterate` // on each element, similar to the way that `unpacker.rs` does in the `unpack_inner` method. // It might be a bit of a refactor, though. // See // for more details. --Alex - VariadicArgumentType::Other(ty.iterate(db)) + VariadicArgumentType::Other(argument_type.iterate(db)) } None => VariadicArgumentType::None, }; @@ -2672,24 +2694,36 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { ); } } else { + let dunder_getitem_return_type = |ty: Type<'db>| match ty + .member_lookup_with_policy( + db, + Name::new_static("__getitem__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Defined(getitem_method, _, Definedness::AlwaysDefined) => getitem_method + .try_call(db, &CallArguments::positional([Type::unknown()])) + .ok() + .map_or_else(Type::unknown, |bindings| bindings.return_type(db)), + _ => Type::unknown(), + }; + let value_type = match argument_type { - Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => paramspec, - Some(ty) => { - match ty - .member_lookup_with_policy( - db, - Name::new_static("__getitem__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place - { - Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method - .try_call(db, &CallArguments::positional([Type::unknown()])) - .ok() - .map_or_else(Type::unknown, |bindings| bindings.return_type(db)), - _ => Type::unknown(), + Some(argument_type @ Type::Union(union)) => { + // See the comment in `match_variadic` for why we special case this situation. + match union.elements(db) { + [paramspec @ Type::TypeVar(typevar), other] + | [other, paramspec @ Type::TypeVar(typevar)] + if typevar.is_paramspec(db) && other.is_unknown() => + { + *paramspec + } + _ => dunder_getitem_return_type(argument_type), } } + Some(paramspec @ Type::TypeVar(typevar)) if typevar.is_paramspec(db) => paramspec, + Some(argument_type) => dunder_getitem_return_type(argument_type), None => Type::unknown(), }; @@ -3206,11 +3240,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { ); } } else { - let value_type = if let Type::TypeVar(typevar) = argument_type - && typevar.is_paramspec(self.db) - { - argument_type - } else { + let mut value_type_fallback = |argument_type: Type<'db>| { // TODO: Instead of calling the `keys` and `__getitem__` methods, we should // instead get the constraints which satisfies the `SupportsKeysAndGetItem` // protocol i.e., the key and value type. @@ -3242,7 +3272,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { argument_index: adjusted_argument_index, provided_ty: argument_type, }); - return; + return None; }; if !key_type @@ -3259,20 +3289,43 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { }); } - match argument_type - .member_lookup_with_policy( - self.db, - Name::new_static("__getitem__"), - MemberLookupPolicy::NO_INSTANCE_FALLBACK, - ) - .place - { - Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method - .try_call(self.db, &CallArguments::positional([Type::unknown()])) - .ok() - .map_or_else(Type::unknown, |bindings| bindings.return_type(self.db)), - _ => Type::unknown(), + Some( + match argument_type + .member_lookup_with_policy( + self.db, + Name::new_static("__getitem__"), + MemberLookupPolicy::NO_INSTANCE_FALLBACK, + ) + .place + { + Place::Defined(keys_method, _, Definedness::AlwaysDefined) => keys_method + .try_call(self.db, &CallArguments::positional([Type::unknown()])) + .ok() + .map_or_else(Type::unknown, |bindings| bindings.return_type(self.db)), + _ => Type::unknown(), + }, + ) + }; + + let value_type = match argument_type { + Type::Union(union) => { + // See the comment in `match_variadic` for why we special case this situation. + match union.elements(self.db) { + [paramspec @ Type::TypeVar(typevar), other] + | [other, paramspec @ Type::TypeVar(typevar)] + if typevar.is_paramspec(self.db) && other.is_unknown() => + { + Some(*paramspec) + } + _ => value_type_fallback(argument_type), + } } + Type::TypeVar(typevar) if typevar.is_paramspec(self.db) => Some(argument_type), + _ => value_type_fallback(argument_type), + }; + + let Some(value_type) = value_type else { + return; }; for (argument_type, parameter_index) in From 0a1a26df12cd1daa93bbc6173e2a8e09a74bc038 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 3 Dec 2025 23:24:33 +0530 Subject: [PATCH 43/59] Allow `ParamSpec` as annotation (revert) --- .../resources/mdtest/annotations/unsupported_special_forms.md | 3 +-- crates/ty_python_semantic/src/types.rs | 3 +++ 2 files changed, 4 insertions(+), 2 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 18ebd03682cab..875fbebeb6b29 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 @@ -64,9 +64,8 @@ def _( reveal_type(c) # revealed: Unknown reveal_type(d) # revealed: Unknown - # error: [invalid-type-form] "Variable of type `ParamSpec` is not allowed in a type expression" def foo(a_: e) -> None: - reveal_type(a_) # revealed: Unknown + reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec` instances in type expressions) ``` ## Inheritance diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6b42595374441..1c0f47863d56a 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7429,6 +7429,9 @@ impl<'db> Type<'db> { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), + Some(KnownClass::ParamSpec) => Ok(todo_type!( + "Support for `typing.ParamSpec` instances in type expressions" + )), Some(KnownClass::TypeVarTuple) => Ok(todo_type!( "Support for `typing.TypeVarTuple` instances in type expressions" )), From ca5ecc873cacdd99697bb95a4b786db2bac6fda6 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Wed, 3 Dec 2025 23:24:52 +0530 Subject: [PATCH 44/59] Allow passing `Any` to specialize a `ParamSpec` --- .../mdtest/generics/legacy/paramspec.md | 1 + .../mdtest/generics/pep695/paramspec.md | 1 + .../src/types/infer/builder.rs | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 5 deletions(-) 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 fecd95967ec19..2b156ab2741a3 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -245,6 +245,7 @@ But, they cannot be omitted when there are multiple type variables. reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int +reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int # TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int` # TODO: error: paramspec is unbound 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 3226db87ae265..ded49d6e22417 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -210,6 +210,7 @@ But, they cannot be omitted when there are multiple type variables. reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int +reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int # TODO: error: paramspec is unbound reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index ae4aa307f7f2f..904ec0d25df0d 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3563,7 +3563,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); } - _ => { + _ if exactly_one_paramspec => { // Square brackets are optional when `ParamSpec` is the only type variable // being specialized. This means that a single name expression represents a // parameter list with a single parameter. For example, @@ -3573,8 +3573,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // // OnlyParamSpec[int] # P: (int, /) // ``` - if exactly_one_paramspec { - let parameters = if param_type.is_todo() { + let parameters = + if param_type.is_todo() { Parameters::todo() } else { Parameters::new( @@ -3583,9 +3583,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .with_annotated_type(param_type)], ) }; - return Ok(Type::paramspec_value_callable(db, parameters)); - } + return Ok(Type::paramspec_value_callable(db, parameters)); } + + Type::Dynamic(DynamicType::Any) => { + return Ok(Type::paramspec_value_callable( + db, + Parameters::gradual_form(), + )); + } + + _ => {} } } From 6cfc7b9e0b1f2db9ada9af550dc061d6a9e178f6 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 4 Dec 2025 12:44:00 +0530 Subject: [PATCH 45/59] Add test for operations on `P.args` / `P.kwargs` --- .../mdtest/generics/pep695/paramspec.md | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) 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 ded49d6e22417..a1880ad84644f 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -136,6 +136,8 @@ class Foo[**P]: The type of `args` and `kwargs` inside the function is `P.args` and `P.kwargs` respectively instead of `tuple[P.args, ...]` and `dict[str, P.kwargs]`. +### Passing `*args` and `**kwargs` to a callable + ```py from typing import Callable @@ -153,13 +155,33 @@ def f[**P](func: Callable[P, int]) -> Callable[P, None]: reveal_type(func(args, kwargs)) # revealed: int # Both parameters are required - # TODO: error and reveal `Unknown` + # TODO: error reveal_type(func()) # revealed: int reveal_type(func(*args)) # revealed: int reveal_type(func(**kwargs)) # revealed: int return wrapper ``` +### Operations on `P.args` and `P.kwargs` + +The type of `P.args` and `P.kwargs` behave like a `tuple` and `dict` respectively. Internally, they +are represented as a type variable that has an upper bound of `tuple[object, ...]` and +`Top[dict[str, Any]]` respectively. + +```py +from typing import Callable, Any + +def f[**P](func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: + reveal_type(args + ("extra",)) # revealed: tuple[object, ...] + reveal_type(args + (1, 2, 3)) # revealed: tuple[object, ...] + reveal_type(args[0]) # revealed: object + + reveal_type("key" in kwargs) # revealed: bool + # TODO: Should be `object | None` + reveal_type(kwargs.get("key")) # revealed: object + reveal_type(kwargs["key"]) # revealed: object +``` + ## Specializing generic classes explicitly ```py From 774389b415b31c04376db7fede4bdca650c9be47 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 4 Dec 2025 14:20:13 +0530 Subject: [PATCH 46/59] Add tests with overloads --- .../mdtest/generics/pep695/paramspec.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) 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 a1880ad84644f..9a11cbcfca898 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -507,3 +507,58 @@ reveal_type(foo) # revealed: Foo[(int, str, /)] reveal_type(foo.method) # revealed: bound method Foo[(int, str, /)].method(int, str, /) -> str reveal_type(foo.method(1, "a")) # revealed: str ``` + +### Overloads + +`overloaded.pyi`: + +```pyi +from typing import overload + +@overload +def int_int(x: int) -> int: ... +@overload +def int_int(x: str) -> int: ... + +@overload +def int_str(x: int) -> int: ... +@overload +def int_str(x: str) -> str: ... + +@overload +def str_str(x: int) -> str: ... +@overload +def str_str(x: str) -> str: ... +``` + +```py +from typing import Callable +from overloaded import int_int, int_str, str_str + +def change_return_type[**P](f: Callable[P, int]) -> Callable[P, str]: + def nested(*args: P.args, **kwargs: P.kwargs) -> str: + return str(f(*args, **kwargs)) + return nested + +def with_parameters[**P]( + f: Callable[P, int], *args: P.args, **kwargs: P.kwargs +) -> Callable[P, str]: + def nested(*args: P.args, **kwargs: P.kwargs) -> str: + return str(f(*args, **kwargs)) + return nested + +reveal_type(change_return_type(int_int)) # revealed: Overload[(x: int) -> str, (x: str) -> str] + +# TODO: This shouldn't error and should pick the first overload because of the return type +# error: [invalid-argument-type] +reveal_type(change_return_type(int_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str] + +# error: [invalid-argument-type] +reveal_type(change_return_type(str_str)) # revealed: Overload[(x: int) -> str, (x: str) -> str] + +# TODO: Both of these shouldn't raise an error +# error: [invalid-argument-type] +reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str, (x: str) -> str] +# error: [invalid-argument-type] +reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str] +``` From 9767d9e62e61d139fc33d2782fe764c502e49e79 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 4 Dec 2025 14:53:40 +0530 Subject: [PATCH 47/59] Remove leftover code from using the new solver --- .../src/types/signatures.rs | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 69a3e918208c3..f37b402aac206 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1067,44 +1067,6 @@ impl<'db> Signature<'db> { return ConstraintSet::from(relation.is_assignability()); } - // TODO: Handle `Concatenate` - match (self.parameters.kind(), other.parameters.kind()) { - (ParametersKind::ParamSpec(self_typevar), ParametersKind::ParamSpec(other_typevar)) => { - return if self_typevar.is_same_typevar_as(db, other_typevar) { - ConstraintSet::from(true) - } else { - ConstraintSet::constrain_typevar( - db, - self_typevar, - Type::TypeVar(other_typevar), - Type::TypeVar(other_typevar), - relation, - ) - }; - } - (ParametersKind::ParamSpec(typevar), _) => { - let paramspec_value = Type::paramspec_value_callable(db, other.parameters.clone()); - return ConstraintSet::constrain_typevar( - db, - typevar, - paramspec_value, - paramspec_value, - relation, - ); - } - (_, ParametersKind::ParamSpec(typevar)) => { - let paramspec_value = Type::paramspec_value_callable(db, self.parameters.clone()); - return ConstraintSet::constrain_typevar( - db, - typevar, - paramspec_value, - paramspec_value, - relation, - ); - } - _ => {} - } - let mut parameters = ParametersZip { current_self: None, current_other: None, From 5c870858055740a1a7e9505b2aaf12bb1b7e835a Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Thu, 4 Dec 2025 15:18:49 +0530 Subject: [PATCH 48/59] Run pre-commit --- .../resources/mdtest/generics/pep695/paramspec.md | 4 +--- 1 file changed, 1 insertion(+), 3 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 9a11cbcfca898..51e4e2320b1b8 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -540,9 +540,7 @@ def change_return_type[**P](f: Callable[P, int]) -> Callable[P, str]: return str(f(*args, **kwargs)) return nested -def with_parameters[**P]( - f: Callable[P, int], *args: P.args, **kwargs: P.kwargs -) -> Callable[P, str]: +def with_parameters[**P](f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> Callable[P, str]: def nested(*args: P.args, **kwargs: P.kwargs) -> str: return str(f(*args, **kwargs)) return nested From cffe40f90c2ed18913311bb0ff69ca63ab361c84 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 10:58:51 +0530 Subject: [PATCH 49/59] Address Alex's review comment on mdtests --- .../annotations/unsupported_special_forms.md | 3 +- .../mdtest/generics/legacy/paramspec.md | 59 +++++++++++++++---- .../mdtest/generics/pep695/paramspec.md | 34 ++++++++++- crates/ty_python_semantic/src/types.rs | 3 - 4 files changed, 81 insertions(+), 18 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 875fbebeb6b29..18ebd03682cab 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 @@ -64,8 +64,9 @@ def _( reveal_type(c) # revealed: Unknown reveal_type(d) # revealed: Unknown + # error: [invalid-type-form] "Variable of type `ParamSpec` is not allowed in a type expression" def foo(a_: e) -> None: - reveal_type(a_) # revealed: @Todo(Support for `typing.ParamSpec` instances in type expressions) + reveal_type(a_) # revealed: Unknown ``` ## Inheritance 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 2b156ab2741a3..fafddd347f505 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -118,17 +118,25 @@ class B: ... ## Validating `ParamSpec` usage -`ParamSpec` is only valid as the first element to `Callable` or the final element to `Concatenate`. +In type annotations, `ParamSpec` is only valid as the first element to `Callable`, the final element +to `Concatenate`, or as a type parameter to `Protocol` or `Generic`. ```py -from typing import ParamSpec, Callable, Concatenate +from typing import ParamSpec, Callable, Concatenate, Protocol, Generic P = ParamSpec("P") +class ValidProtocol(Protocol[P]): + def method(self, c: Callable[P, int]) -> None: ... + +class ValidGeneric(Generic[P]): + def method(self, c: Callable[P, int]) -> None: ... + def valid( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], ) -> None: ... + def invalid( # TODO: error a1: P, @@ -149,16 +157,19 @@ The components of `ParamSpec` i.e., `P.args` and `P.kwargs` are only valid when annotated types of `*args` and `**kwargs` respectively. ```py -from typing import Callable, ParamSpec +from typing import Generic, Callable, ParamSpec P = ParamSpec("P") -def foo(c: Callable[P, int]) -> None: +def foo1(c: Callable[P, int]) -> None: def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ... - # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" - # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" - def nested2(*args: P.kwargs, **kwargs: P.args) -> None: ... + def nested2( + # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" + *args: P.kwargs, + # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" + **kwargs: P.args, + ) -> None: ... # TODO: error def nested3(*args: P.args) -> None: ... @@ -166,11 +177,14 @@ def foo(c: Callable[P, int]) -> None: # TODO: error def nested4(**kwargs: P.kwargs) -> None: ... + # TODO: error + def nested5(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ... + # TODO: error -def bar(*args: P.args, **kwargs: P.kwargs) -> None: +def bar1(*args: P.args, **kwargs: P.kwargs) -> None: pass -class Foo: +class Foo1: # TODO: error def method(self, *args: P.args, **kwargs: P.kwargs) -> None: ... ``` @@ -178,14 +192,14 @@ class Foo: And, they need to be used together. ```py -def foo(c: Callable[P, int]) -> None: +def foo2(c: Callable[P, int]) -> None: # TODO: error def nested1(*args: P.args) -> None: ... # TODO: error def nested2(**kwargs: P.kwargs) -> None: ... -class Foo: +class Foo2: # TODO: error args: P.args @@ -193,6 +207,22 @@ class Foo: kwargs: P.kwargs ``` +The name of these parameters does not need to be `args` or `kwargs`, it's the annotated type to the +respective variadic parameter that matters. + +```py +class Foo3(Generic[P]): + def method1(self, *paramspec_args: P.args, **paramspec_kwargs: P.kwargs) -> None: ... + + def method2( + self, + # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" + *paramspec_args: P.kwargs, + # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" + **paramspec_kwargs: P.args, + ) -> None: ... +``` + ## Specializing generic classes explicitly ```py @@ -254,7 +284,7 @@ reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown ``` -Nor can they be omitted when there are more than one `ParamSpec`. +Nor can they be omitted when there are more than one `ParamSpec`s. ```py p = TwoParamSpec[[int, str], [int]]() @@ -337,6 +367,11 @@ reveal_type(p3.attr2) # revealed: (str, /) -> None # Un-ordered type variables as the default of `PAnother` is `P` class ParamSpecWithDefault5(Generic[PAnother, P]): attr: Callable[PAnother, None] + +# TODO: error +# PAnother has default as P (another ParamSpec) which is not in scope +class ParamSpecWithDefault6(Generic[PAnother]): + attr: Callable[PAnother, None] ``` ## Semantics 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 51e4e2320b1b8..12ad24b10d296 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -131,6 +131,22 @@ class Foo[**P]: kwargs: P.kwargs ``` +The name of these parameters does not need to be `args` or `kwargs`, it's the annotated type to the +respective variadic parameter that matters. + +```py +class Foo3[**P]: + def method1(self, *paramspec_args: P.args, **paramspec_kwargs: P.kwargs) -> None: ... + + def method2( + self, + # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" + *paramspec_args: P.kwargs, + # error: [invalid-type-form] "`P.args` is valid only in `*args` annotation: Did you mean `P.kwargs`?" + **paramspec_kwargs: P.args, + ) -> None: ... +``` + ## Semantics of `P.args` and `P.kwargs` The type of `args` and `kwargs` inside the function is `P.args` and `P.kwargs` respectively instead @@ -177,7 +193,6 @@ def f[**P](func: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: reveal_type(args[0]) # revealed: object reveal_type("key" in kwargs) # revealed: bool - # TODO: Should be `object | None` reveal_type(kwargs.get("key")) # revealed: object reveal_type(kwargs["key"]) # revealed: object ``` @@ -277,7 +292,7 @@ class ParamSpecWithDefault3[**P1, **P2 = P1]: attr1: Callable[P1, None] attr2: Callable[P2, None] -# `P1` hasn't been specialized, so it defaults to `Unknown` gradual form +# `P1` hasn't been specialized, so it defaults to `...` gradual form p1 = ParamSpecWithDefault3() reveal_type(p1.attr1) # revealed: (...) -> None reveal_type(p1.attr2) # revealed: (...) -> None @@ -388,6 +403,19 @@ reveal_type(multiple(xy, xy)) # revealed: (x: int, y: str) -> bool # error: [invalid-argument-type] reveal_type(multiple(xy, yx)) # revealed: (x: int, y: str) -> bool +def keyword_only_with_default_1(*, x: int = 42) -> int: + return 1 + +def keyword_only_with_default_2(*, y: int = 42) -> int: + return 2 + +# The common supertype for two functions with only keyword-only parameters would be an empty +# parameter list i.e., `()` +# TODO: This shouldn't error +# error: [invalid-argument-type] +# revealed: (*, x: int = Literal[42]) -> bool +reveal_type(multiple(keyword_only_with_default_1, keyword_only_with_default_2)) + def keyword_only1(*, x: int) -> int: return 1 @@ -440,6 +468,8 @@ def foo2[**P2](func: Callable[P2, int], *args: P2.args, **kwargs: P2.kwargs) -> # error: [invalid-argument-type] "Argument to function `foo1_with_extra_arg` is incorrect: Expected `str`, found `P2@foo2.args`" foo1_with_extra_arg(func, *args, **kwargs) + + foo1_with_extra_arg(func, "extra", *args, **kwargs) ``` Here, the first argument to `f` can specialize `P` to the parameters of the callable passed to it diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index df73702809ae3..6c0086b6410a0 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7430,9 +7430,6 @@ impl<'db> Type<'db> { Some(KnownClass::TypeVar) => Ok(todo_type!( "Support for `typing.TypeVar` instances in type expressions" )), - Some(KnownClass::ParamSpec) => Ok(todo_type!( - "Support for `typing.ParamSpec` instances in type expressions" - )), Some(KnownClass::TypeVarTuple) => Ok(todo_type!( "Support for `typing.TypeVarTuple` instances in type expressions" )), From bffdffe453b3b5d5462499ab7911b65e93499fef Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 11:42:28 +0530 Subject: [PATCH 50/59] Address Alex's review comments on code changes --- .../mdtest/generics/legacy/paramspec.md | 3 - .../mdtest/generics/pep695/paramspec.md | 1 - crates/ty_python_semantic/src/types.rs | 61 +++++++++++++------ .../ty_python_semantic/src/types/call/bind.rs | 2 +- .../ty_python_semantic/src/types/display.rs | 4 +- .../src/types/infer/builder.rs | 14 ++--- 6 files changed, 51 insertions(+), 34 deletions(-) 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 fafddd347f505..93072cf3699da 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -136,7 +136,6 @@ def valid( a1: Callable[P, int], a2: Callable[Concatenate[int, P], int], ) -> None: ... - def invalid( # TODO: error a1: P, @@ -163,7 +162,6 @@ P = ParamSpec("P") def foo1(c: Callable[P, int]) -> None: def nested1(*args: P.args, **kwargs: P.kwargs) -> None: ... - def nested2( # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" *args: P.kwargs, @@ -213,7 +211,6 @@ respective variadic parameter that matters. ```py class Foo3(Generic[P]): def method1(self, *paramspec_args: P.args, **paramspec_kwargs: P.kwargs) -> None: ... - def method2( self, # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" 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 12ad24b10d296..8a6101a42476a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -137,7 +137,6 @@ respective variadic parameter that matters. ```py class Foo3[**P]: def method1(self, *paramspec_args: P.args, **paramspec_kwargs: P.kwargs) -> None: ... - def method2( self, # error: [invalid-type-form] "`P.kwargs` is valid only in `**kwargs` annotation: Did you mean `P.args`?" diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 6c0086b6410a0..ee043009c96b1 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4986,14 +4986,16 @@ impl<'db> Type<'db> { .into() } - Type::TypeVar(typevar) - if typevar.is_paramspec(db) && matches!(name_str, "args" | "kwargs") => - { - Place::declared(Type::TypeVar(match name_str { - "args" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), - "kwargs" => typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), - _ => unreachable!(), - })) + Type::TypeVar(typevar) if name_str == "args" && typevar.is_paramspec(db) => { + Place::declared(Type::TypeVar( + typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), + )) + .into() + } + Type::TypeVar(typevar) if name_str == "kwargs" && typevar.is_paramspec(db) => { + Place::declared(Type::TypeVar( + typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), + )) .into() } @@ -8603,8 +8605,6 @@ pub struct TrackedConstraintSet<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for TrackedConstraintSet<'_> {} -// TODO: The origin is either `TypeVarInstance` or `BoundTypeVarInstance` - /// Singleton types that are heavily special-cased by ty. Despite its name, /// quite a different type to [`NominalInstanceType`]. /// @@ -9723,7 +9723,15 @@ impl<'db> TypeVarInstance<'db> { }), ) }), - Type::Dynamic(DynamicType::Todo(_)) => Parameters::todo(), + Type::Dynamic(dynamic) => match dynamic { + DynamicType::Todo(_) + | DynamicType::TodoUnpack + | DynamicType::TodoStarredExpression => Parameters::todo(), + DynamicType::Any + | DynamicType::Unknown + | DynamicType::UnknownGeneric(_) + | DynamicType::Divergent(_) => Parameters::unknown(), + }, Type::TypeVar(typevar) if typevar.is_paramspec(db) => { return ty; } @@ -9756,7 +9764,7 @@ impl<'db> TypeVarInstance<'db> { let known_class = func_ty.as_class_literal().and_then(|cls| cls.known(db)); let expr = &call_expr.arguments.find_keyword("default")?.value; let default_type = definition_expression_type(db, definition, expr); - if matches!(known_class, Some(KnownClass::ParamSpec)) { + if known_class == Some(KnownClass::ParamSpec) { Some(convert_type_to_paramspec_value(db, default_type)) } else { Some(default_type) @@ -9899,16 +9907,25 @@ impl std::fmt::Display for ParamSpecAttrKind { pub struct BoundTypeVarIdentity<'db> { pub(crate) identity: TypeVarIdentity<'db>, pub(crate) binding_context: BindingContext<'db>, + /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component + /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. paramspec_attr: Option, } /// A type variable that has been bound to a generic context, and which can be specialized to a /// concrete type. +/// +/// # Ordering +/// +/// Ordering is based on the wrapped data's salsa-assigned id and not on its values. +/// The id may change between runs, or when e.g. a `BoundTypeVarInstance` was garbage-collected and recreated. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] #[derive(PartialOrd, Ord)] pub struct BoundTypeVarInstance<'db> { pub typevar: TypeVarInstance<'db>, binding_context: BindingContext<'db>, + /// If [`Some`], this indicates that this type variable is the `args` or `kwargs` component + /// of a `ParamSpec` i.e., `P.args` or `P.kwargs`. paramspec_attr: Option, } @@ -9949,7 +9966,11 @@ impl<'db> BoundTypeVarInstance<'db> { /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` /// type variable. pub(crate) fn with_paramspec_attr(self, db: &'db dyn Db, kind: ParamSpecAttrKind) -> Self { - debug_assert!(self.is_paramspec(db)); + debug_assert!( + self.is_paramspec(db), + "Expected a ParamSpec, got {:?}", + self.kind(db) + ); let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind { ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()), @@ -9962,8 +9983,8 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).identity(db), Some(TypeVarBoundOrConstraintsEvaluation::Eager(upper_bound)), - None, // explicit_variance - None, // _default + None, // ParamSpecs cannot have explicit variance + None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can ); Self::new(db, typevar, self.binding_context(db), Some(kind)) @@ -9977,7 +9998,11 @@ impl<'db> BoundTypeVarInstance<'db> { /// It's the caller's responsibility to ensure that this method is only called on a `ParamSpec` /// type variable. pub(crate) fn without_paramspec_attr(self, db: &'db dyn Db) -> Self { - debug_assert!(self.is_paramspec(db)); + debug_assert!( + self.is_paramspec(db), + "Expected a ParamSpec, got {:?}", + self.kind(db) + ); Self::new( db, @@ -9985,8 +10010,8 @@ impl<'db> BoundTypeVarInstance<'db> { db, self.typevar(db).identity(db), None, // Remove the upper bound set by `with_paramspec_attr` - None, // explicit_variance - None, // _default + None, // ParamSpecs cannot have explicit variance + None, // `P.args` and `P.kwargs` cannot have defaults even though `P` can ), self.binding_context(db), None, diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 05d365bc39898..707b4a3e0ce09 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3166,7 +3166,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { return false; }; - if !matches!(callable.kind(self.db), CallableTypeKind::ParamSpecValue) { + if callable.kind(self.db) != CallableTypeKind::ParamSpecValue { return false; } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 93127f7136584..19ed71bbfff0e 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -1700,9 +1700,9 @@ impl<'db> FmtDetailed<'db> for DisplayParameters<'_, 'db> { f.write_str("...")?; } ParametersKind::ParamSpec(typevar) => { - f.write_str(&format!("**{}", typevar.name(self.db)))?; + write!(f, "**{}", typevar.name(self.db))?; if let Some(name) = typevar.binding_context(self.db).name(self.db) { - f.write_str(&format!("@{name}"))?; + write!(f, "@{name}")?; } } } diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 613bf0bd6c3a9..0645cc06c3b34 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -2635,14 +2635,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { )); diagnostic::add_type_expression_reference_link(diag); } - // TODO: Should this be `Unknown` instead? Type::homogeneous_tuple(self.db(), Type::unknown()) } // `*args: P` None => { // The diagnostic for this case is handled in `in_type_expression`. - // TODO: Should this be `Unknown` instead? Type::homogeneous_tuple(self.db(), Type::unknown()) } } @@ -2762,7 +2760,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { diag.set_primary_message(format_args!("Did you mean `{name}.kwargs`?")); diagnostic::add_type_expression_reference_link(diag); } - // TODO: Should this be `Unknown` instead? KnownClass::Dict.to_specialized_instance( self.db(), [KnownClass::Str.to_instance(self.db()), Type::unknown()], @@ -2775,7 +2772,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // `**kwargs: P` None => { // The diagnostic for this case is handled in `in_type_expression`. - // TODO: Should this be `Unknown` instead? KnownClass::Dict.to_specialized_instance( self.db(), [KnownClass::Str.to_instance(self.db()), Type::unknown()], @@ -3419,8 +3415,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_paramspec_default(&mut self, default_expr: &ast::Expr) { match default_expr { ast::Expr::EllipsisLiteral(ellipsis) => { - // TODO: This should use `infer_type_expression` but that uses a `todo` type for - // inferring ellipsis literals in type expressions, which is not what we want here. let ty = self.infer_ellipsis_literal_expression(ellipsis); self.store_expression_type(default_expr, ty); return; @@ -3461,6 +3455,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + /// Infer the type of the expression that represents an explicit specialization of a + /// `ParamSpec` type variable. fn infer_paramspec_explicit_specialization_value( &mut self, expr: &ast::Expr, @@ -3478,7 +3474,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ast::Expr::Tuple(ast::ExprTuple { elts, .. }) | ast::Expr::List(ast::ExprList { elts, .. }) => { - // This should be taken care by the caller. + // This should be taken care of by the caller. if expr.is_tuple_expr() { debug_assert!( exactly_one_paramspec, @@ -3534,7 +3530,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Type::KnownInstance(known_instance) if known_instance.class(self.db()) == KnownClass::ParamSpec => { - // TODO: Raise diagnostic: "ParamSpec "P" is unbound" + // TODO: Emit diagnostic: "ParamSpec "P" is unbound" return Err(()); } @@ -11646,7 +11642,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // Helper to get the AST node corresponding to the type argument at `index`. let get_node = |index: usize| -> ast::AnyNodeRef<'_> { match slice_node { - ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => elts + ast::Expr::Tuple(ast::ExprTuple { elts, .. }) if !exactly_one_paramspec => elts .get(index) .expect("type argument index should not be out of range") .into(), From d81f15d67e9109870891a1d671bd27e86216f5e9 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 12:31:53 +0530 Subject: [PATCH 51/59] Apply type mapping for `PartialSpecialization` as well --- .../src/types/signatures.rs | 143 ++++++++++++------ 1 file changed, 98 insertions(+), 45 deletions(-) diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index f37b402aac206..f7ebed49a4088 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -201,67 +201,120 @@ impl<'db> CallableSignature<'db> { tcx: TypeContext<'db>, visitor: &ApplyTypeMappingVisitor<'db>, ) -> Self { - if let TypeMapping::Specialization(specialization) = type_mapping - && let [self_signature] = self.overloads.as_slice() - && let Some((prefix_parameters, typevar)) = self_signature - .parameters - .find_paramspec_from_args_kwargs(db) - { - let prefix_parameters = prefix_parameters - .iter() - .map(|param| param.apply_type_mapping_impl(db, type_mapping, tcx, visitor)) - .collect::>(); - - let generic_context = self_signature - .generic_context - .map(|context| type_mapping.update_signature_generic_context(db, context)); - - let return_ty = self_signature - .return_ty - .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)); - - match specialization.get(db, typevar) { - Some(Type::TypeVar(typevar)) if typevar.is_paramspec(db) => { - return Self::single(Signature { - generic_context, + fn try_apply_type_mapping_for_paramspec<'db>( + db: &'db dyn Db, + self_signature: &Signature<'db>, + prefix_parameters: &[Parameter<'db>], + paramspec_value: Type<'db>, + type_mapping: &TypeMapping<'_, 'db>, + tcx: TypeContext<'db>, + visitor: &ApplyTypeMappingVisitor<'db>, + ) -> Option> { + match paramspec_value { + Type::TypeVar(typevar) if typevar.is_paramspec(db) => { + Some(CallableSignature::single(Signature { + generic_context: self_signature.generic_context.map(|context| { + type_mapping.update_signature_generic_context(db, context) + }), definition: self_signature.definition, parameters: Parameters::new( db, - prefix_parameters.into_iter().chain([ - Parameter::variadic(Name::new_static("args")).with_annotated_type( - Type::TypeVar( - typevar.with_paramspec_attr(db, ParamSpecAttrKind::Args), - ), - ), - Parameter::keyword_variadic(Name::new_static("kwargs")) - .with_annotated_type(Type::TypeVar( - typevar.with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), - )), - ]), + prefix_parameters + .iter() + .map(|param| { + param.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + }) + .chain([ + Parameter::variadic(Name::new_static("args")) + .with_annotated_type(Type::TypeVar( + typevar + .with_paramspec_attr(db, ParamSpecAttrKind::Args), + )), + Parameter::keyword_variadic(Name::new_static("kwargs")) + .with_annotated_type(Type::TypeVar( + typevar + .with_paramspec_attr(db, ParamSpecAttrKind::Kwargs), + )), + ]), ), - return_ty, - }); + return_ty: self_signature + .return_ty + .map(|ty| ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor)), + })) } - Some(Type::Callable(callable)) + Type::Callable(callable) if matches!(callable.kind(db), CallableTypeKind::ParamSpecValue) => { - return Self::from_overloads(callable.signatures(db).iter().map(|signature| { - Signature { - generic_context, + Some(CallableSignature::from_overloads( + callable.signatures(db).iter().map(|signature| Signature { + generic_context: self_signature.generic_context.map(|context| { + type_mapping.update_signature_generic_context(db, context) + }), definition: signature.definition, parameters: Parameters::new( db, prefix_parameters .iter() - .cloned() + .map(|param| { + param.apply_type_mapping_impl( + db, + type_mapping, + tcx, + visitor, + ) + }) .chain(signature.parameters().iter().cloned()), ), - return_ty, - } - })); + return_ty: self_signature.return_ty.map(|ty| { + ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + }), + }), + )) + } + _ => None, + } + } + + match type_mapping { + TypeMapping::Specialization(specialization) => { + if let [self_signature] = self.overloads.as_slice() + && let Some((prefix_parameters, paramspec)) = self_signature + .parameters + .find_paramspec_from_args_kwargs(db) + && let Some(paramspec_value) = specialization.get(db, paramspec) + && let Some(result) = try_apply_type_mapping_for_paramspec( + db, + self_signature, + prefix_parameters, + paramspec_value, + type_mapping, + tcx, + visitor, + ) + { + return result; } - _ => {} } + TypeMapping::PartialSpecialization(partial) => { + if let [self_signature] = self.overloads.as_slice() + && let Some((prefix_parameters, paramspec)) = self_signature + .parameters + .find_paramspec_from_args_kwargs(db) + && let Some(paramspec_value) = partial.get(db, paramspec) + && let Some(result) = try_apply_type_mapping_for_paramspec( + db, + self_signature, + prefix_parameters, + paramspec_value, + type_mapping, + tcx, + visitor, + ) + { + return result; + } + } + _ => {} } Self::from_overloads( From c1ef796bfca96f1e8b57c25bacae7ddf2290e6b2 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 14:29:58 +0530 Subject: [PATCH 52/59] Expand documentation --- .../ty_python_semantic/src/types/call/bind.rs | 54 +++++++++++++++++-- .../ty_python_semantic/src/types/generics.rs | 10 +++- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 707b4a3e0ce09..775c174ab4453 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2579,7 +2579,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { // use the `P.args` type to perform type checking against the parameter type. This // will allow us to error when `*args: P.args` is matched against, for example, // `n: int` and correctly type check when `*args: P.args` is matched against - // `*args: P.args`. + // `*args: P.args` (another ParamSpec). match union.elements(db) { [paramspec @ Type::TypeVar(typevar), other] | [other, paramspec @ Type::TypeVar(typevar)] @@ -3120,15 +3120,55 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { if let Some((_, paramspec)) = paramspec { // If we reach here, none of the arguments matched the `ParamSpec` parameter, but the - // `ParamSpec` could specialize to a parameter list containing some parameters. + // `ParamSpec` could specialize to a parameter list containing some parameters. For + // example, + // + // ```py + // from typing import Callable + // + // def foo[**P](f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + // + // def f(x: int) -> None: ... + // + // foo(f) + // ``` + // + // Here, no arguments match the `ParamSpec` parameter, but `P` specializes to `(x: int)`, + // so we need to perform a sub-call with no arguments. self.evaluate_paramspec_sub_call(None, paramspec); } } /// Try to evaluate a `ParamSpec` sub-call at the given argument index. /// - /// If the argument at the given index matches a parameter which is a `ParamSpec`, invoke - /// a sub-call starting from that argument index and return `true`. Otherwise, return `false`. + /// The `ParamSpec` parameter is always going to be at the end of the parameter list but there + /// can be other parameter before it. If one of these prepended positional parameters contains + /// a free `ParamSpec`, we consider that variable in scope for the purposes of extracting the + /// components of that `ParamSpec`. For example: + /// + /// ```py + /// from typing import Callable + /// + /// def foo[**P](f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + /// + /// def f(x: int, y: str) -> None: ... + /// + /// foo(f, 1, "hello") # P: (x: int, y: str) + /// ``` + /// + /// Here, `P` specializes to `(x: int, y: str)` when `foo` is called with `f`, which means that + /// the parameters of `f` become a part of `foo`'s parameter list replacing the `ParamSpec` + /// parameter which is: + /// + /// ```py + /// def foo(f: Callable[[x: int, y: str], None], x: int, y: str) -> None: ... + /// ``` + /// + /// This method will check whether the parameter matching the argument at `argument_index` is + /// annotated with the components of `ParamSpec`, and if so, will invoke a sub-call considering + /// the arguments starting from `argument_index` against the specialized parameter list. + /// + /// Returns `true` if the sub-call was invoked, `false` otherwise. fn try_paramspec_evaluation_at( &mut self, argument_index: usize, @@ -3153,7 +3193,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { /// The remaining arguments start from `argument_index` if provided, otherwise no arguments /// are passed. /// - /// Returns `false` if the specialization does not contain a mapping for the given `paramspec`. + /// This method returns `false` if the specialization does not contain a mapping for the given + /// `paramspec`, contains an invalid mapping (i.e., not a `Callable` of kind `ParamSpecValue`) + /// or if the value is an overloaded callable. + /// + /// For more details, refer to [`Self::try_paramspec_evaluation_at`]. fn evaluate_paramspec_sub_call( &mut self, argument_index: Option, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index c051492d0b16a..23feb93905a2e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -319,7 +319,15 @@ impl<'db> GenericContext<'db> { self.variables_inner(db).values().copied() } - /// Returns `true` if this generic context contains exactly one `ParamSpec` type variable. + /// Returns `true` if this generic context contains exactly one `ParamSpec` and no other type + /// variables. + /// + /// For example: + /// ```py + /// class Foo[**P]: ... # true + /// class Bar[T, **P]: ... # false + /// class Baz[T]: ... # false + /// ``` pub(crate) fn exactly_one_paramspec(self, db: &'db dyn Db) -> bool { self.variables(db) .exactly_one() From 423fda20276385a6e5cbd81b47f1719a53624820 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 16:38:20 +0530 Subject: [PATCH 53/59] Add regression test for the cycle --- .../mdtest/generics/pep695/paramspec.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 8a6101a42476a..6f8ce326800a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -365,6 +365,29 @@ f2(1) f2("a", "b") ``` +The `converter` function act as a decorator here: + +```py +@converter +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(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"]`" +f3("a", "b") +``` + ### Return type change using the same `ParamSpec` multiple times ```py From 9a578a388e03c0c7b6f63137376cfdb375adbe3c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 20:13:07 +0530 Subject: [PATCH 54/59] Add explanation about allowing `Any` to specialize `ParamSpec` --- .../resources/mdtest/generics/legacy/paramspec.md | 8 +++++++- .../resources/mdtest/generics/pep695/paramspec.md | 8 +++++++- crates/ty_python_semantic/src/types/infer/builder.rs | 11 +++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) 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 93072cf3699da..c2ce11a21ffa8 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -272,7 +272,6 @@ But, they cannot be omitted when there are multiple type variables. reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int -reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int # TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int` # TODO: error: paramspec is unbound @@ -293,6 +292,13 @@ reveal_type(p.attr2) # revealed: (int, /) -> None TwoParamSpec[int, str] ``` +Specializing `ParamSpec` type variable using `typing.Any` isn't explicitly allowed by the spec but +both mypy and Pyright allow this and there are usages of this in the wild e.g., `staticmethod[Any, Any]`. + +```py +reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int +``` + ## Specialization when defaults are involved ```toml 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 6f8ce326800a2..ec39e4e8d833c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -246,7 +246,6 @@ But, they cannot be omitted when there are multiple type variables. reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int -reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int # TODO: error: paramspec is unbound reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown @@ -266,6 +265,13 @@ reveal_type(p.attr2) # revealed: (int, /) -> None TwoParamSpec[int, str] ``` +Specializing `ParamSpec` type variable using `typing.Any` isn't explicitly allowed by the spec but +both mypy and Pyright allow this and there are usages of this in the wild e.g., `staticmethod[Any, Any]`. + +```py +reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int +``` + ## Specialization when defaults are involved ```py diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 0645cc06c3b34..97aa2b2dcf619 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3581,6 +3581,17 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { return Ok(Type::paramspec_value_callable(db, parameters)); } + // This is specifically to handle a case where there are more than one type + // variables and at least one of them is a `ParamSpec` which is specialized + // using `typing.Any`. This isn't explicitly allowed in the spec, but both mypy + // and Pyright allows this and the ecosystem report suggested there are usages + // of this in the wild e.g., `staticmethod[Any, Any]`. For example, + // + // ```python + // class Foo[**P, T]: ... + // + // Foo[Any, int] # P: (Any, /), T: int + // ``` Type::Dynamic(DynamicType::Any) => { return Ok(Type::paramspec_value_callable( db, From a48ac5e95b12dfb67b4d77a772517af4cdaaf662 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 20:24:30 +0530 Subject: [PATCH 55/59] Add test cases around instance attributes and `Final` --- .../mdtest/generics/pep695/paramspec.md | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 ec39e4e8d833c..ab9c1fd9636d9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -146,6 +146,18 @@ class Foo3[**P]: ) -> None: ... ``` +It isn't allowed to annotate an instance attribute either: + +```py +class Foo4[**P]: + def __init__(self, fn: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> None: + self.fn = fn + # TODO: error + self.args: P.args = args + # TODO: error + self.kwargs: P.kwargs = kwargs +``` + ## Semantics of `P.args` and `P.kwargs` The type of `args` and `kwargs` inside the function is `P.args` and `P.kwargs` respectively instead @@ -552,6 +564,22 @@ def baz[**P](fn: Callable[P, None], foo: Foo[P]) -> None: fn(*foo.args, **foo.kwargs) ``` +The `Unknown` can be eliminated by using annotating these attributes with `Final`: + +```py +from typing import Final + +class FooWithFinal[**P]: + def __init__(self, *args: P.args, **kwargs: P.kwargs) -> None: + self.args: Final = args + self.kwargs: Final = kwargs + +def with_final[**P](foo: FooWithFinal[P]) -> None: + reveal_type(foo) # revealed: FooWithFinal[P@with_final] + reveal_type(foo.args) # revealed: P@with_final.args + reveal_type(foo.kwargs) # revealed: P@with_final.kwargs +``` + ### Specializing `Self` when `ParamSpec` is involved ```py From 0dc24928c57a2730f8e53d10cb3912c667ee3066 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 20:35:19 +0530 Subject: [PATCH 56/59] Use `assert!` --- crates/ty_python_semantic/src/types/infer/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 97aa2b2dcf619..6ddaea40a4ad5 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -3476,7 +3476,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | ast::Expr::List(ast::ExprList { elts, .. }) => { // This should be taken care of by the caller. if expr.is_tuple_expr() { - debug_assert!( + assert!( exactly_one_paramspec, "Inferring ParamSpec value during explicit specialization for a \ tuple expression should only happen when it contains exactly one ParamSpec" From 72130206313e442469dfc7e997fec1fc1aecbe45 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 20:35:31 +0530 Subject: [PATCH 57/59] Fix TODO comment --- crates/ty_python_semantic/src/types/generics.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 4431498d469d2..7db5f7e7a2d71 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -1441,7 +1441,12 @@ impl<'db> SpecializationBuilder<'db> { match self.types.entry(identity) { Entry::Occupied(mut entry) => { - // TODO: mypy and Pyright does this, should we keep doing it? + // TODO: The spec says that when a ParamSpec is used multiple times in a signature, + // the type checker can solve it to a common behavioral supertype. We don't + // implement that yet so in case there are multiple ParamSpecs, use the + // specialization from the first occurrence. + // https://github.com/astral-sh/ty/issues/1778 + // https://github.com/astral-sh/ruff/pull/21445#discussion_r2591510145 if bound_typevar.is_paramspec(self.db) { return; } From 18167b1b4d81a3b19db22e786ca404de2d968162 Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 20:42:50 +0530 Subject: [PATCH 58/59] Run pre-commit --- .../resources/mdtest/generics/legacy/paramspec.md | 3 ++- .../resources/mdtest/generics/pep695/paramspec.md | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 c2ce11a21ffa8..c4764c38865fd 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/paramspec.md @@ -293,7 +293,8 @@ TwoParamSpec[int, str] ``` Specializing `ParamSpec` type variable using `typing.Any` isn't explicitly allowed by the spec but -both mypy and Pyright allow this and there are usages of this in the wild e.g., `staticmethod[Any, Any]`. +both mypy and Pyright allow this and there are usages of this in the wild e.g., +`staticmethod[Any, Any]`. ```py reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int 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 ab9c1fd9636d9..6483428bb3f96 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -278,7 +278,8 @@ TwoParamSpec[int, str] ``` Specializing `ParamSpec` type variable using `typing.Any` isn't explicitly allowed by the spec but -both mypy and Pyright allow this and there are usages of this in the wild e.g., `staticmethod[Any, Any]`. +both mypy and Pyright allow this and there are usages of this in the wild e.g., +`staticmethod[Any, Any]`. ```py reveal_type(TypeVarAndParamSpec[int, Any]().attr) # revealed: (...) -> int From e5a4f5962d4eae1b8d3f8512e075dd44ef27115a Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Fri, 5 Dec 2025 21:53:16 +0530 Subject: [PATCH 59/59] Add TODO about variance --- crates/ty_ide/src/hover.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 669c5014d53b5..8f9add508a399 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -2290,6 +2290,7 @@ def function(): "#, ); + // TODO: Should this be constravariant instead? assert_snapshot!(test.hover(), @r" P@Alias (bivariant) ---------------------------------------------