Skip to content

[WIP] Fix crash when joining tuple and NamedTuple #3129

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

Closed
wants to merge 21 commits into from
Closed
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
4 changes: 2 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1624,7 +1624,7 @@ def append_types_for_inference(lvs: List[Expression], rv_types: List[Type]) -> N

append_types_for_inference(right_lvs, right_rv_types)

return TupleType(type_parameters, self.named_type('builtins.tuple'))
return TupleType(type_parameters, self.named_generic_type('builtins.tuple', [AnyType()]))

def split_around_star(self, items: List[T], star_index: int,
length: int) -> Tuple[List[T], List[T], List[T]]:
Expand Down Expand Up @@ -1689,7 +1689,7 @@ def check_lvalue(self, lvalue: Lvalue) -> Tuple[Optional[Type],
self.store_type(lvalue, lvalue_type)
elif isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr):
types = [self.check_lvalue(sub_expr)[0] for sub_expr in lvalue.items]
lvalue_type = TupleType(types, self.named_type('builtins.tuple'))
lvalue_type = TupleType(types, self.named_generic_type('builtins.tuple', [AnyType()]))
else:
lvalue_type = self.expr_checker.accept(lvalue)

Expand Down
9 changes: 5 additions & 4 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1858,7 +1858,7 @@ def visit_tuple_expr(self, e: TupleExpr) -> Type:
tt = self.accept(item, type_context_items[j])
j += 1
items.append(tt)
fallback_item = join.join_type_list(items)
fallback_item = UnionType.make_simplified_union(items)
return TupleType(items, self.chk.named_generic_type('builtins.tuple', [fallback_item]))

