Skip to content

Commit 359ad93

Browse files
authored
Model tuple type aliases better (#20967)
Fixes #17133 (surprisingly has four upvotes, but maybe people are mistriaging, e.g. the other comment in #17133 is something else that I fixed in #20872)
1 parent 656b09a commit 359ad93

File tree

6 files changed

+57
-11
lines changed

6 files changed

+57
-11
lines changed

mypy/checkexpr.py

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

mypy/semanal.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4037,8 +4037,8 @@ def analyze_alias(
40374037
variadic = True
40384038
new_tvar_defs.append(td)
40394039

4040-
empty_tuple_index = typ.empty_tuple_index if isinstance(typ, UnboundType) else False
4041-
return analyzed, new_tvar_defs, depends_on, empty_tuple_index
4040+
indexed = bool(isinstance(typ, UnboundType) and (typ.args or typ.empty_tuple_index))
4041+
return analyzed, new_tvar_defs, depends_on, indexed
40424042

40434043
def is_pep_613(self, s: AssignmentStmt) -> bool:
40444044
if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType):
@@ -4137,10 +4137,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
41374137
res = NoneType()
41384138
alias_tvars: list[TypeVarLikeType] = []
41394139
depends_on: set[str] = set()
4140-
empty_tuple_index = False
4140+
indexed = False
41414141
else:
41424142
tag = self.track_incomplete_refs()
4143-
res, alias_tvars, depends_on, empty_tuple_index = self.analyze_alias(
4143+
res, alias_tvars, depends_on, indexed = self.analyze_alias(
41444144
lvalue.name,
41454145
rvalue,
41464146
allow_placeholder=True,
@@ -4180,12 +4180,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
41804180
no_args = (
41814181
isinstance(res, ProperType)
41824182
and isinstance(res, Instance)
4183-
and not res.args
4184-
and not empty_tuple_index
41854183
and not pep_695
4184+
and not indexed
41864185
)
41874186
if isinstance(res, ProperType) and isinstance(res, Instance):
4188-
if not validate_instance(res, self.fail, empty_tuple_index):
4187+
if not validate_instance(res, self.fail, indexed):
41894188
fix_instance(res, self.fail, self.note, disallow_any=False, options=self.options)
41904189
# Aliases defined within functions can't be accessed outside
41914190
# the function, since the symbol table will no longer
@@ -5689,7 +5688,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
56895688
return
56905689

56915690
tag = self.track_incomplete_refs()
5692-
res, alias_tvars, depends_on, empty_tuple_index = self.analyze_alias(
5691+
res, alias_tvars, depends_on, indexed = self.analyze_alias(
56935692
s.name.name,
56945693
s.value.expr(),
56955694
allow_placeholder=True,

mypy/typeanal.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2160,6 +2160,15 @@ def instantiate_type_alias(
21602160
# Note that we keep the kind of Any for consistency.
21612161
return set_any_tvars(node, [], ctx.line, ctx.column, options, special_form=True)
21622162

2163+
if (
2164+
no_args
2165+
and isinstance(node.target, ProperType)
2166+
and isinstance(node.target, Instance)
2167+
and node.target.type.fullname == "builtins.tuple"
2168+
and len(args)
2169+
):
2170+
no_args = False
2171+
21632172
max_tv_count = len(node.alias_tvars)
21642173
act_len = len(args)
21652174
if (
@@ -2480,13 +2489,14 @@ def make_optional_type(t: Type) -> Type:
24802489
return UnionType([t, NoneType()], t.line, t.column)
24812490

24822491

2483-
def validate_instance(t: Instance, fail: MsgCallback, empty_tuple_index: bool) -> bool:
2492+
def validate_instance(t: Instance, fail: MsgCallback, indexed: bool) -> bool:
24842493
"""Check if this is a well-formed instance with respect to argument count/positions."""
24852494
# TODO: combine logic with instantiate_type_alias().
24862495
if any(unknown_unpack(a) for a in t.args):
24872496
# This type is not ready to be validated, because of unknown total count.
24882497
# TODO: is it OK to fill with TypeOfAny.from_error instead of special form?
24892498
return False
2499+
empty_tuple_index = indexed and not t.args
24902500
if t.type.has_type_var_tuple_type:
24912501
min_tv_count = sum(
24922502
not tv.has_default() and not isinstance(tv, TypeVarTupleType)

mypy/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,7 @@ def __init__(
10431043
self,
10441044
name: str,
10451045
args: Sequence[Type] | None = None,
1046+
*,
10461047
line: int = -1,
10471048
column: int = -1,
10481049
optional: bool = False,

test-data/unit/check-generics.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,7 @@ y: U
10021002
reveal_type(x) # N: Revealed type is "builtins.int | None"
10031003
reveal_type(y) # N: Revealed type is "builtins.int"
10041004

1005-
U[int] # E: Type application targets a non-generic function or class
1005+
U[int] # E: Bad number of arguments for type alias, expected 0, given 1
10061006
O[int] # E: Bad number of arguments for type alias, expected 0, given 1 \
10071007
# E: Type application is only supported for generic classes
10081008

test-data/unit/check-python310.test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,6 +1402,42 @@ def print_test(m: object, typ: type[T]) -> T:
14021402
reveal_type(m) # N: Revealed type is "builtins.object"
14031403
raise
14041404

1405+
[case testMatchClassPatternTupleAlias]
1406+
# flags: --strict-equality --warn-unreachable
1407+
from typing import Tuple
1408+
1409+
tuple_alias = tuple
1410+
Tuple_alias = Tuple
1411+
1412+
def main(m: object):
1413+
match m:
1414+
case tuple():
1415+
reveal_type(m) # N: Revealed type is "builtins.tuple[Any, ...]"
1416+
case _:
1417+
reveal_type(m) # N: Revealed type is "builtins.object"
1418+
1419+
match m:
1420+
case tuple_alias():
1421+
reveal_type(m) # N: Revealed type is "builtins.tuple[Any, ...]"
1422+
case _:
1423+
reveal_type(m) # N: Revealed type is "builtins.object"
1424+
1425+
match m:
1426+
# With real typeshed you'll get an error like this instead:
1427+
# Expected type in class pattern; found "typing._SpecialForm"
1428+
case Tuple(): # E: Expected type in class pattern; found "builtins.int"
1429+
reveal_type(m) # E: Statement is unreachable
1430+
case _:
1431+
reveal_type(m) # N: Revealed type is "builtins.object"
1432+
1433+
match m:
1434+
# This is a false negative, it will raise at runtime
1435+
case Tuple_alias():
1436+
reveal_type(m) # N: Revealed type is "builtins.tuple[Any, ...]"
1437+
case _:
1438+
reveal_type(m) # N: Revealed type is "builtins.object"
1439+
[builtins fixtures/tuple.pyi]
1440+
14051441
[case testMatchNonFinalMatchArgs]
14061442
class A:
14071443
__match_args__ = ("a", "b")

0 commit comments

Comments
 (0)