Skip to content

Commit

Permalink
Generalize X[()] support for Unpack with empty tuples
Browse files Browse the repository at this point in the history
  • Loading branch information
A5rocks committed Dec 31, 2024
1 parent 7b4f862 commit 89c032a
Show file tree
Hide file tree
Showing 9 changed files with 61 additions and 36 deletions.
2 changes: 1 addition & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
3 changes: 1 addition & 2 deletions mypy/exprtotype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 1 addition & 8 deletions mypy/fastparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mypy/server/astdiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)

Expand Down
2 changes: 1 addition & 1 deletion mypy/stubutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 11 additions & 11 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,15 +483,15 @@ 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.
if (
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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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):
Expand All @@ -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,
Expand Down
10 changes: 5 additions & 5 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ class UnboundType(ProperType):
"name",
"args",
"optional",
"empty_tuple_index",
"has_parameters",
"original_str_expr",
"original_str_fallback",
)
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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,
)
Expand Down
33 changes: 33 additions & 0 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]

0 comments on commit 89c032a

Please sign in to comment.