def visit_dict_expr(self, e: DictExpr) -> Type:
Expand Down Expand Up @@ -1898,7 +1898,7 @@ def visit_dict_expr(self, e: DictExpr) -> Type:
#
# def <unnamed>(*v: Tuple[kt, vt]) -> Dict[kt, vt]: ...
constructor = CallableType(
[TupleType([kt, vt], self.named_type('builtins.tuple'))],
[TupleType([kt, vt], self.chk.named_generic_type('builtins.tuple', [AnyType()]))],
[nodes.ARG_STAR],
[None],
self.chk.named_generic_type('builtins.dict', [kt, vt]),
Expand Down Expand Up @@ -2325,8 +2325,9 @@ def check_awaitable_expr(self, t: Type, ctx: Context, msg: str) -> Type:

Also used by `async for` and `async with`.
"""
if not self.chk.check_subtype(t, self.named_type('typing.Awaitable'), ctx,
msg, 'actual type', 'expected type'):
if not self.chk.check_subtype(
t, self.chk.named_generic_type('typing.Awaitable', [AnyType()]), ctx, msg,
'actual type', 'expected type'):
return AnyType()
else:
method = self.analyze_external_member_access('__await__', t, ctx)
Expand Down
4 changes: 3 additions & 1 deletion mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ def visit_overloaded(self, t: Overloaded) -> Type:
return Overloaded(items)

def visit_tuple_type(self, t: TupleType) -> Type:
return t.copy_modified(items=self.expand_types(t.items))
new_fallback = expand_type(t.fallback, self.variables)
assert isinstance(new_fallback, Instance)
return t.copy_modified(items=self.expand_types(t.items), fallback=new_fallback)

def visit_typeddict_type(self, t: TypedDictType) -> Type:
return t.copy_modified(item_types=self.expand_types(t.items.values()))
Expand Down
5 changes: 4 additions & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ def format_simple(self, typ: Type, verbosity: int = 0) -> str:
return '"{}"'.format(base_str)
elif itype.type.fullname() == 'builtins.tuple':
item_type_str = strip_quotes(self.format(itype.args[0]))
return 'Tuple[{}, ...]'.format(item_type_str)
if isinstance(itype.args[0], AnyType):
return 'tuple'
else:
return 'Tuple[{}, ...]'.format(item_type_str)
elif itype.type.fullname() in reverse_type_aliases:
alias = reverse_type_aliases[itype.type.fullname()]
alias = alias.split('.')[-1]
Expand Down
3 changes: 1 addition & 2 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,9 @@ def visit_instance(self, left: Instance) -> bool:
rname = right.type.fullname()
if not left.type.has_base(rname) and rname != 'builtins.object':
return False

# Map left type to corresponding right instances.
t = map_instance_to_supertype(left, right.type)

# TODO: assert len(t.args) == len(right.args)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this is still a TODO? I thought the main idea is to fix this specific point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it seemed to pass in the real world, but fail in tests. Let me figure out what stubs it needs... Or maybe I'll just sneak in some no-fixture tests into the test suite while nobody's watching.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe I'll just sneak in some no-fixture tests into the test suite while nobody's watching.

:-) I like the idea of running test with real full stubs, but let us postpone this for some time.
It is better to update fixtures for now, to make them match real stubs more.

Actually, the fact that some stubs can crash here is a bit dangerous, maybe we are still missing something?

Copy link
Contributor Author

@pkch pkch May 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with Awaitable is fixed, the problem with NewType I'll wait for you to fix it in your PR as we discussed. There may be other problems that I haven't investigated yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm still not sure why the assert fails, I'll look into it later.

return all(self.check_type_parameter(lefta, righta, tvar.variance)
for lefta, righta, tvar in
zip(t.args, right.args, right.type.defn.type_vars))
Expand Down
61 changes: 28 additions & 33 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,38 +260,33 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
self.fail('Invalid type "{}"'.format(name), t)
return t
info = sym.node # type: TypeInfo
if len(t.args) > 0 and info.fullname() == 'builtins.tuple':
return TupleType(self.anal_array(t.args),
Instance(info, [AnyType()], t.line),
t.line)
else:
# Analyze arguments and construct Instance type. The
# number of type arguments and their values are
# checked only later, since we do not always know the
# valid count at this point. Thus we may construct an
# Instance with an invalid number of type arguments.
instance = Instance(info, self.anal_array(t.args), t.line, t.column)
instance.from_generic_builtin = sym.normalized
tup = info.tuple_type
if tup is not None:
# The class has a Tuple[...] base class so it will be
# represented as a tuple type.
if t.args:
self.fail('Generic tuple types not supported', t)
return AnyType()
return tup.copy_modified(items=self.anal_array(tup.items),
fallback=instance)
td = info.typeddict_type
if td is not None:
# The class has a TypedDict[...] base class so it will be
# represented as a typeddict type.
if t.args:
self.fail('Generic TypedDict types not supported', t)
return AnyType()
# Create a named TypedDictType
return td.copy_modified(item_types=self.anal_array(list(td.items.values())),
fallback=instance)
return instance
# Analyze arguments and construct Instance type. The
# number of type arguments and their values are
# checked only later, since we do not always know the
# valid count at this point. Thus we may construct an
# Instance with an invalid number of type arguments.
instance = Instance(info, self.anal_array(t.args), t.line, t.column)
instance.from_generic_builtin = sym.normalized
tup = info.tuple_type
if tup is not None:
# The class has a Tuple[...] base class so it will be
# represented as a tuple type.
if t.args:
self.fail('Generic tuple types not supported', t)
return AnyType()
return tup.copy_modified(items=self.anal_array(tup.items),
fallback=instance)
td = info.typeddict_type
if td is not None:
# The class has a TypedDict[...] base class so it will be
# represented as a typeddict type.
if t.args:
self.fail('Generic TypedDict types not supported', t)
return AnyType()
# Create a named TypedDictType
return td.copy_modified(item_types=self.anal_array(list(td.items.values())),
fallback=instance)
return instance
else:
return AnyType()

Expand Down Expand Up @@ -345,7 +340,7 @@ def visit_tuple_type(self, t: TupleType) -> Type:
self.fail('At most one star type allowed in a tuple', t)
if t.implicit:
return TupleType([AnyType() for _ in t.items],
self.named_type('builtins.tuple'),
self.named_type('builtins.tuple', [AnyType()]),
t.line)
else:
return AnyType()
Expand Down
8 changes: 7 additions & 1 deletion mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ class Instance(Type):

def __init__(self, typ: mypy.nodes.TypeInfo, args: List[Type],
line: int = -1, column: int = -1, erased: bool = False) -> None:
# TODO: assert(typ is NOT_READY or typ.fullname() != 'builtins.tuple' or len(args) == 1)
assert(typ is NOT_READY or typ.fullname() not in ["builtins.Any", "typing.Any"])
self.type = typ
self.args = args
Expand Down Expand Up @@ -888,6 +889,8 @@ def __init__(self, items: List[Type], fallback: Instance, line: int = -1,
column: int = -1, implicit: bool = False) -> None:
self.items = items
self.fallback = fallback
# TODO: assert not (isinstance(fallback, Instance) and fallback.type and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to see the TODO items resolved before this is merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked into this yet.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And again, not sure why assert fails. I'll commit fixes to the other problems first.

# fallback.type.fullname() == 'builtins.tuple' and not fallback.args)
self.implicit = implicit
self.can_be_true = len(self.items) > 0
self.can_be_false = len(self.items) == 0
Expand Down Expand Up @@ -922,7 +925,10 @@ def copy_modified(self, *, fallback: Optional[Instance] = None,
return TupleType(items, fallback, self.line, self.column)

def slice(self, begin: int, stride: int, end: int) -> 'TupleType':
return TupleType(self.items[begin:end:stride], self.fallback,
new_items = self.items[begin:end:stride]
fallback_args = [UnionType.make_simplified_union(new_items)]
new_fallback = self.fallback.copy_modified(args=fallback_args)
return TupleType(new_items, new_fallback,
self.line, self.column, self.implicit)


Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-functions.test
Original file line number Diff line number Diff line change
Expand Up @@ -1745,7 +1745,7 @@ f(1, thing_in_kwargs=["hey"])
from typing import Tuple, Any
def f(x, *args): # type: (...) -> None
success_tuple_type = args # type: Tuple[Any, ...]
fail_tuple_type = args # type: None # E: Incompatible types in assignment (expression has type Tuple[Any, ...], variable has type None)
fail_tuple_type = args # type: None # E: Incompatible types in assignment (expression has type tuple, variable has type None)
f(1, "hello")
[builtins fixtures/tuple.pyi]
[out]
Expand Down
2 changes: 2 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -1789,6 +1789,8 @@ def g(x: Union[int, str]): pass
c = a if f() else b
g(c) # E: Argument 1 to "g" has incompatible type "Union[int, str, tuple]"; expected "Union[int, str]"

[builtins fixtures/list.pyi]

[case testUnificationMultipleInheritance]
class A: pass
class B:
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@ b = B._make(['']) # type: B
[case testNamedTupleIncompatibleRedefinition]
from typing import NamedTuple
class Crash(NamedTuple):
count: int # E: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as Callable[[Tuple[Any, ...], Any], int])
count: int # E: Incompatible types in assignment (expression has type "int", base class "tuple" defined the type as Callable[[tuple, Any], int])
[builtins fixtures/tuple.pyi]

[case testNamedTupleInClassNamespace]
Expand Down
9 changes: 5 additions & 4 deletions test-data/unit/check-tuples.test
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ from typing import Tuple
t1 = None # type: Tuple[A, A]
t2 = None # type: tuple

t1 = t2 # E: Incompatible types in assignment (expression has type Tuple[Any, ...], variable has type "Tuple[A, A]")
t1 = t2 # E: Incompatible types in assignment (expression has type tuple, variable has type "Tuple[A, A]")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe put tuple in quotes like this "tuple"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the test would fail then -- or did you mean to change the code the generates error messages?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or did you mean to change the code the generates error messages?

Sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a change, but for consistency, also changed a few other similar cases (which involve types other than tuple). lmk if that's ok.

t2 = t1

class A: pass
Expand Down Expand Up @@ -604,10 +604,10 @@ b = s in t

[file builtins.py]
from typing import TypeVar, Generic
_T = TypeVar('_T')
_T_co = TypeVar('_T_co', covariant=True)
class object:
def __init__(self) -> None: pass
class tuple(Generic[_T]):
class tuple(Generic[_T_co]):
def __len__(self) -> int: pass
def __str__(self) -> str: pass
def __contains__(self, o: object) -> bool: pass
Expand Down Expand Up @@ -702,6 +702,7 @@ B()[100]
[case testValidTupleBaseClass]
from typing import Tuple
class A(tuple): pass
[builtins fixtures/tuple.pyi]
[out]

[case testTupleBaseClass2-skip]
Expand Down Expand Up @@ -913,7 +914,7 @@ def f(a: Tuple) -> None: pass
f(())
f((1,))
f(('', ''))
f(0) # E: Argument 1 to "f" has incompatible type "int"; expected Tuple[Any, ...]
f(0) # E: Argument 1 to "f" has incompatible type "int"; expected tuple
[builtins fixtures/tuple.pyi]

[case testTupleSingleton]
Expand Down
4 changes: 2 additions & 2 deletions test-data/unit/fixtures/async_await.pyi
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing

T = typing.TypeVar('T')
class list(typing.Generic[T], typing.Sequence[T]): pass
class list(typing.Sequence[T]): pass

class object:
def __init__(self): pass
Expand All @@ -11,7 +11,7 @@ class int: pass
class str: pass
class dict: pass
class set: pass
class tuple: pass
class tuple(typing.Generic[T]): pass
class BaseException: pass
class StopIteration(BaseException): pass
class StopAsyncIteration(BaseException): pass
Expand Down
4 changes: 3 additions & 1 deletion test-data/unit/fixtures/bool.pyi
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# builtins stub used in boolean-related test cases.
from typing import Generic, TypeVar
T = TypeVar('T')

class object:
def __init__(self) -> None: pass

class type: pass
class tuple: pass
class tuple(Generic[T]): pass
class function: pass
class bool: pass
class int: pass
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/fixtures/dict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class list(Iterable[T], Generic[T]): # needed by some test cases
def __iter__(self) -> Iterator[T]: pass
def __mul__(self, x: int) -> list[T]: pass

class tuple: pass
class tuple(Generic[T]): pass
class function: pass
class float: pass
class bool: pass
Expand Down
4 changes: 3 additions & 1 deletion test-data/unit/fixtures/exception.pyi
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Generic, TypeVar
T = TypeVar('T')

class object:
def __init__(self): pass

class type: pass
class tuple: pass
class tuple(Generic[T]): pass
class function: pass
class int: pass
class str: pass
Expand Down
7 changes: 5 additions & 2 deletions test-data/unit/fixtures/fine_grained.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# enough to handle them.

import types
from typing import TypeVar, Generic

T = TypeVar('T')

class Any: pass

Expand All @@ -20,7 +23,7 @@ class str:

class float: pass
class bytes: pass
class tuple: pass
class tuple(Generic[T]): pass
class function: pass
class ellipsis: pass
class list: pass
class list(Generic[T]): pass
5 changes: 4 additions & 1 deletion test-data/unit/fixtures/float.pyi
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Generic, TypeVar
T = TypeVar('T')

Any = 0

class object:
Expand All @@ -12,7 +15,7 @@ class str:

class bytes: pass

class tuple: pass
class tuple(Generic[T]): pass
class function: pass

class ellipsis: pass
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/fixtures/floatdict.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class str:

class bytes: pass

class tuple: pass
class tuple(Generic[T]): pass
class function: pass

class ellipsis: pass
Expand Down
2 changes: 1 addition & 1 deletion test-data/unit/fixtures/for.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class object:
def __init__(self) -> None: pass

class type: pass
class tuple: pass
class tuple(Generic[t]): pass
class function: pass
class bool: pass
class int: pass # for convenience
Expand Down
7 changes: 5 additions & 2 deletions test-data/unit/fixtures/isinstancelist.pyi
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import Iterable, Iterator, TypeVar, List, Mapping, overload, Tuple, Set, Union
from typing import Iterable, Iterator, TypeVar, List, Mapping, overload, Tuple, Set, Union, Sequence, Generic

class object:
def __init__(self) -> None: pass

class type:
def __init__(self, x) -> None: pass

class tuple: pass
class function: pass
class ellipsis: pass

Expand All @@ -24,6 +23,10 @@ T = TypeVar('T')
KT = TypeVar('KT')
VT = TypeVar('VT')

class tuple(Sequence[T], Generic[T]):
def __iter__(self) -> Iterator[T]: pass
def __getitem__(self, x: int) -> T: pass

class list(Iterable[T]):
def __iter__(self) -> Iterator[T]: pass
def __mul__(self, x: int) -> list[T]: pass
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/list.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class list(Iterable[T], Generic[T]):
def extend(self, x: Iterable[T]) -> None: pass

class tuple(Generic[T]): pass

class function: pass
class int: pass
class float: pass
Expand Down
Loading