From edeb8cfe3757ab6c777a0c5f18d46a345d91fc2e Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Mon, 23 Feb 2026 01:45:20 -0800 Subject: [PATCH] Model tuple type aliases better --- mypy/checkexpr.py | 2 +- mypy/semanal.py | 15 ++++++------ mypy/typeanal.py | 12 +++++++++- mypy/types.py | 1 + test-data/unit/check-generics.test | 2 +- test-data/unit/check-python310.test | 36 +++++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 11 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index d5b79b123c20..45ec5fc72f55 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -4975,7 +4975,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, indexed=True): fix_instance( fake, self.chk.fail, self.chk.note, disallow_any=False, options=self.chk.options ) diff --git a/mypy/semanal.py b/mypy/semanal.py index 6d4140f68719..1441f2a0f2cd 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -4035,8 +4035,8 @@ def analyze_alias( variadic = True new_tvar_defs.append(td) - empty_tuple_index = typ.empty_tuple_index if isinstance(typ, UnboundType) else False - return analyzed, new_tvar_defs, depends_on, empty_tuple_index + indexed = bool(isinstance(typ, UnboundType) and (typ.args or typ.empty_tuple_index)) + return analyzed, new_tvar_defs, depends_on, indexed def is_pep_613(self, s: AssignmentStmt) -> bool: if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): @@ -4135,10 +4135,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: res = NoneType() alias_tvars: list[TypeVarLikeType] = [] depends_on: set[str] = set() - empty_tuple_index = False + indexed = False else: tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, empty_tuple_index = self.analyze_alias( + res, alias_tvars, depends_on, indexed = self.analyze_alias( lvalue.name, rvalue, allow_placeholder=True, @@ -4178,12 +4178,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: no_args = ( isinstance(res, ProperType) and isinstance(res, Instance) - and not res.args - and not empty_tuple_index and not pep_695 + and not indexed ) 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, indexed): 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 @@ -5668,7 +5667,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: return tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, empty_tuple_index = self.analyze_alias( + res, alias_tvars, depends_on, indexed = self.analyze_alias( s.name.name, s.value.expr(), allow_placeholder=True, diff --git a/mypy/typeanal.py b/mypy/typeanal.py index b07cd5342074..b22e1f80be59 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -2160,6 +2160,15 @@ def instantiate_type_alias( # Note that we keep the kind of Any for consistency. return set_any_tvars(node, [], ctx.line, ctx.column, options, special_form=True) + if ( + no_args + and isinstance(node.target, ProperType) + and isinstance(node.target, Instance) + and node.target.type.fullname == "builtins.tuple" + and len(args) + ): + no_args = False + max_tv_count = len(node.alias_tvars) act_len = len(args) if ( @@ -2480,13 +2489,14 @@ 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, indexed: 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): # This type is not ready to be validated, because of unknown total count. # TODO: is it OK to fill with TypeOfAny.from_error instead of special form? return False + empty_tuple_index = indexed and not t.args if t.type.has_type_var_tuple_type: min_tv_count = sum( not tv.has_default() and not isinstance(tv, TypeVarTupleType) diff --git a/mypy/types.py b/mypy/types.py index 32e52bf2d4b3..d4ed728f4c9b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1043,6 +1043,7 @@ def __init__( self, name: str, args: Sequence[Type] | None = None, + *, line: int = -1, column: int = -1, optional: bool = False, diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index 26d709850c1e..a3a5b02d54f8 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -1002,7 +1002,7 @@ y: U reveal_type(x) # N: Revealed type is "builtins.int | None" reveal_type(y) # N: Revealed type is "builtins.int" -U[int] # E: Type application targets a non-generic function or class +U[int] # E: Bad number of arguments for type alias, expected 0, given 1 O[int] # E: Bad number of arguments for type alias, expected 0, given 1 \ # E: Type application is only supported for generic classes diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 466b46686831..962999f6d96d 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1402,6 +1402,42 @@ def print_test(m: object, typ: type[T]) -> T: reveal_type(m) # N: Revealed type is "builtins.object" raise +[case testMatchClassPatternTupleAlias] +# flags: --strict-equality --warn-unreachable +from typing import Tuple + +tuple_alias = tuple +Tuple_alias = Tuple + +def main(m: object): + match m: + case tuple(): + reveal_type(m) # N: Revealed type is "builtins.tuple[Any, ...]" + case _: + reveal_type(m) # N: Revealed type is "builtins.object" + + match m: + case tuple_alias(): + reveal_type(m) # N: Revealed type is "builtins.tuple[Any, ...]" + case _: + reveal_type(m) # N: Revealed type is "builtins.object" + + match m: + # With real typeshed you'll get an error like this instead: + # Expected type in class pattern; found "typing._SpecialForm" + case Tuple(): # E: Expected type in class pattern; found "builtins.int" + reveal_type(m) # E: Statement is unreachable + case _: + reveal_type(m) # N: Revealed type is "builtins.object" + + match m: + # This is a false negative, it will raise at runtime + case Tuple_alias(): + reveal_type(m) # N: Revealed type is "builtins.tuple[Any, ...]" + case _: + reveal_type(m) # N: Revealed type is "builtins.object" +[builtins fixtures/tuple.pyi] + [case testMatchNonFinalMatchArgs] class A: __match_args__ = ("a", "b")