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

Correctly handle variadic instances with empty arguments #16238

Merged
merged 4 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4662,7 +4662,10 @@ class C(Generic[T, Unpack[Ts]]): ...
info = t.type_object()
# We reuse the logic from semanal phase to reduce code duplication.
fake = Instance(info, args, line=ctx.line, column=ctx.column)
if not validate_instance(fake, self.chk.fail):
# 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):
fix_instance(
fake, self.chk.fail, self.chk.note, disallow_any=False, options=self.chk.options
)
Expand Down
2 changes: 1 addition & 1 deletion mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def expand_type_by_instance(typ: Type, instance: Instance) -> Type:
def expand_type_by_instance(typ: Type, instance: Instance) -> Type:
"""Substitute type variables in type using values from an Instance.
Type variables are considered to be bound by the class declaration."""
if not instance.args:
if not instance.args and not instance.type.has_type_var_tuple_type:
return typ
else:
variables: dict[TypeVarId, Type] = {}
Expand Down
6 changes: 4 additions & 2 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2505,8 +2505,10 @@ def format_literal_value(typ: LiteralType) -> str:
else:
base_str = itype.type.name
if not itype.args:
# No type arguments, just return the type name
return base_str
if not itype.type.has_type_var_tuple_type:
# No type arguments, just return the type name
return base_str
return base_str + "[()]"
elif itype.type.fullname == "builtins.tuple":
item_type_str = format(itype.args[0])
return f"{'tuple' if options.use_lowercase_names() else 'Tuple'}[{item_type_str}, ...]"
Expand Down
28 changes: 20 additions & 8 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,11 @@
check_for_explicit_any,
detect_diverging_alias,
find_self_type,
fix_instance_types,
fix_instance,
has_any_from_unimported_type,
no_subscript_builtin_alias,
type_constructors,
validate_instance,
)
from mypy.typeops import function_type, get_type_vars, try_getting_str_literals_from_type
from mypy.types import (
Expand Down Expand Up @@ -722,7 +723,9 @@ def create_alias(self, tree: MypyFile, target_name: str, alias: str, name: str)
target = self.named_type_or_none(target_name, [])
assert target is not None
# Transform List to List[Any], etc.
fix_instance_types(target, self.fail, self.note, self.options)
fix_instance(
target, self.fail, self.note, disallow_any=False, options=self.options
)
alias_node = TypeAlias(
target,
alias,
Expand Down Expand Up @@ -3455,7 +3458,7 @@ def analyze_simple_literal_type(self, rvalue: Expression, is_final: bool) -> Typ

def analyze_alias(
self, name: str, rvalue: Expression, allow_placeholder: bool = False
) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str]]:
) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]:
"""Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable).

If yes, return the corresponding type, a list of
Expand All @@ -3474,7 +3477,7 @@ def analyze_alias(
self.fail(
"Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE
)
return None, [], set(), []
return None, [], set(), [], False

found_type_vars = typ.accept(TypeVarLikeQuery(self, self.tvar_scope))
tvar_defs: list[TypeVarLikeType] = []
Expand Down Expand Up @@ -3508,7 +3511,8 @@ def analyze_alias(
new_tvar_defs.append(td)

qualified_tvars = [node.fullname for _name, node in found_type_vars]
return analyzed, new_tvar_defs, depends_on, qualified_tvars
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

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 @@ -3591,9 +3595,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
else:
tag = self.track_incomplete_refs()
res, alias_tvars, depends_on, qualified_tvars = self.analyze_alias(
res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias(
lvalue.name, rvalue, allow_placeholder=True
)
if not res:
Expand Down Expand Up @@ -3626,8 +3631,15 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
# Note: with the new (lazy) type alias representation we only need to set no_args to True
# if the expected number of arguments is non-zero, so that aliases like A = List work.
# However, eagerly expanding aliases like Text = str is a nice performance optimization.
no_args = isinstance(res, Instance) and not res.args # type: ignore[misc]
fix_instance_types(res, self.fail, self.note, self.options)
no_args = (
isinstance(res, ProperType)
and isinstance(res, Instance)
and not res.args
and not empty_tuple_index
)
if isinstance(res, ProperType) and isinstance(res, Instance):
if not validate_instance(res, self.fail, empty_tuple_index):
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
# exist. Work around by expanding them eagerly when used.
Expand Down
4 changes: 3 additions & 1 deletion mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ def visit_instance(self, left: Instance) -> bool:
right_args = (
right_prefix + (TupleType(list(right_middle), fallback),) + right_suffix
)
if not self.proper_subtype:
if not self.proper_subtype and t.args:
for arg in map(get_proper_type, t.args):
if isinstance(arg, UnpackType):
unpacked = get_proper_type(arg.type)
Expand All @@ -557,6 +557,8 @@ def visit_instance(self, left: Instance) -> bool:
break
else:
return True
if len(left_args) != len(right_args):
return False
type_params = zip(left_args, right_args, right.type.defn.type_vars)
else:
type_params = zip(t.args, right.args, right.type.defn.type_vars)
Expand Down
54 changes: 15 additions & 39 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
ParamSpecType,
PartialType,
PlaceholderType,
ProperType,
RawExpressionType,
RequiredType,
SyntheticTypeVisitor,
Expand Down Expand Up @@ -89,7 +90,6 @@
has_type_vars,
)
from mypy.types_utils import is_bad_type_type_item
from mypy.typetraverser import TypeTraverserVisitor
from mypy.typevars import fill_typevars

T = TypeVar("T")
Expand Down Expand Up @@ -425,9 +425,10 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
# 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, Instance) # type: ignore[misc]
and not self.defining_alias
and not validate_instance(res, self.fail)
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)
):
fix_instance(
res,
Expand All @@ -442,7 +443,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)
return self.analyze_type_with_type_info(node, t.args, t, t.empty_tuple_index)
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 @@ -700,7 +701,7 @@ def get_omitted_any(self, typ: Type, fullname: str | None = None) -> AnyType:
return get_omitted_any(disallow_any, self.fail, self.note, typ, self.options, fullname)

def analyze_type_with_type_info(
self, info: TypeInfo, args: Sequence[Type], ctx: Context
self, info: TypeInfo, args: Sequence[Type], ctx: Context, empty_tuple_index: bool
) -> Type:
"""Bind unbound type when were able to find target TypeInfo.

