Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generalize X[()] support for Unpack with empty tuples #18395

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Loading