Skip to content

Commit 89c032a

Browse files
committed
Generalize X[()] support for Unpack with empty tuples
1 parent 7b4f862 commit 89c032a

File tree

9 files changed

+61
-36
lines changed

9 files changed

+61
-36
lines changed

mypy/checkexpr.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4896,7 +4896,7 @@ class C(Generic[T, Unpack[Ts]]): ...
48964896
# This code can be only called either from checking a type application, or from
48974897
# checking a type alias (after the caller handles no_args aliases), so we know it
48984898
# was initially an IndexExpr, and we allow empty tuple type arguments.
4899-
if not validate_instance(fake, self.chk.fail, empty_tuple_index=True):
4899+
if not validate_instance(fake, self.chk.fail, has_parameters=True):
49004900
fix_instance(
49014901
fake, self.chk.fail, self.chk.note, disallow_any=False, options=self.chk.options
49024902
)

mypy/exprtotype.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ def expr_to_unanalyzed_type(
129129
expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr, allow_unpack=True)
130130
for arg in args
131131
)
132-
if not base.args:
133-
base.empty_tuple_index = True
132+
base.has_parameters = True
134133
return base
135134
else:
136135
raise TypeTranslationError()

mypy/fastparse.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,22 +2063,15 @@ def visit_Slice(self, n: ast3.Slice) -> Type:
20632063

20642064
# Subscript(expr value, expr slice, expr_context ctx) # Python 3.9 and later
20652065
def visit_Subscript(self, n: ast3.Subscript) -> Type:
2066-
empty_tuple_index = False
20672066
if isinstance(n.slice, ast3.Tuple):
20682067
params = self.translate_expr_list(n.slice.elts)
2069-
if len(n.slice.elts) == 0:
2070-
empty_tuple_index = True
20712068
else:
20722069
params = [self.visit(n.slice)]
20732070

20742071
value = self.visit(n.value)
20752072
if isinstance(value, UnboundType) and not value.args:
20762073
return UnboundType(
2077-
value.name,
2078-
params,
2079-
line=self.line,
2080-
column=value.column,
2081-
empty_tuple_index=empty_tuple_index,
2074+
value.name, params, line=self.line, column=value.column, has_parameters=True
20822075
)
20832076
else:
20842077
return self.invalid_type(n)