Expand Down Expand Up @@ -735,7 +736,9 @@ def analyze_type_with_type_info(

# Check type argument count.
instance.args = tuple(flatten_nested_tuples(instance.args))
if not self.defining_alias and not validate_instance(instance, self.fail):
if not (self.defining_alias and self.nesting_level == 0) and not validate_instance(
instance, self.fail, empty_tuple_index
):
fix_instance(
instance,
self.fail,
Expand Down Expand Up @@ -1203,7 +1206,7 @@ def visit_placeholder_type(self, t: PlaceholderType) -> Type:
else:
# TODO: Handle non-TypeInfo
assert isinstance(n.node, TypeInfo)
return self.analyze_type_with_type_info(n.node, t.args, t)
return self.analyze_type_with_type_info(n.node, t.args, t, False)

def analyze_callable_args_for_paramspec(
self, callable_args: Type, ret_type: Type, fallback: Instance
Expand Down Expand Up @@ -2256,7 +2259,7 @@ def make_optional_type(t: Type) -> Type:
return UnionType([t, NoneType()], t.line, t.column)


def validate_instance(t: Instance, fail: MsgCallback) -> bool:
def validate_instance(t: Instance, fail: MsgCallback, empty_tuple_index: 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 @@ -2279,8 +2282,9 @@ def validate_instance(t: Instance, fail: MsgCallback) -> bool:
)
return False
elif not t.args:
# The Any arguments should be set by the caller.
return False
if not (empty_tuple_index and len(t.type.type_vars) == 1):
# The Any arguments should be set by the caller.
return False
else:
# We also need to check if we are not performing a type variable tuple split.
unpack = find_unpack_in_list(t.args)
Expand Down Expand Up @@ -2313,34 +2317,6 @@ def validate_instance(t: Instance, fail: MsgCallback) -> bool:
return True


def fix_instance_types(t: Type, fail: MsgCallback, note: MsgCallback, options: Options) -> None:
"""Recursively fix all instance types (type argument count) in a given type.

For example 'Union[Dict, List[str, int]]' will be transformed into
'Union[Dict[Any, Any], List[Any]]' in place.
"""
t.accept(InstanceFixer(fail, note, options))


class InstanceFixer(TypeTraverserVisitor):
def __init__(self, fail: MsgCallback, note: MsgCallback, options: Options) -> None:
self.fail = fail
self.note = note
self.options = options

def visit_instance(self, typ: Instance) -> None:
super().visit_instance(typ)
if not validate_instance(typ, self.fail):
fix_instance(
typ,
self.fail,
self.note,
disallow_any=False,
options=self.options,
use_generic_error=True,
)


def find_self_type(typ: Type, lookup: Callable[[str], SymbolTableNode | None]) -> bool:
return typ.accept(HasSelfType(lookup))

Expand Down
2 changes: 2 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3163,6 +3163,8 @@ def visit_instance(self, t: Instance) -> str:
s += f"[{self.list_str(t.args)}, ...]"
else:
s += f"[{self.list_str(t.args)}]"
elif t.type.has_type_var_tuple_type and len(t.type.type_vars) == 1:
s += "[()]"
if self.id_mapper:
s += f"<{self.id_mapper.id(t.type)}>"
return s
Expand Down
19 changes: 19 additions & 0 deletions test-data/unit/check-flags.test
Original file line number Diff line number Diff line change
Expand Up @@ -2277,3 +2277,22 @@ list(2) # E: No overload variant of "list" matches argument type "int" [call-o
# N: def [T] __init__(self) -> List[T] \
# N: def [T] __init__(self, x: Iterable[T]) -> List[T]
[builtins fixtures/list.pyi]

[case testNestedGenericInAliasDisallow]
# flags: --disallow-any-generics
from typing import TypeVar, Generic, List, Union

class C(Generic[T]): ...

A = Union[C, List] # E: Missing type parameters for generic type "C" \
# E: Missing type parameters for generic type "List"
[builtins fixtures/list.pyi]

[case testNestedGenericInAliasAllow]
# flags: --allow-any-generics
from typing import TypeVar, Generic, List, Union

class C(Generic[T]): ...

A = Union[C, List] # OK
[builtins fixtures/list.pyi]
49 changes: 48 additions & 1 deletion test-data/unit/check-typevar-tuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ variadic_single: Variadic[int]
reveal_type(variadic_single) # N: Revealed type is "__main__.Variadic[builtins.int]"

empty: Variadic[()]
reveal_type(empty) # N: Revealed type is "__main__.Variadic[Unpack[builtins.tuple[Any, ...]]]"
reveal_type(empty) # N: Revealed type is "__main__.Variadic[()]"

omitted: Variadic
reveal_type(omitted) # N: Revealed type is "__main__.Variadic[Unpack[builtins.tuple[Any, ...]]]"

bad: Variadic[Unpack[Tuple[int, ...]], str, Unpack[Tuple[bool, ...]]] # E: More than one Unpack in a type is not allowed
reveal_type(bad) # N: Revealed type is "__main__.Variadic[Unpack[builtins.tuple[builtins.int, ...]], builtins.str]"
Expand Down Expand Up @@ -1846,6 +1849,50 @@ def foo3(func: Callable[[int, Unpack[Args2]], T], *args: Unpack[Args2]) -> T:
return submit(func, 1, *args)
[builtins fixtures/tuple.pyi]

[case testTypeVarTupleEmptySpecialCase]
from typing import Any, Callable, Generic
from typing_extensions import Unpack, TypeVarTuple

Ts = TypeVarTuple("Ts")
class MyClass(Generic[Unpack[Ts]]):
func: Callable[[Unpack[Ts]], object]

def __init__(self, func: Callable[[Unpack[Ts]], object]) -> None:
self.func = func

explicit: MyClass[()]
reveal_type(explicit) # N: Revealed type is "__main__.MyClass[()]"
reveal_type(explicit.func) # N: Revealed type is "def () -> builtins.object"

a: Any
explicit_2 = MyClass[()](a)
reveal_type(explicit_2) # N: Revealed type is "__main__.MyClass[()]"
reveal_type(explicit_2.func) # N: Revealed type is "def () -> builtins.object"

Alias = MyClass[()]
explicit_3: Alias
reveal_type(explicit_3) # N: Revealed type is "__main__.MyClass[()]"
reveal_type(explicit_3.func) # N: Revealed type is "def () -> builtins.object"

explicit_4 = Alias(a)
reveal_type(explicit_4) # N: Revealed type is "__main__.MyClass[()]"
reveal_type(explicit_4.func) # N: Revealed type is "def () -> builtins.object"

def no_args() -> None: ...
implicit = MyClass(no_args)
reveal_type(implicit) # N: Revealed type is "__main__.MyClass[()]"
reveal_type(implicit.func) # N: Revealed type is "def () -> builtins.object"

def one_arg(__a: int) -> None: ...
x = MyClass(one_arg)
x = explicit # E: Incompatible types in assignment (expression has type "MyClass[()]", variable has type "MyClass[int]")

# Consistently handle special case for no argument aliases
Direct = MyClass
y = Direct(one_arg)
reveal_type(y) # N: Revealed type is "__main__.MyClass[builtins.int]"
[builtins fixtures/tuple.pyi]

[case testTypeVarTupleRuntimeTypeApplication]
from typing import Generic, TypeVar, Tuple
from typing_extensions import Unpack, TypeVarTuple
Expand Down