Skip to content

Commit

Permalink
Correctly handle variadic instances with empty arguments (#16238)
Browse files Browse the repository at this point in the history
Fixes #16199

It was surprisingly hard to fix, because all possible fixes strongly
interfered with the code that makes "no-args" aliases possible:
```python
l = list
x: l[int]  # OK, same as list[int]
```
So after all I re-organized (and actually simplified) that old code.
  • Loading branch information
ilevkivskyi authored Oct 16, 2023
1 parent 2bcec24 commit 85f40b5
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 53 deletions.
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

0 comments on commit 85f40b5

Please sign in to comment.