mypy/semanal.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3933,8 +3933,8 @@ def analyze_alias(
39333933
new_tvar_defs.append(td)
39343934

39353935
qualified_tvars = [node.fullname for _name, node in alias_type_vars]
3936-
empty_tuple_index = typ.empty_tuple_index if isinstance(typ, UnboundType) else False
3937-
return analyzed, new_tvar_defs, depends_on, qualified_tvars, empty_tuple_index
3936+
has_parameters = typ.has_parameters if isinstance(typ, UnboundType) else False
3937+
return analyzed, new_tvar_defs, depends_on, qualified_tvars, has_parameters
39383938

39393939
def is_pep_613(self, s: AssignmentStmt) -> bool:
39403940
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:
40304030
alias_tvars: list[TypeVarLikeType] = []
40314031
depends_on: set[str] = set()
40324032
qualified_tvars: list[str] = []
4033-
empty_tuple_index = False
4033+
has_parameters = False
40344034
else:
40354035
tag = self.track_incomplete_refs()
4036-
res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias(
4036+
res, alias_tvars, depends_on, qualified_tvars, has_parameters = self.analyze_alias(
40374037
lvalue.name,
40384038
rvalue,
40394039
allow_placeholder=True,
@@ -4080,12 +4080,12 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
40804080
isinstance(res, ProperType)
40814081
and isinstance(res, Instance)
40824082
and not res.args
4083-
and not empty_tuple_index
4083+
and not (has_parameters and res.type.type_vars)
40844084
and not pep_695
40854085
and not pep_613
40864086
)
40874087
if isinstance(res, ProperType) and isinstance(res, Instance):
4088-
if not validate_instance(res, self.fail, empty_tuple_index):
4088+
if not validate_instance(res, self.fail, has_parameters):
40894089
fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options)
40904090
# Aliases defined within functions can't be accessed outside
40914091
# the function, since the symbol table will no longer
@@ -5550,7 +5550,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
55505550
return
55515551

55525552
tag = self.track_incomplete_refs()
5553-
res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias(
5553+
res, alias_tvars, depends_on, qualified_tvars, has_parameters = self.analyze_alias(
55545554
s.name.name,
55555555
s.value.expr(),
55565556
allow_placeholder=True,

mypy/server/astdiff.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ def visit_unbound_type(self, typ: UnboundType) -> SnapshotItem:
363363
"UnboundType",
364364
typ.name,
365365
typ.optional,
366-
typ.empty_tuple_index,
366+
typ.has_parameters,
367367
snapshot_types(typ.args),
368368
)
369369

mypy/stubutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ def visit_unbound_type(self, t: UnboundType) -> str:
279279
self.stubgen.import_tracker.require_name(s)
280280
if t.args:
281281
s += f"[{self.args_str(t.args)}]"
282-
elif t.empty_tuple_index:
282+
elif t.has_parameters:
283283
s += "[()]"
284284
return s
285285

mypy/typeanal.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -483,15 +483,15 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
483483
self.options,
484484
unexpanded_type=t,
485485
disallow_any=disallow_any,
486-
empty_tuple_index=t.empty_tuple_index,
486+
has_parameters=t.has_parameters,
487487
)
488488
# The only case where instantiate_type_alias() can return an incorrect instance is
489489
# when it is top-level instance, so no need to recurse.
490490
if (
491491
isinstance(res, ProperType)
492492
and isinstance(res, Instance)
493493
and not (self.defining_alias and self.nesting_level == 0)
494-
and not validate_instance(res, self.fail, t.empty_tuple_index)
494+
and not validate_instance(res, self.fail, t.has_parameters)
495495
):
496496
fix_instance(
497497
res,
@@ -506,7 +506,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
506506
res = get_proper_type(res)
507507
return res
508508
elif isinstance(node, TypeInfo):
509-
return self.analyze_type_with_type_info(node, t.args, t, t.empty_tuple_index)
509+
return self.analyze_type_with_type_info(node, t.args, t, t.has_parameters)
510510
elif node.fullname in TYPE_ALIAS_NAMES:
511511
return AnyType(TypeOfAny.special_form)
512512
# 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
629629
else:
630630
self.fail('Name "tuple" is not defined', t)
631631
return AnyType(TypeOfAny.special_form)
632-
if len(t.args) == 0 and not t.empty_tuple_index:
632+
if len(t.args) == 0 and not t.has_parameters:
633633
# Bare 'Tuple' is same as 'tuple'
634634
any_type = self.get_omitted_any(t)
635635
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:
815815
warn(deprecated, ctx, code=codes.DEPRECATED)
816816

817817
def analyze_type_with_type_info(
818-
self, info: TypeInfo, args: Sequence[Type], ctx: Context, empty_tuple_index: bool
818+
self, info: TypeInfo, args: Sequence[Type], ctx: Context, has_parameters: bool
819819
) -> Type:
820820
"""Bind unbound type when were able to find target TypeInfo.
821821
@@ -853,7 +853,7 @@ def analyze_type_with_type_info(
853853
# Check type argument count.
854854
instance.args = tuple(flatten_nested_tuples(instance.args))
855855
if not (self.defining_alias and self.nesting_level == 0) and not validate_instance(
856-
instance, self.fail, empty_tuple_index
856+
instance, self.fail, has_parameters
857857
):
858858
fix_instance(
859859
instance,
@@ -2121,7 +2121,7 @@ def instantiate_type_alias(
21212121
unexpanded_type: Type | None = None,
21222122
disallow_any: bool = False,
21232123
use_standard_error: bool = False,
2124-
empty_tuple_index: bool = False,
2124+
has_parameters: bool = False,
21252125
) -> Type:
21262126
"""Create an instance of a (generic) type alias from alias node and type arguments.
21272127
@@ -2149,7 +2149,7 @@ def instantiate_type_alias(
21492149
if (
21502150
max_tv_count > 0
21512151
and act_len == 0
2152-
and not (empty_tuple_index and node.tvar_tuple_index is not None)
2152+
and not (has_parameters and node.tvar_tuple_index is not None)
21532153
):
21542154
# Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...]
21552155
return set_any_tvars(
@@ -2466,7 +2466,7 @@ def make_optional_type(t: Type) -> Type:
24662466
return UnionType([t, NoneType()], t.line, t.column)
24672467

24682468

2469-
def validate_instance(t: Instance, fail: MsgCallback, empty_tuple_index: bool) -> bool:
2469+
def validate_instance(t: Instance, fail: MsgCallback, has_parameters: bool) -> bool:
24702470
"""Check if this is a well-formed instance with respect to argument count/positions."""
24712471
# TODO: combine logic with instantiate_type_alias().
24722472
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) -
24852485
):
24862486
correct = True
24872487
if not t.args:
2488-
if not (empty_tuple_index and len(t.type.type_vars) == 1):
2488+
if not (has_parameters and len(t.type.type_vars) == 1):
24892489
# The Any arguments should be set by the caller.
2490-
if empty_tuple_index and min_tv_count:
2490+
if has_parameters and min_tv_count:
24912491
fail(
24922492
f"At least {min_tv_count} type argument(s) expected, none given",
24932493
t,

mypy/types.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -916,7 +916,7 @@ class UnboundType(ProperType):
916916
"name",
917917
"args",
918918
"optional",
919-
"empty_tuple_index",
919+
"has_parameters",
920920
"original_str_expr",
921921
"original_str_fallback",
922922
)
@@ -928,7 +928,7 @@ def __init__(
928928
line: int = -1,
929929
column: int = -1,
930930
optional: bool = False,
931-
empty_tuple_index: bool = False,
931+
has_parameters: bool = False,
932932
original_str_expr: str | None = None,
933933
original_str_fallback: str | None = None,
934934
) -> None:
@@ -939,8 +939,8 @@ def __init__(
939939
self.args = tuple(args)
940940
# Should this type be wrapped in an Optional?
941941
self.optional = optional
942-
# Special case for X[()]
943-
self.empty_tuple_index = empty_tuple_index
942+
# Distinguish between X[()] and X
943+
self.has_parameters = has_parameters
944944
# If this UnboundType was originally defined as a str or bytes, keep track of
945945
# the original contents of that string-like thing. This way, if this UnboundExpr
946946
# 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
966966
line=self.line,
967967
column=self.column,
968968
optional=self.optional,
969-
empty_tuple_index=self.empty_tuple_index,
969+
has_parameters=self.has_parameters,
970970
original_str_expr=self.original_str_expr,
971971
original_str_fallback=self.original_str_fallback,
972972
)

test-data/unit/check-generics.test

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3548,3 +3548,36 @@ def foo(x: T):
35483548
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]])"
35493549
reveal_type(C(0, x)) # N: Revealed type is "__main__.C[__main__.Int[T`-1]]"
35503550
reveal_type(C("yes", x)) # N: Revealed type is "__main__.C[__main__.Str[T`-1]]"
3551+
3552+
[case testCanUnpackEmptyTuple]
3553+
# flags: --disallow-any-generics
3554+
from typing import Generic, Unpack, Tuple
3555+
from typing_extensions import TypeVarTuple
3556+
3557+
Ts = TypeVarTuple("Ts")
3558+
class X(Generic[Unpack[Ts]]): ...
3559+
3560+
def check(
3561+
x: X[()], # works
3562+
y: X[Unpack[Tuple[()]]], # works
3563+
z: X[Unpack[Tuple[Unpack[Tuple[()]]]]], # works
3564+
): ...
3565+
[builtins fixtures/tuple.pyi]
3566+
3567+
[case testNoGenericInAlias]
3568+
# flags: --disallow-any-generics
3569+
from typing import Generic, Unpack, Union
3570+
from typing_extensions import TypeVarTuple, TypeAlias
3571+
3572+
Ts = TypeVarTuple("Ts")
3573+
class X(Generic[Unpack[Ts]]): ...
3574+
3575+
Alias1 = X
3576+
reveal_type(Alias1) # N: Revealed type is "def [Ts] () -> __main__.X[Unpack[Ts`1]]"
3577+
3578+
Alias2 = Union[X] # E: Missing type parameters for generic type "X[()]"
3579+
reveal_type(Alias2) # N: Revealed type is "def () -> __main__.X[Unpack[builtins.tuple[Any, ...]]]"
3580+
3581+
Alias3 = Union[X[()]]
3582+
reveal_type(Alias3) # N: Revealed type is "def () -> __main__.X[()]"
3583+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)