From bc829f8b995cc15c3831ea4479eda49d388b6188 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 12:18:17 +0300 Subject: [PATCH 1/9] Warn on invalid `*args` and `**kwargs` with `ParamSpec` --- mypy/semanal.py | 23 +++++++++++++++++++ mypy/typeanal.py | 1 + .../unit/check-parameter-specification.test | 21 +++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index 9fca74b71872..ffd7da7c5cfd 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -843,6 +843,7 @@ def analyze_func_def(self, defn: FuncDef) -> None: defn.type = result self.add_type_alias_deps(analyzer.aliases_used) self.check_function_signature(defn) + self.check_paramspec_definition(defn.type) if isinstance(defn, FuncDef): assert isinstance(defn.type, CallableType) defn.type = set_callable_name(defn.type, defn) @@ -1282,6 +1283,28 @@ def check_function_signature(self, fdef: FuncItem) -> None: elif len(sig.arg_types) > len(fdef.arguments): self.fail("Type signature has too many arguments", fdef, blocker=True) + def check_paramspec_definition(self, func: CallableType) -> None: + param_spec_var = next( + (var for var in func.variables if isinstance(var, ParamSpecType)), None + ) + if param_spec_var: + args = func.var_arg() + kwargs = func.kw_arg() + if args is None and kwargs is None: + return # Looks like this function does not have starred args + + args_type = args.typ if args is not None else None + kwargs_type = kwargs.typ if kwargs is not None else None + + if not isinstance(args_type, ParamSpecType) or not isinstance( + kwargs_type, ParamSpecType + ): + self.fail( + f'ParamSpec must have "*args" typed as "{param_spec_var.name}.args" and "**kwargs" typed as "{param_spec_var.name}.kwargs"', + func, + code=codes.VALID_TYPE, + ) + def visit_decorator(self, dec: Decorator) -> None: self.statement = dec # TODO: better don't modify them at all. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index fa928c439bfa..ff777f4608c5 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -863,6 +863,7 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: ] else: arg_types = self.anal_array(t.arg_types, nested=nested) + ret = t.copy_modified( arg_types=arg_types, ret_type=self.anal_type(t.ret_type, nested=nested), diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index f91995379b61..f01b73e14b2f 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1166,3 +1166,24 @@ def func3(callback: Callable[P1, str]) -> Callable[P1, str]: return "foo" return inner [builtins fixtures/paramspec.pyi] + + +[case testInvalidDefinitions] +from typing import Callable, ParamSpec + +P = ParamSpec('P') + +def c1(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: ... +def c2(f: Callable[P, int]) -> int: ... + +def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f2(f: Callable[P, int], *args: P.args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f3(f: Callable[P, int], *args: P.args) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f4(f: Callable[P, int], **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f5(f: Callable[P, int], *args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" + +# Error message test: +P1 = ParamSpec('P1') + +def m1(f: Callable[P1, int], *a, **k: P1.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" +[builtins fixtures/paramspec.pyi] From 1430c350df237dc1b88b062db70d79dec76105a4 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 12:19:18 +0300 Subject: [PATCH 2/9] Unrelated change --- mypy/typeanal.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index ff777f4608c5..fa928c439bfa 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -863,7 +863,6 @@ def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: ] else: arg_types = self.anal_array(t.arg_types, nested=nested) - ret = t.copy_modified( arg_types=arg_types, ret_type=self.anal_type(t.ret_type, nested=nested), From 7779d27ca5e1844928d8dec32c143ddf11765459 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 12:35:14 +0300 Subject: [PATCH 3/9] Fix CI --- mypy/semanal.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index ffd7da7c5cfd..e7f99bf57aa9 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1283,7 +1283,8 @@ def check_function_signature(self, fdef: FuncItem) -> None: elif len(sig.arg_types) > len(fdef.arguments): self.fail("Type signature has too many arguments", fdef, blocker=True) - def check_paramspec_definition(self, func: CallableType) -> None: + def check_paramspec_definition(self, func: Type) -> None: + assert isinstance(func, CallableType) param_spec_var = next( (var for var in func.variables if isinstance(var, ParamSpecType)), None ) From 65dfc86f00e0405afa74eb7b23781fe923e2f265 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 13:00:51 +0300 Subject: [PATCH 4/9] Fix CI --- mypy/semanal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index e7f99bf57aa9..f833e1aafce4 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1283,7 +1283,7 @@ def check_function_signature(self, fdef: FuncItem) -> None: elif len(sig.arg_types) > len(fdef.arguments): self.fail("Type signature has too many arguments", fdef, blocker=True) - def check_paramspec_definition(self, func: Type) -> None: + def check_paramspec_definition(self, func: ProperType) -> None: assert isinstance(func, CallableType) param_spec_var = next( (var for var in func.variables if isinstance(var, ParamSpecType)), None From c9218444d814cc8955eeb3aeeeff03ecc85aeecf Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 14:30:44 +0300 Subject: [PATCH 5/9] Fix CI --- mypy/semanal.py | 47 ++++++++++++++- .../unit/check-parameter-specification.test | 57 ++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index f833e1aafce4..7d9b6531933a 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -69,6 +69,8 @@ from mypy.nodes import ( ARG_NAMED, ARG_POS, + ARG_STAR, + ARG_STAR2, CONTRAVARIANT, COVARIANT, GDEF, @@ -843,7 +845,7 @@ def analyze_func_def(self, defn: FuncDef) -> None: defn.type = result self.add_type_alias_deps(analyzer.aliases_used) self.check_function_signature(defn) - self.check_paramspec_definition(defn.type) + self.check_paramspec_definition(defn) if isinstance(defn, FuncDef): assert isinstance(defn.type, CallableType) defn.type = set_callable_name(defn.type, defn) @@ -1283,8 +1285,18 @@ def check_function_signature(self, fdef: FuncItem) -> None: elif len(sig.arg_types) > len(fdef.arguments): self.fail("Type signature has too many arguments", fdef, blocker=True) - def check_paramspec_definition(self, func: ProperType) -> None: + def check_paramspec_definition(self, defn: FuncDef) -> None: + func = defn.type assert isinstance(func, CallableType) + has_paramspec_callable = any( + arg.from_concatenate + or (arg.arg_types and isinstance(get_proper_type(arg.arg_types[0]), ParamSpecType)) + for arg in get_proper_types(func.arg_types) + if isinstance(arg, CallableType) + ) + if not has_paramspec_callable: + return # Callable[ParamSpec, ...] was not found + param_spec_var = next( (var for var in func.variables if isinstance(var, ParamSpecType)), None ) @@ -1297,6 +1309,37 @@ def check_paramspec_definition(self, func: ProperType) -> None: args_type = args.typ if args is not None else None kwargs_type = kwargs.typ if kwargs is not None else None + args_defn = next( + ( + arg_def + for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) + if arg_kind == ARG_STAR + ), + None, + ) + kwargs_defn = next( + ( + arg_def + for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) + if arg_kind == ARG_STAR2 + ), + None, + ) + + args_defn_type = args_defn.type_annotation if args_defn is not None else None + kwargs_defn_type = kwargs_defn.type_annotation if kwargs_defn is not None else None + + # This may happen on invalid `ParamSpec` args / kwargs definition: + if not ( + isinstance(args_defn_type, UnboundType) + and args_defn_type.name.endswith(".args") + or isinstance(kwargs_defn_type, UnboundType) + and kwargs_defn_type.name.endswith(".kwargs") + ): + # Looks like both `*args` and `**kwargs` are not `ParamSpec` + # It might be something else, skipping. + return + if not isinstance(args_type, ParamSpecType) or not isinstance( kwargs_type, ParamSpecType ): diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index f01b73e14b2f..a14be59d4a67 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1168,22 +1168,75 @@ def func3(callback: Callable[P1, str]) -> Callable[P1, str]: [builtins fixtures/paramspec.pyi] -[case testInvalidDefinitions] +[case testInvalidParamSpecDefinitionsWithArgsKwargs] from typing import Callable, ParamSpec P = ParamSpec('P') def c1(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: ... def c2(f: Callable[P, int]) -> int: ... +def c3(f: Callable[P, int], *args, **kwargs) -> int: ... + +# It is ok to define, +def c4(f: Callable[P, int], *args: int, **kwargs: str) -> int: + # but not ok to call: + f(*args, **kwargs) # E: Argument 1 has incompatible type "*Tuple[int, ...]"; expected "P.args" \ + # E: Argument 2 has incompatible type "**Dict[str, str]"; expected "P.kwargs" + return 1 def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" def f2(f: Callable[P, int], *args: P.args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" def f3(f: Callable[P, int], *args: P.args) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" def f4(f: Callable[P, int], **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" -def f5(f: Callable[P, int], *args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" # Error message test: P1 = ParamSpec('P1') def m1(f: Callable[P1, int], *a, **k: P1.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" [builtins fixtures/paramspec.pyi] + + +[case testInvalidParamSpecAndConcatenateDefinitionsWithArgsKwargs] +from typing import Callable, ParamSpec +from typing_extensions import Concatenate + +P = ParamSpec('P') + +def c1(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs: P.kwargs) -> int: ... +def c2(f: Callable[Concatenate[int, P], int]) -> int: ... +def c3(f: Callable[Concatenate[int, P], int], *args, **kwargs) -> int: ... + +# It is ok to define, +def c4(f: Callable[Concatenate[int, P], int], *args: int, **kwargs: str) -> int: + # but not ok to call: + f(1, *args, **kwargs) # E: Argument 2 has incompatible type "*Tuple[int, ...]"; expected "P.args" \ + # E: Argument 3 has incompatible type "**Dict[str, str]"; expected "P.kwargs" + return 1 + +def f1(f: Callable[Concatenate[int, P], int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f2(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f3(f: Callable[Concatenate[int, P], int], *args: P.args) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f4(f: Callable[Concatenate[int, P], int], **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +[builtins fixtures/paramspec.pyi] + + +[case testValidParamSpecInsideGenericWithoutArgsAndKwargs] +from typing import Callable, ParamSpec, Generic +from typing_extensions import Concatenate + +P = ParamSpec('P') + +class Some(Generic[P]): ... + +def create(s: Some[P], *args: int): ... +def update(s: Some[P], **kwargs: int): ... +def delete(s: Some[P]): ... + +def from_callable1(c: Callable[P, int], *args: int, **kwargs: int) -> Some[P]: ... +def from_callable2(c: Callable[P, int], **kwargs: int) -> Some[P]: ... +def from_callable3(c: Callable[P, int], *args: int) -> Some[P]: ... + +def from_extra1(c: Callable[Concatenate[int, P], int], *args: int, **kwargs: int) -> Some[P]: ... +def from_extra2(c: Callable[Concatenate[int, P], int], **kwargs: int) -> Some[P]: ... +def from_extra3(c: Callable[Concatenate[int, P], int], *args: int) -> Some[P]: ... +[builtins fixtures/paramspec.pyi] From 94ad912801ec948994b160b0282e01b7f55acc83 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 14:37:28 +0300 Subject: [PATCH 6/9] Simplify new check --- mypy/semanal.py | 97 +++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 7d9b6531933a..32fa7f8b59a6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1288,6 +1288,18 @@ def check_function_signature(self, fdef: FuncItem) -> None: def check_paramspec_definition(self, defn: FuncDef) -> None: func = defn.type assert isinstance(func, CallableType) + + param_spec_var = next( + (var for var in func.variables if isinstance(var, ParamSpecType)), None + ) + if param_spec_var is None: + return + + args = func.var_arg() + kwargs = func.kw_arg() + if args is None and kwargs is None: + return # Looks like this function does not have starred args + has_paramspec_callable = any( arg.from_concatenate or (arg.arg_types and isinstance(get_proper_type(arg.arg_types[0]), ParamSpecType)) @@ -1297,57 +1309,48 @@ def check_paramspec_definition(self, defn: FuncDef) -> None: if not has_paramspec_callable: return # Callable[ParamSpec, ...] was not found - param_spec_var = next( - (var for var in func.variables if isinstance(var, ParamSpecType)), None + args_type = args.typ if args is not None else None + kwargs_type = kwargs.typ if kwargs is not None else None + + args_defn = next( + ( + arg_def + for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) + if arg_kind == ARG_STAR + ), + None, + ) + kwargs_defn = next( + ( + arg_def + for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) + if arg_kind == ARG_STAR2 + ), + None, ) - if param_spec_var: - args = func.var_arg() - kwargs = func.kw_arg() - if args is None and kwargs is None: - return # Looks like this function does not have starred args - - args_type = args.typ if args is not None else None - kwargs_type = kwargs.typ if kwargs is not None else None - - args_defn = next( - ( - arg_def - for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) - if arg_kind == ARG_STAR - ), - None, - ) - kwargs_defn = next( - ( - arg_def - for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) - if arg_kind == ARG_STAR2 - ), - None, - ) - args_defn_type = args_defn.type_annotation if args_defn is not None else None - kwargs_defn_type = kwargs_defn.type_annotation if kwargs_defn is not None else None + args_defn_type = args_defn.type_annotation if args_defn is not None else None + kwargs_defn_type = kwargs_defn.type_annotation if kwargs_defn is not None else None - # This may happen on invalid `ParamSpec` args / kwargs definition: - if not ( - isinstance(args_defn_type, UnboundType) - and args_defn_type.name.endswith(".args") - or isinstance(kwargs_defn_type, UnboundType) - and kwargs_defn_type.name.endswith(".kwargs") - ): - # Looks like both `*args` and `**kwargs` are not `ParamSpec` - # It might be something else, skipping. - return + # This may happen on invalid `ParamSpec` args / kwargs definition: + if not ( + isinstance(args_defn_type, UnboundType) + and args_defn_type.name.endswith(".args") + or isinstance(kwargs_defn_type, UnboundType) + and kwargs_defn_type.name.endswith(".kwargs") + ): + # Looks like both `*args` and `**kwargs` are not `ParamSpec` + # It might be something else, skipping. + return - if not isinstance(args_type, ParamSpecType) or not isinstance( - kwargs_type, ParamSpecType - ): - self.fail( - f'ParamSpec must have "*args" typed as "{param_spec_var.name}.args" and "**kwargs" typed as "{param_spec_var.name}.kwargs"', - func, - code=codes.VALID_TYPE, - ) + if not isinstance(args_type, ParamSpecType) or not isinstance( + kwargs_type, ParamSpecType + ): + self.fail( + f'ParamSpec must have "*args" typed as "{param_spec_var.name}.args" and "**kwargs" typed as "{param_spec_var.name}.kwargs"', + func, + code=codes.VALID_TYPE, + ) def visit_decorator(self, dec: Decorator) -> None: self.statement = dec From d25447a78a8a2281103b966eee347dd632ee9d6d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Oct 2022 14:37:48 +0300 Subject: [PATCH 7/9] Format --- mypy/semanal.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 32fa7f8b59a6..5a7c30a9cfdb 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1343,9 +1343,7 @@ def check_paramspec_definition(self, defn: FuncDef) -> None: # It might be something else, skipping. return - if not isinstance(args_type, ParamSpecType) or not isinstance( - kwargs_type, ParamSpecType - ): + if not isinstance(args_type, ParamSpecType) or not isinstance(kwargs_type, ParamSpecType): self.fail( f'ParamSpec must have "*args" typed as "{param_spec_var.name}.args" and "**kwargs" typed as "{param_spec_var.name}.kwargs"', func, From 7540268fb8f09ec1c89b7b325b58415343de2fca Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 15 Oct 2022 15:46:17 +0300 Subject: [PATCH 8/9] Address review --- mypy/semanal.py | 79 ++++++++----------- .../unit/check-parameter-specification.test | 39 +++++++++ 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 5a7c30a9cfdb..4ec4938bdacb 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1289,63 +1289,54 @@ def check_paramspec_definition(self, defn: FuncDef) -> None: func = defn.type assert isinstance(func, CallableType) - param_spec_var = next( - (var for var in func.variables if isinstance(var, ParamSpecType)), None - ) - if param_spec_var is None: - return + if not any(isinstance(var, ParamSpecType) for var in func.variables): + return # Function does not have param spec variables args = func.var_arg() kwargs = func.kw_arg() if args is None and kwargs is None: return # Looks like this function does not have starred args - has_paramspec_callable = any( - arg.from_concatenate - or (arg.arg_types and isinstance(get_proper_type(arg.arg_types[0]), ParamSpecType)) - for arg in get_proper_types(func.arg_types) - if isinstance(arg, CallableType) - ) - if not has_paramspec_callable: - return # Callable[ParamSpec, ...] was not found - - args_type = args.typ if args is not None else None - kwargs_type = kwargs.typ if kwargs is not None else None - - args_defn = next( - ( - arg_def - for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) - if arg_kind == ARG_STAR - ), - None, - ) - kwargs_defn = next( - ( - arg_def - for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds) - if arg_kind == ARG_STAR2 - ), - None, - ) - - args_defn_type = args_defn.type_annotation if args_defn is not None else None - kwargs_defn_type = kwargs_defn.type_annotation if kwargs_defn is not None else None - - # This may happen on invalid `ParamSpec` args / kwargs definition: + args_defn_type = None + kwargs_defn_type = None + for arg_def, arg_kind in zip(defn.arguments, defn.arg_kinds): + if arg_kind == ARG_STAR: + args_defn_type = arg_def.type_annotation + elif arg_kind == ARG_STAR2: + kwargs_defn_type = arg_def.type_annotation + + # This may happen on invalid `ParamSpec` args / kwargs definition, + # type analyzer sets types of arguments to `Any`, but keeps + # definition types as `UnboundType` for now. if not ( - isinstance(args_defn_type, UnboundType) - and args_defn_type.name.endswith(".args") - or isinstance(kwargs_defn_type, UnboundType) - and kwargs_defn_type.name.endswith(".kwargs") + (isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args")) + or ( + isinstance(kwargs_defn_type, UnboundType) + and kwargs_defn_type.name.endswith(".kwargs") + ) ): # Looks like both `*args` and `**kwargs` are not `ParamSpec` # It might be something else, skipping. return - if not isinstance(args_type, ParamSpecType) or not isinstance(kwargs_type, ParamSpecType): + args_type = args.typ if args is not None else None + kwargs_type = kwargs.typ if kwargs is not None else None + + if ( + not isinstance(args_type, ParamSpecType) + or not isinstance(kwargs_type, ParamSpecType) + or args_type.name != kwargs_type.name + ): + if args_defn_type is not None and args_defn_type.name.endswith(".args"): + param_name = args_defn_type.name.split(".")[0] + elif kwargs_defn_type is not None and kwargs_defn_type.name.endswith(".kwargs"): + param_name = kwargs_defn_type.name.split(".")[0] + else: + # Fallback for cases that probably should not ever happen: + param_name = "P" + self.fail( - f'ParamSpec must have "*args" typed as "{param_spec_var.name}.args" and "**kwargs" typed as "{param_spec_var.name}.kwargs"', + f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_name}.kwargs"', func, code=codes.VALID_TYPE, ) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index a14be59d4a67..f8face26c71b 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -1240,3 +1240,42 @@ def from_extra1(c: Callable[Concatenate[int, P], int], *args: int, **kwargs: int def from_extra2(c: Callable[Concatenate[int, P], int], **kwargs: int) -> Some[P]: ... def from_extra3(c: Callable[Concatenate[int, P], int], *args: int) -> Some[P]: ... [builtins fixtures/paramspec.pyi] + + +[case testUnboundParamSpec] +from typing import Callable, ParamSpec + +P1 = ParamSpec('P1') +P2 = ParamSpec('P2') + +def f0(f: Callable[P1, int], *args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" + +def f1(*args: P1.args): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" +def f2(**kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" +def f3(*args: P1.args, **kwargs: int): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" +def f4(*args: int, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" + +# Error message is based on the `args` definition: +def f5(*args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs" +def f6(*args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" + +# Multiple `ParamSpec` variables can be found, they should not affect error message: +P3 = ParamSpec('P3') + +def f7(first: Callable[P3, int], *args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec must have "*args" typed as "P1.args" and "**kwargs" typed as "P1.kwargs" +def f8(first: Callable[P3, int], *args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec must have "*args" typed as "P2.args" and "**kwargs" typed as "P2.kwargs" +[builtins fixtures/paramspec.pyi] + + +[case testArgsKwargsWithoutParamSpecVar] +from typing import Generic, Callable, ParamSpec + +P = ParamSpec('P') + +# This must be allowed: +class Some(Generic[P]): + def call(self, *args: P.args, **kwargs: P.kwargs): ... + +# TODO: this probably should be reported. +def call(*args: P.args, **kwargs: P.kwargs): ... +[builtins fixtures/paramspec.pyi] From d888f0f722a98fa916b2ba17f463516fe3f80176 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 15 Oct 2022 15:58:51 +0300 Subject: [PATCH 9/9] Fix CI --- mypy/semanal.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 4ec4938bdacb..c3be240a38b6 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1327,9 +1327,11 @@ def check_paramspec_definition(self, defn: FuncDef) -> None: or not isinstance(kwargs_type, ParamSpecType) or args_type.name != kwargs_type.name ): - if args_defn_type is not None and args_defn_type.name.endswith(".args"): + if isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"): param_name = args_defn_type.name.split(".")[0] - elif kwargs_defn_type is not None and kwargs_defn_type.name.endswith(".kwargs"): + elif isinstance(kwargs_defn_type, UnboundType) and kwargs_defn_type.name.endswith( + ".kwargs" + ): param_name = kwargs_defn_type.name.split(".")[0] else: # Fallback for cases that probably should not ever happen: