From ab6f17e1fdf51bb7919c622c8253204831a7b819 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 11 Dec 2024 01:20:35 +0100 Subject: [PATCH 1/7] Add compliance test, stop ignoring some weird paramspec positions --- mypy/typeanal.py | 10 ++ .../unit/check-parameter-specification.test | 162 ++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 2f85e83bb3c34..f5f31e1771e97 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -304,6 +304,13 @@ def not_declared_in_type_params(self, tvar_name: str) -> bool: def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type: sym = self.lookup_qualified(t.name, t) + from_parts = False + if t.name.endswith((".args", ".kwargs")): + maybe_pspec = self.lookup_qualified(t.name.rsplit(".", 1)[0], t) + if maybe_pspec and isinstance(maybe_pspec.node, ParamSpecExpr): + sym = maybe_pspec + from_parts = True + if sym is not None: node = sym.node if isinstance(node, PlaceholderNode): @@ -367,6 +374,9 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) self.fail( f'ParamSpec "{t.name}" used with arguments', t, code=codes.VALID_TYPE ) + if from_parts and not self.allow_param_spec_literals: + self.fail("ParamSpec parts are not allowed here", t, code=codes.VALID_TYPE) + return AnyType(TypeOfAny.from_error) # Change the line number return ParamSpecType( tvar_def.name, diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 7f038b8117419..b3a8a31a99dfe 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2473,3 +2473,165 @@ def run(func: Callable[Concatenate[int, str, P], T], *args: P.args, **kwargs: P. return func2(*args_prefix, *args) [builtins fixtures/paramspec.pyi] + + + + + + + +[case testParamSpecConformance1] +#flags: --python-version 3.12 +from typing import Any, Callable, List, ParamSpec +from typing_extensions import Concatenate, TypeAlias + +P = ParamSpec("P") # OK +WrongName = ParamSpec("NotIt") # E: String argument 1 "NotIt" to ParamSpec(...) does not match variable name "WrongName" \ + # E: "int" not callable + + +# > Valid use locations + +TA1: TypeAlias = P # E # E: Invalid location for ParamSpec "P" \ + # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" + +TA2: TypeAlias = Callable[P, None] # OK +TA3: TypeAlias = Callable[Concatenate[int, P], None] # OK +TA4: TypeAlias = Callable[..., None] # OK +TA5: TypeAlias = Callable[..., None] # OK + + +def func1(x: P) -> P: # E # E: Invalid location for ParamSpec "P" \ + # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" + ... + + +def func2(x: Concatenate[int, P]) -> int: # E # E: Invalid location for Concatenate \ + # N: You can use Concatenate as the first argument to Callable + ... + + +def func3(x: List[P]) -> None: # E # E: Invalid location for ParamSpec "P" \ + # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" + ... + + +def func4(x: Callable[[int, str], P]) -> None: # E # E: Invalid location for ParamSpec "P" \ + # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" + ... + + +def func5(*args: P, **kwargs: P) -> None: # E # E: Invalid location for ParamSpec "P" \ + # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" + ... + + +[builtins fixtures/paramspec.pyi] + + + + + + +[case testParamSpecConformance2] +#flags: --python-version 3.12 +from typing import Any, Callable, ParamSpec, assert_type +from typing_extensions import Concatenate, TypeAlias + +P = ParamSpec("P") + + +def puts_p_into_scope1(f: Callable[P, int]) -> None: + def inner(*args: P.args, **kwargs: P.kwargs) -> None: # OK + pass + + def mixed_up(*args: P.kwargs, **kwargs: P.args) -> None: # E # E: Use "P.args" for variadic "*" parameter \ + # E: Use "P.kwargs" for variadic "**" parameter + pass + + def misplaced1(x: P.args) -> None: # E # E: ParamSpec parts are not allowed here + pass + + def bad_kwargs1(*args: P.args, **kwargs: P.args) -> None: # E # E: Use "P.kwargs" for variadic "**" parameter + pass + + def bad_kwargs2(*args: P.args, **kwargs: Any) -> None: # E + pass + + +def out_of_scope(*args: P.args, **kwargs: P.kwargs) -> None: # E + pass + + +def puts_p_into_scope2(f: Callable[P, int]) -> None: + stored_args: P.args # E # E: ParamSpec parts are not allowed here + stored_kwargs: P.kwargs # E # E: ParamSpec parts are not allowed here + + def just_args(*args: P.args) -> None: # E # E: ParamSpec parts are not allowed here + pass + + def just_kwargs(**kwargs: P.kwargs) -> None: # E # E: ParamSpec parts are not allowed here + pass + + +def decorator(f: Callable[P, int]) -> Callable[P, None]: + def foo(*args: P.args, **kwargs: P.kwargs) -> None: + assert_type(f(*args, **kwargs), int) # OK + + f(*kwargs, **args) # E # E: Argument 1 has incompatible type "*P.kwargs"; expected "P.args" \ + # E: Argument 2 has incompatible type "**P.args"; expected "P.kwargs" + + f(1, *args, **kwargs) # E # E: Argument 1 has incompatible type "int"; expected "P.args" + + return foo # OK + + +def add(f: Callable[P, int]) -> Callable[Concatenate[str, P], None]: + def foo(s: str, *args: P.args, **kwargs: P.kwargs) -> None: # OK + pass + + def bar(*args: P.args, s: str, **kwargs: P.kwargs) -> None: # E # E: ParamSpec parts are not allowed here + pass + + return foo # OK + + +def remove(f: Callable[Concatenate[int, P], int]) -> Callable[P, None]: + def foo(*args: P.args, **kwargs: P.kwargs) -> None: + f(1, *args, **kwargs) # OK + + f(*args, 1, **kwargs) # E # E: Argument 1 has incompatible type "*P.args"; expected "int" \ + # E: Argument 2 has incompatible type "int"; expected "P.args" + + f(*args, **kwargs) # E # E: Argument 1 has incompatible type "*P.args"; expected "int" + + return foo # OK + + +def outer(f: Callable[P, None]) -> Callable[P, None]: + def foo(x: int, *args: P.args, **kwargs: P.kwargs) -> None: + f(*args, **kwargs) + + def bar(*args: P.args, **kwargs: P.kwargs) -> None: + foo(1, *args, **kwargs) # OK + foo(x=1, *args, **kwargs) # E # E: "foo" gets multiple values for keyword argument "x" \ + # E: Argument 1 to "foo" has incompatible type "*P.args"; expected "int" \ + # E: Argument 3 to "foo" has incompatible type "**P.kwargs"; expected "int" + + return bar + + +def twice(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: + return f(*args, **kwargs) + f(*args, **kwargs) + + +def a_int_b_str(a: int, b: str) -> int: + return 0 + + +twice(a_int_b_str, 1, "A") # OK +twice(a_int_b_str, b="A", a=1) # OK +twice(a_int_b_str, "A", 1) # E # E: Argument 2 to "twice" has incompatible type "str"; expected "int" \ + # E: Argument 3 to "twice" has incompatible type "int"; expected "str" + +[builtins fixtures/paramspec.pyi] From 6ef6d89458cc0433b7c8b4ae34c13a3ea0c5a202 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 11 Dec 2024 01:53:19 +0100 Subject: [PATCH 2/7] Move ParamSpec validity checks to typeanal entirely --- mypy/semanal.py | 60 ----------------- mypy/typeanal.py | 67 +++++++++---------- .../unit/check-parameter-specification.test | 41 ++++++------ 3 files changed, 51 insertions(+), 117 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index edcc50e66e307..db5461b270d07 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -71,7 +71,6 @@ from mypy.nodes import ( ARG_NAMED, ARG_POS, - ARG_STAR, ARG_STAR2, CONTRAVARIANT, COVARIANT, @@ -980,7 +979,6 @@ 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) if isinstance(defn, FuncDef): assert isinstance(defn.type, CallableType) defn.type = set_callable_name(defn.type, defn) @@ -1609,64 +1607,6 @@ 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, defn: FuncDef) -> None: - func = defn.type - assert isinstance(func, CallableType) - - 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 - - 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") - ) - ): - # Looks like both `*args` and `**kwargs` are not `ParamSpec` - # It might be something else, skipping. - return - - 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 isinstance(args_defn_type, UnboundType) and args_defn_type.name.endswith(".args"): - param_name = args_defn_type.name.split(".")[0] - 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: - param_name = "P" - - self.fail( - f'ParamSpec must have "*args" typed as "{param_name}.args" and "**kwargs" typed as "{param_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 f5f31e1771e97..f1b413fd0e668 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1102,46 +1102,42 @@ def visit_callable_type( variables, _ = self.bind_function_type_variables(t, t) type_guard = self.anal_type_guard(t.ret_type) type_is = self.anal_type_is(t.ret_type) + arg_kinds = t.arg_kinds - if len(arg_kinds) >= 2 and arg_kinds[-2] == ARG_STAR and arg_kinds[-1] == ARG_STAR2: - arg_types = self.anal_array(t.arg_types[:-2], nested=nested) + [ - self.anal_star_arg_type(t.arg_types[-2], ARG_STAR, nested=nested), - self.anal_star_arg_type(t.arg_types[-1], ARG_STAR2, nested=nested), - ] - # If nested is True, it means we are analyzing a Callable[...] type, rather - # than a function definition type. We need to "unpack" ** TypedDict annotation - # here (for function definitions it is done in semanal). - if nested and isinstance(arg_types[-1], UnpackType): - # TODO: it would be better to avoid this get_proper_type() call. - unpacked = get_proper_type(arg_types[-1].type) - if isinstance(unpacked, TypedDictType): - arg_types[-1] = unpacked - unpacked_kwargs = True - arg_types = self.check_unpacks_in_list(arg_types) - else: - star_index = None - if ARG_STAR in arg_kinds: - star_index = arg_kinds.index(ARG_STAR) - star2_index = None - if ARG_STAR2 in arg_kinds: - star2_index = arg_kinds.index(ARG_STAR2) - arg_types = [] - for i, ut in enumerate(t.arg_types): - at = self.anal_type( - ut, nested=nested, allow_unpack=i in (star_index, star2_index) - ) - if nested and isinstance(at, UnpackType) and i == star_index: + arg_types = [] + has_pspec_args = has_pspec_kwargs = None + for kind, ut in zip(arg_kinds, t.arg_types): + if kind == ARG_STAR: + has_pspec_args, at = self.anal_star_arg_type(ut, kind, nested=nested) + elif kind == ARG_STAR2: + has_pspec_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) + if nested and isinstance(at, UnpackType): # TODO: it would be better to avoid this get_proper_type() call. p_at = get_proper_type(at.type) if isinstance(p_at, TypedDictType) and not at.from_star_syntax: # Automatically detect Unpack[Foo] in Callable as backwards # compatible syntax for **Foo, if Foo is a TypedDict. at = p_at - arg_kinds[i] = ARG_STAR2 unpacked_kwargs = True - arg_types.append(at) - if nested: - arg_types = self.check_unpacks_in_list(arg_types) + else: + if has_pspec_args: + self.fail("Arguments not allowed after ParamSpec.args", t) + at = self.anal_type(ut, nested=nested, allow_unpack=False) + arg_types.append(at) + if nested: + arg_types = self.check_unpacks_in_list(arg_types) + + if has_pspec_args != has_pspec_kwargs: + name = has_pspec_args or has_pspec_kwargs + self.fail( + f'ParamSpec must have "*args" typed as "{name}.args" and "**kwargs" typed as "{name}.kwargs"', + t, + ) + if ARG_STAR in arg_kinds: + arg_types[arg_kinds.index(ARG_STAR)] = AnyType(TypeOfAny.from_error) + if ARG_STAR2 in arg_kinds: + arg_types[arg_kinds.index(ARG_STAR2)] = AnyType(TypeOfAny.from_error) + # If there were multiple (invalid) unpacks, the arg types list will become shorter, # we need to trim the kinds/names as well to avoid crashes. arg_kinds = t.arg_kinds[: len(arg_types)] @@ -1196,7 +1192,7 @@ def anal_type_is_arg(self, t: UnboundType, fullname: str) -> Type | None: return self.anal_type(t.args[0]) return None - def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type: + def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> tuple[str | None, Type]: """Analyze signature argument type for *args and **kwargs argument.""" if isinstance(t, UnboundType) and t.name and "." in t.name and not t.args: components = t.name.split(".") @@ -1205,6 +1201,7 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type: if sym is not None and isinstance(sym.node, ParamSpecExpr): tvar_def = self.tvar_scope.get_binding(sym) if isinstance(tvar_def, ParamSpecType): + # if t.line==25:breakpoint() if kind == ARG_STAR: make_paramspec = paramspec_args if components[-1] != "args": @@ -1223,7 +1220,7 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type: ) else: assert False, kind - return make_paramspec( + return tvar_name, make_paramspec( tvar_def.name, tvar_def.fullname, tvar_def.id, @@ -1231,7 +1228,7 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> Type: line=t.line, column=t.column, ) - return self.anal_type(t, nested=nested, allow_unpack=True) + return None, self.anal_type(t, nested=nested, allow_unpack=True) def visit_overloaded(self, t: Overloaded) -> Type: # Overloaded types are manually constructed in semanal.py by analyzing the diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index b3a8a31a99dfe..b7915a590342f 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -440,13 +440,13 @@ class C(Generic[P, P2]): def m1(self, *args: P.args, **kwargs: P.kwargs) -> None: self.m1(*args, **kwargs) self.m2(*args, **kwargs) # E: Argument 1 to "m2" of "C" has incompatible type "*P.args"; expected "P2.args" \ - # E: Argument 2 to "m2" of "C" has incompatible type "**P.kwargs"; expected "P2.kwargs" + # E: Argument 2 to "m2" of "C" has incompatible type "**P.kwargs"; expected "P2.kwargs" self.m1(*kwargs, **args) # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" \ - # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" + # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" self.m3(*args, **kwargs) # E: Argument 1 to "m3" of "C" has incompatible type "*P.args"; expected "int" \ - # E: Argument 2 to "m3" of "C" has incompatible type "**P.kwargs"; expected "int" + # E: Argument 2 to "m3" of "C" has incompatible type "**P.kwargs"; expected "int" self.m4(*args, **kwargs) # E: Argument 1 to "m4" of "C" has incompatible type "*P.args"; expected "int" \ - # E: Argument 2 to "m4" of "C" has incompatible type "**P.kwargs"; expected "int" + # E: Argument 2 to "m4" of "C" has incompatible type "**P.kwargs"; expected "int" self.m1(*args, **args) # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" self.m1(*kwargs, **kwargs) # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" @@ -1264,7 +1264,7 @@ def f1(f: Callable[P, int], *args, **kwargs: P.kwargs) -> int: ... # E: ParamSp 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: P.args, extra_keyword_arg: 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: P.args, extra_keyword_arg: int, **kwargs: P.kwargs) -> int: ... # E: Arguments not allowed after ParamSpec.args # Error message test: P1 = ParamSpec('P1') @@ -1294,7 +1294,10 @@ def f1(f: Callable[Concatenate[int, P], int], *args, **kwargs: P.kwargs) -> int: 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" -def f5(f: Callable[Concatenate[int, P], int], *args: P.args, extra_keyword_arg: int, **kwargs: P.kwargs) -> int: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" +def f5(f: Callable[Concatenate[int, P], int], *args: P.args, extra_keyword_arg: int, **kwargs: P.kwargs) -> int: ... # E: Arguments not allowed after ParamSpec.args + + + [builtins fixtures/paramspec.pyi] @@ -2474,12 +2477,6 @@ def run(func: Callable[Concatenate[int, str, P], T], *args: P.args, **kwargs: P. [builtins fixtures/paramspec.pyi] - - - - - - [case testParamSpecConformance1] #flags: --python-version 3.12 from typing import Any, Callable, List, ParamSpec @@ -2525,14 +2522,8 @@ def func5(*args: P, **kwargs: P) -> None: # E # E: Invalid location for ParamS # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" ... - [builtins fixtures/paramspec.pyi] - - - - - [case testParamSpecConformance2] #flags: --python-version 3.12 from typing import Any, Callable, ParamSpec, assert_type @@ -2555,7 +2546,7 @@ def puts_p_into_scope1(f: Callable[P, int]) -> None: def bad_kwargs1(*args: P.args, **kwargs: P.args) -> None: # E # E: Use "P.kwargs" for variadic "**" parameter pass - def bad_kwargs2(*args: P.args, **kwargs: Any) -> None: # E + def bad_kwargs2(*args: P.args, **kwargs: Any) -> None: # E # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" pass @@ -2567,10 +2558,10 @@ def puts_p_into_scope2(f: Callable[P, int]) -> None: stored_args: P.args # E # E: ParamSpec parts are not allowed here stored_kwargs: P.kwargs # E # E: ParamSpec parts are not allowed here - def just_args(*args: P.args) -> None: # E # E: ParamSpec parts are not allowed here + def just_args(*args: P.args) -> None: # E # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" pass - def just_kwargs(**kwargs: P.kwargs) -> None: # E # E: ParamSpec parts are not allowed here + def just_kwargs(**kwargs: P.kwargs) -> None: # E # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" pass @@ -2590,7 +2581,7 @@ def add(f: Callable[P, int]) -> Callable[Concatenate[str, P], None]: def foo(s: str, *args: P.args, **kwargs: P.kwargs) -> None: # OK pass - def bar(*args: P.args, s: str, **kwargs: P.kwargs) -> None: # E # E: ParamSpec parts are not allowed here + def bar(*args: P.args, s: str, **kwargs: P.kwargs) -> None: # E # E: Arguments not allowed after ParamSpec.args pass return foo # OK @@ -2634,4 +2625,10 @@ twice(a_int_b_str, b="A", a=1) # OK twice(a_int_b_str, "A", 1) # E # E: Argument 2 to "twice" has incompatible type "str"; expected "int" \ # E: Argument 3 to "twice" has incompatible type "int"; expected "str" + + + + + + [builtins fixtures/paramspec.pyi] From 5d9d703fff3189318158e12a708906cb279fbdc6 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 11 Dec 2024 02:27:29 +0100 Subject: [PATCH 3/7] Remove "special" treatment for ParamSpec parts encountered during analysis - only bare paramspec binds. --- mypy/typeanal.py | 45 ++-- .../unit/check-parameter-specification.test | 225 ++++++------------ 2 files changed, 90 insertions(+), 180 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index f1b413fd0e668..634861bbc692d 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -304,12 +304,14 @@ def not_declared_in_type_params(self, tvar_name: str) -> bool: def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type: sym = self.lookup_qualified(t.name, t) - from_parts = False + paramspec_name = None if t.name.endswith((".args", ".kwargs")): - maybe_pspec = self.lookup_qualified(t.name.rsplit(".", 1)[0], t) + paramspec_name = t.name.rsplit(".", 1)[0] + maybe_pspec = self.lookup_qualified(paramspec_name, t) if maybe_pspec and isinstance(maybe_pspec.node, ParamSpecExpr): sym = maybe_pspec - from_parts = True + else: + paramspec_name = None if sym is not None: node = sym.node @@ -363,10 +365,11 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) if tvar_def is None: if self.allow_unbound_tvars: return t + name = paramspec_name or t.name if self.defining_alias and self.not_declared_in_type_params(t.name): - msg = f'ParamSpec "{t.name}" is not included in type_params' + msg = f'ParamSpec "{name}" is not included in type_params' else: - msg = f'ParamSpec "{t.name}" is unbound' + msg = f'ParamSpec "{name}" is unbound' self.fail(msg, t, code=codes.VALID_TYPE) return AnyType(TypeOfAny.from_error) assert isinstance(tvar_def, ParamSpecType) @@ -374,8 +377,10 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) self.fail( f'ParamSpec "{t.name}" used with arguments', t, code=codes.VALID_TYPE ) - if from_parts and not self.allow_param_spec_literals: - self.fail("ParamSpec parts are not allowed here", t, code=codes.VALID_TYPE) + if paramspec_name is not None and not self.allow_param_spec_literals: + self.fail( + "ParamSpec components are not allowed here", t, code=codes.VALID_TYPE + ) return AnyType(TypeOfAny.from_error) # Change the line number return ParamSpecType( @@ -1105,12 +1110,12 @@ def visit_callable_type( arg_kinds = t.arg_kinds arg_types = [] - has_pspec_args = has_pspec_kwargs = None + pspec_with_args = pspec_with_kwargs = None for kind, ut in zip(arg_kinds, t.arg_types): if kind == ARG_STAR: - has_pspec_args, at = self.anal_star_arg_type(ut, kind, nested=nested) + pspec_with_args, at = self.anal_star_arg_type(ut, kind, nested=nested) elif kind == ARG_STAR2: - has_pspec_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) + pspec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) if nested and isinstance(at, UnpackType): # TODO: it would be better to avoid this get_proper_type() call. p_at = get_proper_type(at.type) @@ -1120,15 +1125,15 @@ def visit_callable_type( at = p_at unpacked_kwargs = True else: - if has_pspec_args: + if pspec_with_args: self.fail("Arguments not allowed after ParamSpec.args", t) at = self.anal_type(ut, nested=nested, allow_unpack=False) arg_types.append(at) if nested: arg_types = self.check_unpacks_in_list(arg_types) - if has_pspec_args != has_pspec_kwargs: - name = has_pspec_args or has_pspec_kwargs + if pspec_with_args != pspec_with_kwargs: + name = pspec_with_args or pspec_with_kwargs self.fail( f'ParamSpec must have "*args" typed as "{name}.args" and "**kwargs" typed as "{name}.kwargs"', t, @@ -1201,7 +1206,6 @@ def anal_star_arg_type(self, t: Type, kind: ArgKind, nested: bool) -> tuple[str if sym is not None and isinstance(sym.node, ParamSpecExpr): tvar_def = self.tvar_scope.get_binding(sym) if isinstance(tvar_def, ParamSpecType): - # if t.line==25:breakpoint() if kind == ARG_STAR: make_paramspec = paramspec_args if components[-1] != "args": @@ -2574,18 +2578,7 @@ def _seems_like_callable(self, type: UnboundType) -> bool: def visit_unbound_type(self, t: UnboundType) -> None: name = t.name - node = None - - # Special case P.args and P.kwargs for ParamSpecs only. - if name.endswith("args"): - if name.endswith((".args", ".kwargs")): - base = ".".join(name.split(".")[:-1]) - n = self.api.lookup_qualified(base, t) - if n is not None and isinstance(n.node, ParamSpecExpr): - node = n - name = base - if node is None: - node = self.api.lookup_qualified(name, t) + node = self.api.lookup_qualified(name, t) if node and node.fullname in SELF_TYPE_NAMES: self.has_self_type = True if ( diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index b7915a590342f..d87c144a5d5a8 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -14,7 +14,7 @@ P5 = ParamSpec("P5", covariant=True, bound=int) # E: The variance and bound arg [builtins fixtures/paramspec.pyi] [case testParamSpecLocations] -from typing import Callable, List +from typing import Any, Callable, List from typing_extensions import ParamSpec, Concatenate P = ParamSpec('P') @@ -36,6 +36,20 @@ def foo5(x: Callable[[int, str], P]) -> None: ... # E: Invalid location for Par def foo6(x: Callable[[P], int]) -> None: ... # E: Invalid location for ParamSpec "P" \ # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" + +def wrapper(f: Callable[P, int]) -> None: + def inner(*args: P.args, **kwargs: P.kwargs) -> None: ... # OK + + def extra_args_left(x: int, *args: P.args, **kwargs: P.kwargs) -> None: ... # OK + def extra_args_between(*args: P.args, x: int, **kwargs: P.kwargs) -> None: ... # E: Arguments not allowed after ParamSpec.args + + def swapped(*args: P.kwargs, **kwargs: P.args) -> None: ... # E: Use "P.args" for variadic "*" parameter \ + # E: Use "P.kwargs" for variadic "**" parameter + def bad_kwargs(*args: P.args, **kwargs: P.args) -> None: ... # E: Use "P.kwargs" for variadic "**" parameter + def bad_args(*args: P.kwargs, **kwargs: P.kwargs) -> None: ... # E: Use "P.args" for variadic "*" parameter + + def misplaced(x: P.args) -> None: ... # E: ParamSpec components are not allowed here + def bad_kwargs_any(*args: P.args, **kwargs: Any) -> None: ... # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" [builtins fixtures/paramspec.pyi] [case testParamSpecImports] @@ -1295,9 +1309,6 @@ def f2(f: Callable[Concatenate[int, P], int], *args: P.args, **kwargs) -> int: . 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" def f5(f: Callable[Concatenate[int, P], int], *args: P.args, extra_keyword_arg: int, **kwargs: P.kwargs) -> int: ... # E: Arguments not allowed after ParamSpec.args - - - [builtins fixtures/paramspec.pyi] @@ -1329,22 +1340,28 @@ 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 f0(f: Callable[P1, int], *args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec "P2" is unbound \ + # 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" +def f1(*args: P1.args): ... # E: ParamSpec "P1" is unbound +def f2(**kwargs: P1.kwargs): ... # E: ParamSpec "P1" is unbound +def f3(*args: P1.args, **kwargs: int): ... # E: ParamSpec "P1" is unbound +def f4(*args: int, **kwargs: P1.kwargs): ... # E: ParamSpec "P1" is unbound # 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" +def f5(*args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec "P2" is unbound \ + # E: ParamSpec "P1" is unbound +def f6(*args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec "P1" is unbound \ + # E: ParamSpec "P2" is unbound # 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" +def f7(first: Callable[P3, int], *args: P1.args, **kwargs: P2.kwargs): ... # E: ParamSpec "P1" is unbound \ + # E: ParamSpec "P2" is unbound +def f8(first: Callable[P3, int], *args: P2.args, **kwargs: P1.kwargs): ... # E: ParamSpec "P2" is unbound \ + # E: ParamSpec "P1" is unbound + [builtins fixtures/paramspec.pyi] @@ -1357,7 +1374,8 @@ P = ParamSpec('P') class Some(Generic[P]): def call(self, *args: P.args, **kwargs: P.kwargs): ... -def call(*args: P.args, **kwargs: P.kwargs): ... +def call(*args: P.args, **kwargs: P.kwargs): ... # E: ParamSpec "P" is unbound + [builtins fixtures/paramspec.pyi] [case testParamSpecInferenceCrash] @@ -2147,7 +2165,7 @@ from typing_extensions import ParamSpec P= ParamSpec("P") T = TypeVar("T") -def smoke_testable(*args: P.args, **kwargs: P.kwargs) -> Callable[[Callable[P, T]], Type[T]]: +def smoke_testable(*args: P.args, **kwargs: P.kwargs) -> Callable[[Callable[P, T]], Type[T]]: # E: ParamSpec "P" is unbound ... @smoke_testable(name="bob", size=512, flt=0.5) @@ -2156,7 +2174,7 @@ class SomeClass: pass # Error message is confusing, but this is a known issue, see #4530. -@smoke_testable(name=42, size="bad", flt=0.5) # E: Argument 1 has incompatible type "Type[OtherClass]"; expected "Callable[[int, str, float], OtherClass]" +@smoke_testable(name=42, size="bad", flt=0.5) class OtherClass: def __init__(self, size: int, name: str, flt: float) -> None: pass @@ -2477,158 +2495,57 @@ def run(func: Callable[Concatenate[int, str, P], T], *args: P.args, **kwargs: P. [builtins fixtures/paramspec.pyi] -[case testParamSpecConformance1] -#flags: --python-version 3.12 -from typing import Any, Callable, List, ParamSpec -from typing_extensions import Concatenate, TypeAlias - -P = ParamSpec("P") # OK -WrongName = ParamSpec("NotIt") # E: String argument 1 "NotIt" to ParamSpec(...) does not match variable name "WrongName" \ - # E: "int" not callable - - -# > Valid use locations - -TA1: TypeAlias = P # E # E: Invalid location for ParamSpec "P" \ - # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" - -TA2: TypeAlias = Callable[P, None] # OK -TA3: TypeAlias = Callable[Concatenate[int, P], None] # OK -TA4: TypeAlias = Callable[..., None] # OK -TA5: TypeAlias = Callable[..., None] # OK - - -def func1(x: P) -> P: # E # E: Invalid location for ParamSpec "P" \ - # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" - ... - - -def func2(x: Concatenate[int, P]) -> int: # E # E: Invalid location for Concatenate \ - # N: You can use Concatenate as the first argument to Callable - ... - - -def func3(x: List[P]) -> None: # E # E: Invalid location for ParamSpec "P" \ - # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" - ... - - -def func4(x: Callable[[int, str], P]) -> None: # E # E: Invalid location for ParamSpec "P" \ - # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" - ... - - -def func5(*args: P, **kwargs: P) -> None: # E # E: Invalid location for ParamSpec "P" \ - # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" - ... - -[builtins fixtures/paramspec.pyi] - -[case testParamSpecConformance2] -#flags: --python-version 3.12 -from typing import Any, Callable, ParamSpec, assert_type -from typing_extensions import Concatenate, TypeAlias +[case testParamSpecScoping] +from typing import Any, Callable, Generic +from typing_extensions import Concatenate, ParamSpec P = ParamSpec("P") +P2 = ParamSpec("P2") +def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... +def contains_other(f: Callable[P2, None], c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... -def puts_p_into_scope1(f: Callable[P, int]) -> None: - def inner(*args: P.args, **kwargs: P.kwargs) -> None: # OK - pass - - def mixed_up(*args: P.kwargs, **kwargs: P.args) -> None: # E # E: Use "P.args" for variadic "*" parameter \ - # E: Use "P.kwargs" for variadic "**" parameter - pass - - def misplaced1(x: P.args) -> None: # E # E: ParamSpec parts are not allowed here - pass - - def bad_kwargs1(*args: P.args, **kwargs: P.args) -> None: # E # E: Use "P.kwargs" for variadic "**" parameter - pass - - def bad_kwargs2(*args: P.args, **kwargs: Any) -> None: # E # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" - pass - - -def out_of_scope(*args: P.args, **kwargs: P.kwargs) -> None: # E - pass - - -def puts_p_into_scope2(f: Callable[P, int]) -> None: - stored_args: P.args # E # E: ParamSpec parts are not allowed here - stored_kwargs: P.kwargs # E # E: ParamSpec parts are not allowed here - - def just_args(*args: P.args) -> None: # E # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" - pass - - def just_kwargs(**kwargs: P.kwargs) -> None: # E # E: ParamSpec must have "*args" typed as "P.args" and "**kwargs" typed as "P.kwargs" - pass - - -def decorator(f: Callable[P, int]) -> Callable[P, None]: - def foo(*args: P.args, **kwargs: P.kwargs) -> None: - assert_type(f(*args, **kwargs), int) # OK - - f(*kwargs, **args) # E # E: Argument 1 has incompatible type "*P.kwargs"; expected "P.args" \ - # E: Argument 2 has incompatible type "**P.args"; expected "P.kwargs" - - f(1, *args, **kwargs) # E # E: Argument 1 has incompatible type "int"; expected "P.args" - - return foo # OK - - -def add(f: Callable[P, int]) -> Callable[Concatenate[str, P], None]: - def foo(s: str, *args: P.args, **kwargs: P.kwargs) -> None: # OK - pass - - def bar(*args: P.args, s: str, **kwargs: P.kwargs) -> None: # E # E: Arguments not allowed after ParamSpec.args - pass - - return foo # OK - - -def remove(f: Callable[Concatenate[int, P], int]) -> Callable[P, None]: - def foo(*args: P.args, **kwargs: P.kwargs) -> None: - f(1, *args, **kwargs) # OK - - f(*args, 1, **kwargs) # E # E: Argument 1 has incompatible type "*P.args"; expected "int" \ - # E: Argument 2 has incompatible type "int"; expected "P.args" - - f(*args, **kwargs) # E # E: Argument 1 has incompatible type "*P.args"; expected "int" - - return foo # OK - - -def outer(f: Callable[P, None]) -> Callable[P, None]: - def foo(x: int, *args: P.args, **kwargs: P.kwargs) -> None: - f(*args, **kwargs) - - def bar(*args: P.args, **kwargs: P.kwargs) -> None: - foo(1, *args, **kwargs) # OK - foo(x=1, *args, **kwargs) # E # E: "foo" gets multiple values for keyword argument "x" \ - # E: Argument 1 to "foo" has incompatible type "*P.args"; expected "int" \ - # E: Argument 3 to "foo" has incompatible type "**P.kwargs"; expected "int" - - return bar - +def contains_only_other(c: Callable[P2, None], *args: P.args, **kwargs: P.kwargs) -> None: ... # E: ParamSpec "P" is unbound -def twice(f: Callable[P, int], *args: P.args, **kwargs: P.kwargs) -> int: - return f(*args, **kwargs) + f(*args, **kwargs) +def puts_p_into_scope(f: Callable[P, int]) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... +def puts_p_into_scope_concatenate(f: Callable[Concatenate[int, P], int]) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... -def a_int_b_str(a: int, b: str) -> int: - return 0 +def wrapper() -> None: + def puts_p_into_scope1(f: Callable[P, int]) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... +class Wrapper: + def puts_p_into_scope1(self, f: Callable[P, int]) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... -twice(a_int_b_str, 1, "A") # OK -twice(a_int_b_str, b="A", a=1) # OK -twice(a_int_b_str, "A", 1) # E # E: Argument 2 to "twice" has incompatible type "str"; expected "int" \ - # E: Argument 3 to "twice" has incompatible type "int"; expected "str" + def contains(self, c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def uses(self, *args: P.args, **kwargs: P.kwargs) -> None: ... # E: ParamSpec "P" is unbound + def method(self) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... # E: ParamSpec "P" is unbound +class GenericWrapper(Generic[P]): + x: P.args # E: ParamSpec components are not allowed here + y: P.kwargs # E: ParamSpec components are not allowed here + def contains(self, c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def puts_p_into_scope1(self, f: Callable[P, int]) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... + def uses(self, *args: P.args, **kwargs: P.kwargs) -> None: ... + def method(self) -> None: + def contains(c: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ... + def inherits(*args: P.args, **kwargs: P.kwargs) -> None: ... [builtins fixtures/paramspec.pyi] From d461ba299a21880421eb7fda0c1b1f17063ab607 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 11 Dec 2024 02:50:05 +0100 Subject: [PATCH 4/7] Fix broken `Callable[[Unpack[SomeTypedDict]], None]` --- mypy/typeanal.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 634861bbc692d..9f5cca49fc3c0 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1111,11 +1111,10 @@ def visit_callable_type( arg_kinds = t.arg_kinds arg_types = [] pspec_with_args = pspec_with_kwargs = None - for kind, ut in zip(arg_kinds, t.arg_types): + for i, ut in enumerate(t.arg_types): + kind = arg_kinds[i] if kind == ARG_STAR: pspec_with_args, at = self.anal_star_arg_type(ut, kind, nested=nested) - elif kind == ARG_STAR2: - pspec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) if nested and isinstance(at, UnpackType): # TODO: it would be better to avoid this get_proper_type() call. p_at = get_proper_type(at.type) @@ -1124,6 +1123,9 @@ def visit_callable_type( # compatible syntax for **Foo, if Foo is a TypedDict. at = p_at unpacked_kwargs = True + arg_kinds[i] = ARG_STAR2 + elif kind == ARG_STAR2: + pspec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) else: if pspec_with_args: self.fail("Arguments not allowed after ParamSpec.args", t) From dfa08879085005314e36a1b6fbe0626f0f3e8bcc Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 11 Dec 2024 04:30:10 +0100 Subject: [PATCH 5/7] Recover Unpack sanity checks --- mypy/typeanal.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 9f5cca49fc3c0..7c28a8db1f6ae 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1115,15 +1115,6 @@ def visit_callable_type( kind = arg_kinds[i] if kind == ARG_STAR: pspec_with_args, at = self.anal_star_arg_type(ut, kind, nested=nested) - if nested and isinstance(at, UnpackType): - # TODO: it would be better to avoid this get_proper_type() call. - p_at = get_proper_type(at.type) - if isinstance(p_at, TypedDictType) and not at.from_star_syntax: - # Automatically detect Unpack[Foo] in Callable as backwards - # compatible syntax for **Foo, if Foo is a TypedDict. - at = p_at - unpacked_kwargs = True - arg_kinds[i] = ARG_STAR2 elif kind == ARG_STAR2: pspec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) else: @@ -1131,7 +1122,18 @@ def visit_callable_type( self.fail("Arguments not allowed after ParamSpec.args", t) at = self.anal_type(ut, nested=nested, allow_unpack=False) arg_types.append(at) - if nested: + + if nested and arg_types: + last = arg_types[-1] + if isinstance(last, UnpackType): + # TODO: it would be better to avoid this get_proper_type() call. + p_at = get_proper_type(last.type) + if isinstance(p_at, TypedDictType) and not last.from_star_syntax: + # Automatically detect Unpack[Foo] in Callable as backwards + # compatible syntax for **Foo, if Foo is a TypedDict. + arg_kinds[-1] = ARG_STAR2 + arg_types[-1] = p_at + unpacked_kwargs = True arg_types = self.check_unpacks_in_list(arg_types) if pspec_with_args != pspec_with_kwargs: From 643a0dbd2ef782d9bca96af0370736e79f42bbbe Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 11 Dec 2024 05:27:18 +0100 Subject: [PATCH 6/7] Fix runtime crash in prefect, clean up naming --- mypy/typeanal.py | 45 ++++++++++++------- .../unit/check-parameter-specification.test | 8 ++-- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7c28a8db1f6ae..c9cddb0590d65 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -304,14 +304,14 @@ def not_declared_in_type_params(self, tvar_name: str) -> bool: def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) -> Type: sym = self.lookup_qualified(t.name, t) - paramspec_name = None + param_spec_name = None if t.name.endswith((".args", ".kwargs")): - paramspec_name = t.name.rsplit(".", 1)[0] - maybe_pspec = self.lookup_qualified(paramspec_name, t) - if maybe_pspec and isinstance(maybe_pspec.node, ParamSpecExpr): - sym = maybe_pspec + param_spec_name = t.name.rsplit(".", 1)[0] + maybe_param_spec = self.lookup_qualified(param_spec_name, t) + if maybe_param_spec and isinstance(maybe_param_spec.node, ParamSpecExpr): + sym = maybe_param_spec else: - paramspec_name = None + param_spec_name = None if sym is not None: node = sym.node @@ -365,7 +365,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) if tvar_def is None: if self.allow_unbound_tvars: return t - name = paramspec_name or t.name + name = param_spec_name or t.name if self.defining_alias and self.not_declared_in_type_params(t.name): msg = f'ParamSpec "{name}" is not included in type_params' else: @@ -377,7 +377,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) self.fail( f'ParamSpec "{t.name}" used with arguments', t, code=codes.VALID_TYPE ) - if paramspec_name is not None and not self.allow_param_spec_literals: + if param_spec_name is not None and not self.allow_param_spec_literals: self.fail( "ParamSpec components are not allowed here", t, code=codes.VALID_TYPE ) @@ -1110,20 +1110,25 @@ def visit_callable_type( arg_kinds = t.arg_kinds arg_types = [] - pspec_with_args = pspec_with_kwargs = None - for i, ut in enumerate(t.arg_types): - kind = arg_kinds[i] + param_spec_with_args = param_spec_with_kwargs = None + param_spec_invalid = False + for kind, ut in zip(arg_kinds, t.arg_types): if kind == ARG_STAR: - pspec_with_args, at = self.anal_star_arg_type(ut, kind, nested=nested) + param_spec_with_args, at = self.anal_star_arg_type(ut, kind, nested=nested) elif kind == ARG_STAR2: - pspec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) + param_spec_with_kwargs, at = self.anal_star_arg_type(ut, kind, nested=nested) else: - if pspec_with_args: - self.fail("Arguments not allowed after ParamSpec.args", t) + if param_spec_with_args: + param_spec_invalid = True + self.fail( + "Arguments not allowed after ParamSpec.args", t, code=codes.VALID_TYPE + ) at = self.anal_type(ut, nested=nested, allow_unpack=False) arg_types.append(at) if nested and arg_types: + # If we've got a Callable[[Unpack[SomeTypedDict]], None], make sure + # Unpack is interpreted as `**` and not as `*`. last = arg_types[-1] if isinstance(last, UnpackType): # TODO: it would be better to avoid this get_proper_type() call. @@ -1136,12 +1141,18 @@ def visit_callable_type( unpacked_kwargs = True arg_types = self.check_unpacks_in_list(arg_types) - if pspec_with_args != pspec_with_kwargs: - name = pspec_with_args or pspec_with_kwargs + if not param_spec_invalid and param_spec_with_args != param_spec_with_kwargs: + # If already invalid, do not report more errors - definition has + # to be fixed anyway + name = param_spec_with_args or param_spec_with_kwargs self.fail( f'ParamSpec must have "*args" typed as "{name}.args" and "**kwargs" typed as "{name}.kwargs"', t, + code=codes.VALID_TYPE, ) + param_spec_invalid = True + + if param_spec_invalid: if ARG_STAR in arg_kinds: arg_types[arg_kinds.index(ARG_STAR)] = AnyType(TypeOfAny.from_error) if ARG_STAR2 in arg_kinds: diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index d87c144a5d5a8..ddcfa478e8adc 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -454,13 +454,13 @@ class C(Generic[P, P2]): def m1(self, *args: P.args, **kwargs: P.kwargs) -> None: self.m1(*args, **kwargs) self.m2(*args, **kwargs) # E: Argument 1 to "m2" of "C" has incompatible type "*P.args"; expected "P2.args" \ - # E: Argument 2 to "m2" of "C" has incompatible type "**P.kwargs"; expected "P2.kwargs" + # E: Argument 2 to "m2" of "C" has incompatible type "**P.kwargs"; expected "P2.kwargs" self.m1(*kwargs, **args) # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" \ - # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" + # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" self.m3(*args, **kwargs) # E: Argument 1 to "m3" of "C" has incompatible type "*P.args"; expected "int" \ - # E: Argument 2 to "m3" of "C" has incompatible type "**P.kwargs"; expected "int" + # E: Argument 2 to "m3" of "C" has incompatible type "**P.kwargs"; expected "int" self.m4(*args, **kwargs) # E: Argument 1 to "m4" of "C" has incompatible type "*P.args"; expected "int" \ - # E: Argument 2 to "m4" of "C" has incompatible type "**P.kwargs"; expected "int" + # E: Argument 2 to "m4" of "C" has incompatible type "**P.kwargs"; expected "int" self.m1(*args, **args) # E: Argument 2 to "m1" of "C" has incompatible type "**P.args"; expected "P.kwargs" self.m1(*kwargs, **kwargs) # E: Argument 1 to "m1" of "C" has incompatible type "*P.kwargs"; expected "P.args" From c8204ed8f5f2abc4671fd09894c25e093d936382 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Thu, 2 Jan 2025 18:38:04 +0100 Subject: [PATCH 7/7] Remove an non-conformant old test, move the problematic part of it to testParamSpecLocations --- .../unit/check-parameter-specification.test | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index ddcfa478e8adc..75ab3da2ee410 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -14,7 +14,7 @@ P5 = ParamSpec("P5", covariant=True, bound=int) # E: The variance and bound arg [builtins fixtures/paramspec.pyi] [case testParamSpecLocations] -from typing import Any, Callable, List +from typing import Any, Callable, List, Type from typing_extensions import ParamSpec, Concatenate P = ParamSpec('P') @@ -37,6 +37,11 @@ def foo5(x: Callable[[int, str], P]) -> None: ... # E: Invalid location for Par def foo6(x: Callable[[P], int]) -> None: ... # E: Invalid location for ParamSpec "P" \ # N: You can use ParamSpec as the first argument to Callable, e.g., "Callable[P, int]" +def foo7( + *args: P.args, **kwargs: P.kwargs # E: ParamSpec "P" is unbound +) -> Callable[[Callable[P, T]], Type[T]]: + ... + def wrapper(f: Callable[P, int]) -> None: def inner(*args: P.args, **kwargs: P.kwargs) -> None: ... # OK @@ -2158,28 +2163,6 @@ submit( ) [builtins fixtures/paramspec.pyi] -[case testParamSpecGenericWithNamedArg2] -from typing import Callable, TypeVar, Type -from typing_extensions import ParamSpec - -P= ParamSpec("P") -T = TypeVar("T") - -def smoke_testable(*args: P.args, **kwargs: P.kwargs) -> Callable[[Callable[P, T]], Type[T]]: # E: ParamSpec "P" is unbound - ... - -@smoke_testable(name="bob", size=512, flt=0.5) -class SomeClass: - def __init__(self, size: int, name: str, flt: float) -> None: - pass - -# Error message is confusing, but this is a known issue, see #4530. -@smoke_testable(name=42, size="bad", flt=0.5) -class OtherClass: - def __init__(self, size: int, name: str, flt: float) -> None: - pass -[builtins fixtures/paramspec.pyi] - [case testInferenceAgainstGenericCallableUnionParamSpec] from typing import Callable, TypeVar, List, Union from typing_extensions import ParamSpec