@@ -310,6 +310,15 @@ def not_declared_in_type_params(self, tvar_name: str) -> bool:
310310
311311 def visit_unbound_type_nonoptional (self , t : UnboundType , defining_literal : bool ) -> Type :
312312 sym = self .lookup_qualified (t .name , t )
313+ param_spec_name = None
314+ if t .name .endswith ((".args" , ".kwargs" )):
315+ param_spec_name = t .name .rsplit ("." , 1 )[0 ]
316+ maybe_param_spec = self .lookup_qualified (param_spec_name , t )
317+ if maybe_param_spec and isinstance (maybe_param_spec .node , ParamSpecExpr ):
318+ sym = maybe_param_spec
319+ else :
320+ param_spec_name = None
321+
313322 if sym is not None :
314323 node = sym .node
315324 if isinstance (node , PlaceholderNode ):
@@ -362,17 +371,23 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
362371 if tvar_def is None :
363372 if self .allow_unbound_tvars :
364373 return t
374+ name = param_spec_name or t .name
365375 if self .defining_alias and self .not_declared_in_type_params (t .name ):
366- msg = f'ParamSpec "{ t . name } " is not included in type_params'
376+ msg = f'ParamSpec "{ name } " is not included in type_params'
367377 else :
368- msg = f'ParamSpec "{ t . name } " is unbound'
378+ msg = f'ParamSpec "{ name } " is unbound'
369379 self .fail (msg , t , code = codes .VALID_TYPE )
370380 return AnyType (TypeOfAny .from_error )
371381 assert isinstance (tvar_def , ParamSpecType )
372382 if len (t .args ) > 0 :
373383 self .fail (
374384 f'ParamSpec "{ t .name } " used with arguments' , t , code = codes .VALID_TYPE
375385 )
386+ if param_spec_name is not None and not self .allow_param_spec_literals :
387+ self .fail (
388+ "ParamSpec components are not allowed here" , t , code = codes .VALID_TYPE
389+ )
390+ return AnyType (TypeOfAny .from_error )
376391 # Change the line number
377392 return ParamSpecType (
378393 tvar_def .name ,
@@ -1113,46 +1128,57 @@ def visit_callable_type(
11131128 variables , _ = self .bind_function_type_variables (t , t )
11141129 type_guard = self .anal_type_guard (t .ret_type )
11151130 type_is = self .anal_type_is (t .ret_type )
1131+
11161132 arg_kinds = t .arg_kinds
1117- if len (arg_kinds ) >= 2 and arg_kinds [- 2 ] == ARG_STAR and arg_kinds [- 1 ] == ARG_STAR2 :
1118- arg_types = self .anal_array (t .arg_types [:- 2 ], nested = nested ) + [
1119- self .anal_star_arg_type (t .arg_types [- 2 ], ARG_STAR , nested = nested ),
1120- self .anal_star_arg_type (t .arg_types [- 1 ], ARG_STAR2 , nested = nested ),
1121- ]
1122- # If nested is True, it means we are analyzing a Callable[...] type, rather
1123- # than a function definition type. We need to "unpack" ** TypedDict annotation
1124- # here (for function definitions it is done in semanal).
1125- if nested and isinstance (arg_types [- 1 ], UnpackType ):
1133+ arg_types = []
1134+ param_spec_with_args = param_spec_with_kwargs = None
1135+ param_spec_invalid = False
1136+ for kind , ut in zip (arg_kinds , t .arg_types ):
1137+ if kind == ARG_STAR :
1138+ param_spec_with_args , at = self .anal_star_arg_type (ut , kind , nested = nested )
1139+ elif kind == ARG_STAR2 :
1140+ param_spec_with_kwargs , at = self .anal_star_arg_type (ut , kind , nested = nested )
1141+ else :
1142+ if param_spec_with_args :
1143+ param_spec_invalid = True
1144+ self .fail (
1145+ "Arguments not allowed after ParamSpec.args" , t , code = codes .VALID_TYPE
1146+ )
1147+ at = self .anal_type (ut , nested = nested , allow_unpack = False )
1148+ arg_types .append (at )
1149+
1150+ if nested and arg_types :
1151+ # If we've got a Callable[[Unpack[SomeTypedDict]], None], make sure
1152+ # Unpack is interpreted as `**` and not as `*`.
1153+ last = arg_types [- 1 ]
1154+ if isinstance (last , UnpackType ):
11261155 # TODO: it would be better to avoid this get_proper_type() call.
1127- unpacked = get_proper_type (arg_types [- 1 ].type )
1128- if isinstance (unpacked , TypedDictType ):
1129- arg_types [- 1 ] = unpacked
1156+ p_at = get_proper_type (last .type )
1157+ if isinstance (p_at , TypedDictType ) and not last .from_star_syntax :
1158+ # Automatically detect Unpack[Foo] in Callable as backwards
1159+ # compatible syntax for **Foo, if Foo is a TypedDict.
1160+ arg_kinds [- 1 ] = ARG_STAR2
1161+ arg_types [- 1 ] = p_at
11301162 unpacked_kwargs = True
1131- arg_types = self .check_unpacks_in_list (arg_types )
1132- else :
1133- star_index = None
1163+ arg_types = self .check_unpacks_in_list (arg_types )
1164+
1165+ if not param_spec_invalid and param_spec_with_args != param_spec_with_kwargs :
1166+ # If already invalid, do not report more errors - definition has
1167+ # to be fixed anyway
1168+ name = param_spec_with_args or param_spec_with_kwargs
1169+ self .fail (
1170+ f'ParamSpec must have "*args" typed as "{ name } .args" and "**kwargs" typed as "{ name } .kwargs"' ,
1171+ t ,
1172+ code = codes .VALID_TYPE ,
1173+ )
1174+ param_spec_invalid = True
1175+
1176+ if param_spec_invalid :
11341177 if ARG_STAR in arg_kinds :
1135- star_index = arg_kinds .index (ARG_STAR )
1136- star2_index = None
1178+ arg_types [arg_kinds .index (ARG_STAR )] = AnyType (TypeOfAny .from_error )
11371179 if ARG_STAR2 in arg_kinds :
1138- star2_index = arg_kinds .index (ARG_STAR2 )
1139- arg_types = []
1140- for i , ut in enumerate (t .arg_types ):
1141- at = self .anal_type (
1142- ut , nested = nested , allow_unpack = i in (star_index , star2_index )
1143- )
1144- if nested and isinstance (at , UnpackType ) and i == star_index :
1145- # TODO: it would be better to avoid this get_proper_type() call.
1146- p_at = get_proper_type (at .type )
1147- if isinstance (p_at , TypedDictType ) and not at .from_star_syntax :
1148- # Automatically detect Unpack[Foo] in Callable as backwards
1149- # compatible syntax for **Foo, if Foo is a TypedDict.
1150- at = p_at
1151- arg_kinds [i ] = ARG_STAR2
1152- unpacked_kwargs = True
1153- arg_types .append (at )
1154- if nested :
1155- arg_types = self .check_unpacks_in_list (arg_types )
1180+ arg_types [arg_kinds .index (ARG_STAR2 )] = AnyType (TypeOfAny .from_error )
1181+
11561182 # If there were multiple (invalid) unpacks, the arg types list will become shorter,
11571183 # we need to trim the kinds/names as well to avoid crashes.
11581184 arg_kinds = t .arg_kinds [: len (arg_types )]
@@ -1207,7 +1233,7 @@ def anal_type_is_arg(self, t: UnboundType, fullname: str) -> Type | None:
12071233 return self .anal_type (t .args [0 ])
12081234 return None
12091235
1210- def anal_star_arg_type (self , t : Type , kind : ArgKind , nested : bool ) -> Type :
1236+ def anal_star_arg_type (self , t : Type , kind : ArgKind , nested : bool ) -> tuple [ str | None , Type ] :
12111237 """Analyze signature argument type for *args and **kwargs argument."""
12121238 if isinstance (t , UnboundType ) and t .name and "." in t .name and not t .args :
12131239 components = t .name .split ("." )
@@ -1234,15 +1260,15 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type:
12341260 )
12351261 else :
12361262 assert False , kind
1237- return make_paramspec (
1263+ return tvar_name , make_paramspec (
12381264 tvar_def .name ,
12391265 tvar_def .fullname ,
12401266 tvar_def .id ,
12411267 named_type_func = self .named_type ,
12421268 line = t .line ,
12431269 column = t .column ,
12441270 )
1245- return self .anal_type (t , nested = nested , allow_unpack = True )
1271+ return None , self .anal_type (t , nested = nested , allow_unpack = True )
12461272
12471273 def visit_overloaded (self , t : Overloaded ) -> Type :
12481274 # Overloaded types are manually constructed in semanal.py by analyzing the
@@ -2586,18 +2612,7 @@ def _seems_like_callable(self, type: UnboundType) -> bool:
25862612
25872613 def visit_unbound_type (self , t : UnboundType ) -> None :
25882614 name = t .name
2589- node = None
2590-
2591- # Special case P.args and P.kwargs for ParamSpecs only.
2592- if name .endswith ("args" ):
2593- if name .endswith ((".args" , ".kwargs" )):
2594- base = "." .join (name .split ("." )[:- 1 ])
2595- n = self .api .lookup_qualified (base , t )
2596- if n is not None and isinstance (n .node , ParamSpecExpr ):
2597- node = n
2598- name = base
2599- if node is None :
2600- node = self .api .lookup_qualified (name , t )
2615+ node = self .api .lookup_qualified (name , t )
26012616 if node and node .fullname in SELF_TYPE_NAMES :
26022617 self .has_self_type = True
26032618 if (
0 commit comments