From 89c032a4039255dc430a61a922670027eab0268e Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 1 Jan 2025 05:01:55 +0900 Subject: [PATCH] Generalize X[()] support for Unpack with empty tuples --- mypy/checkexpr.py | 2 +- mypy/exprtotype.py | 3 +-- mypy/fastparse.py | 9 +------- mypy/semanal.py | 14 ++++++------- mypy/server/astdiff.py | 2 +- mypy/stubutil.py | 2 +- mypy/typeanal.py | 22 ++++++++++---------- mypy/types.py | 10 ++++----- test-data/unit/check-generics.test | 33 ++++++++++++++++++++++++++++++ 9 files changed, 61 insertions(+), 36 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b6618109bb44..f50fde10b8d2 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4896,7 +4896,7 @@ class C(Generic[T, Unpack[Ts]]): ... # This code can be only called either from checking a type application, or from # checking a type alias (after the caller handles no_args aliases), so we know it # was initially an IndexExpr, and we allow empty tuple type arguments. - if not validate_instance(fake, self.chk.fail, empty_tuple_index=True): + if not validate_instance(fake, self.chk.fail, has_parameters=True): fix_instance( fake, self.chk.fail, self.chk.note, disallow_any=False, options=self.chk.options ) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 506194a4b285..74ce9eec0611 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -129,8 +129,7 @@ def expr_to_unanalyzed_type( expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr, allow_unpack=True) for arg in args ) - if not base.args: - base.empty_tuple_index = True + base.has_parameters = True return base else: raise TypeTranslationError() diff --git a/mypy/fastparse.py b/mypy/fastparse.py index 6985fd567402..b32e3e3408a0 100644 --- a/mypy/fastparse.py +++ b/mypy/fastparse.py @@ -2063,22 +2063,15 @@ def visit_Slice(self, n: ast3.Slice) -> Type: # Subscript(expr value, expr slice, expr_context ctx) # Python 3.9 and later def visit_Subscript(self, n: ast3.Subscript) -> Type: - empty_tuple_index = False if isinstance(n.slice, ast3.Tuple): params = self.translate_expr_list(n.slice.elts) - if len(n.slice.elts) == 0: - empty_tuple_index = True else: params = [self.visit(n.slice)] value = self.visit(n.value) if isinstance(value, UnboundType) and not value.args: return UnboundType( - value.name, - params, - line=self.line, - column=value.column, - empty_tuple_index=empty_tuple_index, + value.name, params, line=self.line, column=value.column, has_parameters=True ) else: return self.invalid_type(n) diff --git a/mypy/semanal.py b/mypy/semanal.py index 8335f91c4d3b..a5c1f2d7e6ab 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3933,8 +3933,8 @@ def analyze_alias( new_tvar_defs.append(td) qualified_tvars = [node.fullname for _name, node in alias_type_vars] - empty_tuple_index = typ.empty_tuple_index if isinstance(typ, UnboundType) else False - return analyzed, new_tvar_defs, depends_on, qualified_tvars, empty_tuple_index + has_parameters = typ.has_parameters if isinstance(typ, UnboundType) else False + return analyzed, new_tvar_defs, depends_on, qualified_tvars, has_parameters def is_pep_613(self, s: AssignmentStmt) -> bool: if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): @@ -4030,10 +4030,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: alias_tvars: list[TypeVarLikeType] = [] depends_on: set[str] = set() qualified_tvars: list[str] = [] - empty_tuple_index = False + has_parameters = False else: tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias( + res, alias_tvars, depends_on, qualified_tvars, has_parameters = self.analyze_alias( lvalue.name, rvalue, allow_placeholder=True, @@ -4080,12 +4080,12 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: isinstance(res, ProperType) and isinstance(res, Instance) and not res.args - and not empty_tuple_index + and not (has_parameters and res.type.type_vars) and not pep_695 and not pep_613 ) if isinstance(res, ProperType) and isinstance(res, Instance): - if not validate_instance(res, self.fail, empty_tuple_index): + if not validate_instance(res, self.fail, has_parameters): fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options) # Aliases defined within functions can't be accessed outside # the function, since the symbol table will no longer @@ -5550,7 +5550,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: return tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias( + res, alias_tvars, depends_on, qualified_tvars, has_parameters = self.analyze_alias( s.name.name, s.value.expr(), allow_placeholder=True, diff --git a/mypy/server/astdiff.py b/mypy/server/astdiff.py index f91687823841..3e0b1566c793 100644 --- a/mypy/server/astdiff.py +++ b/mypy/server/astdiff.py @@ -363,7 +363,7 @@ def visit_unbound_type(self, typ: UnboundType) -> SnapshotItem: "UnboundType", typ.name, typ.optional, - typ.empty_tuple_index, + typ.has_parameters, snapshot_types(typ.args), ) diff --git a/mypy/stubutil.py b/mypy/stubutil.py index cbb3d2f77414..05159f9dd3aa 100644 --- a/mypy/stubutil.py +++ b/mypy/stubutil.py @@ -279,7 +279,7 @@ def visit_unbound_type(self, t: UnboundType) -> str: self.stubgen.import_tracker.require_name(s) if t.args: s += f"[{self.args_str(t.args)}]" - elif t.empty_tuple_index: + elif t.has_parameters: s += "[()]" return s diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7de987a83a2b..fbba9d096780 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -483,7 +483,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) self.options, unexpanded_type=t, disallow_any=disallow_any, - empty_tuple_index=t.empty_tuple_index, + has_parameters=t.has_parameters, ) # The only case where instantiate_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. @@ -491,7 +491,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) isinstance(res, ProperType) and isinstance(res, Instance) and not (self.defining_alias and self.nesting_level == 0) - and not validate_instance(res, self.fail, t.empty_tuple_index) + and not validate_instance(res, self.fail, t.has_parameters) ): fix_instance( res, @@ -506,7 +506,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) res = get_proper_type(res) return res elif isinstance(node, TypeInfo): - return self.analyze_type_with_type_info(node, t.args, t, t.empty_tuple_index) + return self.analyze_type_with_type_info(node, t.args, t, t.has_parameters) elif node.fullname in TYPE_ALIAS_NAMES: return AnyType(TypeOfAny.special_form) # Concatenate is an operator, no need for a proper type @@ -629,7 +629,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Typ else: self.fail('Name "tuple" is not defined', t) return AnyType(TypeOfAny.special_form) - if len(t.args) == 0 and not t.empty_tuple_index: + if len(t.args) == 0 and not t.has_parameters: # Bare 'Tuple' is same as 'tuple' any_type = self.get_omitted_any(t) return self.named_type("builtins.tuple", [any_type], line=t.line, column=t.column) @@ -815,7 +815,7 @@ def check_and_warn_deprecated(self, info: TypeInfo, ctx: Context) -> None: warn(deprecated, ctx, code=codes.DEPRECATED) def analyze_type_with_type_info( - self, info: TypeInfo, args: Sequence[Type], ctx: Context, empty_tuple_index: bool + self, info: TypeInfo, args: Sequence[Type], ctx: Context, has_parameters: bool ) -> Type: """Bind unbound type when were able to find target TypeInfo. @@ -853,7 +853,7 @@ def analyze_type_with_type_info( # Check type argument count. instance.args = tuple(flatten_nested_tuples(instance.args)) if not (self.defining_alias and self.nesting_level == 0) and not validate_instance( - instance, self.fail, empty_tuple_index + instance, self.fail, has_parameters ): fix_instance( instance, @@ -2121,7 +2121,7 @@ def instantiate_type_alias( unexpanded_type: Type | None = None, disallow_any: bool = False, use_standard_error: bool = False, - empty_tuple_index: bool = False, + has_parameters: bool = False, ) -> Type: """Create an instance of a (generic) type alias from alias node and type arguments. @@ -2149,7 +2149,7 @@ def instantiate_type_alias( if ( max_tv_count > 0 and act_len == 0 - and not (empty_tuple_index and node.tvar_tuple_index is not None) + and not (has_parameters and node.tvar_tuple_index is not None) ): # Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...] return set_any_tvars( @@ -2466,7 +2466,7 @@ def make_optional_type(t: Type) -> Type: return UnionType([t, NoneType()], t.line, t.column) -def validate_instance(t: Instance, fail: MsgCallback, empty_tuple_index: bool) -> bool: +def validate_instance(t: Instance, fail: MsgCallback, has_parameters: bool) -> bool: """Check if this is a well-formed instance with respect to argument count/positions.""" # TODO: combine logic with instantiate_type_alias(). if any(unknown_unpack(a) for a in t.args): @@ -2485,9 +2485,9 @@ def validate_instance(t: Instance, fail: MsgCallback, empty_tuple_index: bool) - ): correct = True if not t.args: - if not (empty_tuple_index and len(t.type.type_vars) == 1): + if not (has_parameters and len(t.type.type_vars) == 1): # The Any arguments should be set by the caller. - if empty_tuple_index and min_tv_count: + if has_parameters and min_tv_count: fail( f"At least {min_tv_count} type argument(s) expected, none given", t, diff --git a/mypy/types.py b/mypy/types.py index f3745695889f..8fee42f7fde0 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -916,7 +916,7 @@ class UnboundType(ProperType): "name", "args", "optional", - "empty_tuple_index", + "has_parameters", "original_str_expr", "original_str_fallback", ) @@ -928,7 +928,7 @@ def __init__( line: int = -1, column: int = -1, optional: bool = False, - empty_tuple_index: bool = False, + has_parameters: bool = False, original_str_expr: str | None = None, original_str_fallback: str | None = None, ) -> None: @@ -939,8 +939,8 @@ def __init__( self.args = tuple(args) # Should this type be wrapped in an Optional? self.optional = optional - # Special case for X[()] - self.empty_tuple_index = empty_tuple_index + # Distinguish between X[()] and X + self.has_parameters = has_parameters # If this UnboundType was originally defined as a str or bytes, keep track of # the original contents of that string-like thing. This way, if this UnboundExpr # ever shows up inside of a LiteralType, we can determine whether that @@ -966,7 +966,7 @@ def copy_modified(self, args: Bogus[Sequence[Type] | None] = _dummy) -> UnboundT line=self.line, column=self.column, optional=self.optional, - empty_tuple_index=self.empty_tuple_index, + has_parameters=self.has_parameters, original_str_expr=self.original_str_expr, original_str_fallback=self.original_str_fallback, ) diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 5d6ad8e19631..d02847bdcc69 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -3548,3 +3548,36 @@ def foo(x: T): reveal_type(C) # N: Revealed type is "Overload(def [T, S] (x: builtins.int, y: S`-1) -> __main__.C[__main__.Int[S`-1]], def [T, S] (x: builtins.str, y: S`-1) -> __main__.C[__main__.Str[S`-1]])" reveal_type(C(0, x)) # N: Revealed type is "__main__.C[__main__.Int[T`-1]]" reveal_type(C("yes", x)) # N: Revealed type is "__main__.C[__main__.Str[T`-1]]" + +[case testCanUnpackEmptyTuple] +# flags: --disallow-any-generics +from typing import Generic, Unpack, Tuple +from typing_extensions import TypeVarTuple + +Ts = TypeVarTuple("Ts") +class X(Generic[Unpack[Ts]]): ... + +def check( + x: X[()], # works + y: X[Unpack[Tuple[()]]], # works + z: X[Unpack[Tuple[Unpack[Tuple[()]]]]], # works +): ... +[builtins fixtures/tuple.pyi] + +[case testNoGenericInAlias] +# flags: --disallow-any-generics +from typing import Generic, Unpack, Union +from typing_extensions import TypeVarTuple, TypeAlias + +Ts = TypeVarTuple("Ts") +class X(Generic[Unpack[Ts]]): ... + +Alias1 = X +reveal_type(Alias1) # N: Revealed type is "def [Ts] () -> __main__.X[Unpack[Ts`1]]" + +Alias2 = Union[X] # E: Missing type parameters for generic type "X[()]" +reveal_type(Alias2) # N: Revealed type is "def () -> __main__.X[Unpack[builtins.tuple[Any, ...]]]" + +Alias3 = Union[X[()]] +reveal_type(Alias3) # N: Revealed type is "def () -> __main__.X[()]" +[builtins fixtures/tuple.pyi]