From 93dac05cc8461f13c2031dff48711eecbe2595af Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 3 Jun 2024 17:10:38 +0100 Subject: [PATCH] [PEP 695] Fix a few issues and add tests (#17318) Fix badly formed types that could be created when using aliases like `type A = list`. Improve some error messages when using PEP 695 syntax. Add a few PEP 695 tests. Work on #15238. --- mypy/message_registry.py | 3 ++ mypy/messages.py | 6 ++- mypy/semanal.py | 5 +++ mypy/typeanal.py | 30 ++++++++++--- test-data/unit/check-python312.test | 69 +++++++++++++++++++++++++++++ 5 files changed, 105 insertions(+), 8 deletions(-) diff --git a/mypy/message_registry.py b/mypy/message_registry.py index ccc1443dacf0..3852431f2290 100644 --- a/mypy/message_registry.py +++ b/mypy/message_registry.py @@ -194,6 +194,9 @@ def with_additional_msg(self, info: str) -> ErrorMessage: "A function returning TypeVar should receive at least " "one argument containing the same TypeVar" ) +TYPE_PARAMETERS_SHOULD_BE_DECLARED: Final = ( + "All type parameters should be declared ({} not declared)" +) # Super TOO_MANY_ARGS_FOR_SUPER: Final = ErrorMessage('Too many arguments for "super"') diff --git a/mypy/messages.py b/mypy/messages.py index 53a7f7d97774..de079feda048 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2423,7 +2423,11 @@ def annotation_in_unchecked_function(self, context: Context) -> None: def type_parameters_should_be_declared(self, undeclared: list[str], context: Context) -> None: names = ", ".join('"' + n + '"' for n in undeclared) - self.fail(f"All type parameters should be declared ({names} not declared)", context) + self.fail( + message_registry.TYPE_PARAMETERS_SHOULD_BE_DECLARED.format(names), + context, + code=codes.VALID_TYPE, + ) def quote_type_string(type_string: str) -> str: diff --git a/mypy/semanal.py b/mypy/semanal.py index 0689d5416efe..44db7ddf5618 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3694,6 +3694,7 @@ def analyze_alias( allow_placeholder: bool = False, declared_type_vars: TypeVarLikeList | None = None, all_declared_type_params_names: list[str] | None = None, + python_3_12_type_alias: bool = False, ) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]: """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). @@ -3747,6 +3748,7 @@ def analyze_alias( global_scope=global_scope, allowed_alias_tvars=tvar_defs, alias_type_params_names=all_declared_type_params_names, + python_3_12_type_alias=python_3_12_type_alias, ) # There can be only one variadic variable at most, the error is reported elsewhere. @@ -5321,6 +5323,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: allow_placeholder=True, declared_type_vars=type_params, all_declared_type_params_names=all_type_params_names, + python_3_12_type_alias=True, ) if not res: res = AnyType(TypeOfAny.from_error) @@ -5355,6 +5358,8 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: # so we need to replace it with non-explicit Anys. res = make_any_non_explicit(res) eager = self.is_func_scope() + if isinstance(res, ProperType) and isinstance(res, Instance) and not res.args: + fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options) alias_node = TypeAlias( res, self.qualified_name(s.name.name), diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 8f138ab5698f..bf53204ffce9 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -142,6 +142,7 @@ def analyze_type_alias( global_scope: bool = True, allowed_alias_tvars: list[TypeVarLikeType] | None = None, alias_type_params_names: list[str] | None = None, + python_3_12_type_alias: bool = False, ) -> tuple[Type, set[str]]: """Analyze r.h.s. of a (potential) type alias definition. @@ -160,6 +161,7 @@ def analyze_type_alias( prohibit_self_type="type alias target", allowed_alias_tvars=allowed_alias_tvars, alias_type_params_names=alias_type_params_names, + python_3_12_type_alias=python_3_12_type_alias, ) analyzer.in_dynamic_func = in_dynamic_func analyzer.global_scope = global_scope @@ -202,6 +204,7 @@ def __init__( is_typeshed_stub: bool, *, defining_alias: bool = False, + python_3_12_type_alias: bool = False, allow_tuple_literal: bool = False, allow_unbound_tvars: bool = False, allow_placeholder: bool = False, @@ -220,6 +223,7 @@ def __init__( self.tvar_scope = tvar_scope # Are we analysing a type alias definition rvalue? self.defining_alias = defining_alias + self.python_3_12_type_alias = python_3_12_type_alias self.allow_tuple_literal = allow_tuple_literal # Positive if we are analyzing arguments of another (outer) type self.nesting_level = 0 @@ -364,7 +368,12 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) and (tvar_def is None or tvar_def not in self.allowed_alias_tvars) ): if self.not_declared_in_type_params(t.name): - msg = f'Type variable "{t.name}" is not included in type_params' + if self.python_3_12_type_alias: + msg = message_registry.TYPE_PARAMETERS_SHOULD_BE_DECLARED.format( + f'"{t.name}"' + ) + else: + msg = f'Type variable "{t.name}" is not included in type_params' else: msg = f'Can\'t use bound type variable "{t.name}" to define generic alias' self.fail(msg, t, code=codes.VALID_TYPE) @@ -393,7 +402,12 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) if self.allow_unbound_tvars: return t if self.defining_alias and self.not_declared_in_type_params(t.name): - msg = f'TypeVarTuple "{t.name}" is not included in type_params' + if self.python_3_12_type_alias: + msg = message_registry.TYPE_PARAMETERS_SHOULD_BE_DECLARED.format( + f'"{t.name}"' + ) + else: + msg = f'TypeVarTuple "{t.name}" is not included in type_params' else: msg = f'TypeVarTuple "{t.name}" is unbound' self.fail(msg, t, code=codes.VALID_TYPE) @@ -1309,11 +1323,13 @@ def analyze_callable_args_for_paramspec( and self.not_declared_in_type_params(tvar_def.name) and tvar_def not in self.allowed_alias_tvars ): - self.fail( - f'ParamSpec "{tvar_def.name}" is not included in type_params', - callable_args, - code=codes.VALID_TYPE, - ) + if self.python_3_12_type_alias: + msg = message_registry.TYPE_PARAMETERS_SHOULD_BE_DECLARED.format( + f'"{tvar_def.name}"' + ) + else: + msg = f'ParamSpec "{tvar_def.name}" is not included in type_params' + self.fail(msg, callable_args, code=codes.VALID_TYPE) return callable_with_ellipsis( AnyType(TypeOfAny.special_form), ret_type=ret_type, fallback=fallback ) diff --git a/test-data/unit/check-python312.test b/test-data/unit/check-python312.test index 6dd61351d7a8..8443aadb6905 100644 --- a/test-data/unit/check-python312.test +++ b/test-data/unit/check-python312.test @@ -1244,3 +1244,72 @@ class C[T]: class D[T](C[S]): # E: All type parameters should be declared ("S" not declared) pass + +[case testPEP695MixNewAndOldStyleTypeVarTupleAndParamSpec] +# mypy: enable-incomplete-feature=NewGenericSyntax +from typing import TypeVarTuple, ParamSpec, Callable +Ts = TypeVarTuple("Ts") +P = ParamSpec("P") + +def f[T](x: T, f: Callable[P, None] # E: All type parameters should be declared ("P" not declared) + ) -> Callable[P, T]: ... +def g[T](x: T, f: tuple[*Ts] # E: All type parameters should be declared ("Ts" not declared) + ) -> tuple[T, *Ts]: ... +[builtins fixtures/tuple.pyi] + +[case testPEP695MixNewAndOldStyleGenericsInTypeAlias] +# mypy: enable-incomplete-feature=NewGenericSyntax +from typing import TypeVar, ParamSpec, TypeVarTuple, Callable + +T = TypeVar("T") +Ts = TypeVarTuple("Ts") +P = ParamSpec("P") + +type A = list[T] # E: All type parameters should be declared ("T" not declared) +a: A[int] # E: Bad number of arguments for type alias, expected 0, given 1 +reveal_type(a) # N: Revealed type is "builtins.list[Any]" + +type B = tuple[*Ts] # E: All type parameters should be declared ("Ts" not declared) +type C = Callable[P, None] # E: All type parameters should be declared ("P" not declared) +[builtins fixtures/tuple.pyi] + +[case testPEP695NonGenericAliasToGenericClass] +# mypy: enable-incomplete-feature=NewGenericSyntax +class C[T]: pass +type A = C +x: C +y: A +reveal_type(x) # N: Revealed type is "__main__.C[Any]" +reveal_type(y) # N: Revealed type is "__main__.C[Any]" +z: A[int] # E: Bad number of arguments for type alias, expected 0, given 1 + +[case testPEP695SelfType] +# mypy: enable-incomplete-feature=NewGenericSyntax +from typing import Self + +class C: + @classmethod + def m[T](cls, x: T) -> tuple[Self, T]: + return cls(), x + +class D(C): + pass + +reveal_type(C.m(1)) # N: Revealed type is "Tuple[__main__.C, builtins.int]" +reveal_type(D.m(1)) # N: Revealed type is "Tuple[__main__.D, builtins.int]" + +class E[T]: + def m(self) -> Self: + return self + + def mm[S](self, x: S) -> tuple[Self, S]: + return self, x + +class F[T](E[T]): + pass + +reveal_type(E[int]().m()) # N: Revealed type is "__main__.E[builtins.int]" +reveal_type(E[int]().mm(b'x')) # N: Revealed type is "Tuple[__main__.E[builtins.int], builtins.bytes]" +reveal_type(F[str]().m()) # N: Revealed type is "__main__.F[builtins.str]" +reveal_type(F[str]().mm(b'x')) # N: Revealed type is "Tuple[__main__.F[builtins.str], builtins.bytes]" +[builtins fixtures/tuple.pyi]