8000 Allow assignments to multiple targets from union types by ilevkivskyi · Pull Request #4067 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Allow assignments to multiple targets from union types #4067

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

Merged
merged 11 commits into from
Oct 11, 2017

Conversation

ilevkivskyi
Copy link
Member

Fixes #3859
Fixes #3240
Fixes #1855
Fixes #1575

This is a simple fix of various bugs and a crash based on the idea originally proposed in #2219. The idea is to check assignment for every item in a union. However, in contrast to #2219, I think it will be more expected/consistent to construct a union of the resulting types instead of a join, for example:

x: Union[int, str]
x1 = x
reveal_type(x1) # Revealed type is 'Union[int, str]'

y: Union[Tuple[int], Tuple[str]]
(y1,) = y
reveal_type(y1) # Revealed type is 'Union[int, str]'

Thanks to @elazarg for initial work on this!

@JukkaL JukkaL self-assigned this Oct 9, 2017
Copy link
Collaborator
@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

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

It's great to finally have this fixed. Left a bunch of minor comments, mostly about documenting what's going on.

b: B

a: Union[Tuple[int, int], Tuple[int, object]]
(x[0], b.x) = a
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add test similar to this but where there is a type error.

@@ -1417,3 +1417,33 @@ async def main() -> None:
[out]
_testAsyncioGatherPreciseType.py:9: error: Revealed type is 'builtins.str'
_testAsyncioGatherPreciseType.py:10: error: Revealed type is 'builtins.str'

[case testNoCrashOnGenericUnionUnpacking]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Pythoneval test cases have a big fixed overhead, so I'd suggest combining all of these three test cases into a single test case.

mypy/binder.py Outdated
@@ -37,6 +42,10 @@ def __init__(self) -> None:
self.unreachable = False


if MYPY:
Assigns = DefaultDict[Expression, List[Tuple[Type, Optional[Type]]]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add comment that explains what this is used for.

mypy/checker.py Outdated
item_type = iterable.args[0]
if not isinstance(item_type, AnyType):
return item_type
# Try also structural typing
Copy link
Collaborator

Choose a reason for hiding this comment

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

This relies on map_instance_to_supertype returning Iterable[Any] in case there is no explicit base class. Add a note about it here and also update docstring of map_instance_to_supertype to mention it (currently it says that the second argument must be a superclass).

@@ -1716,7 +1749,7 @@ def split_around_star(self, items: List[T], star_index: int,
returns in: ([1,2], [3,4,5], [6,7])
"""
nr_right_of_star = length - star_index - 1
right_index = nr_right_of_star if -nr_right_of_star != 0 else len(items)
right_index = -nr_right_of_star if nr_right_of_star != 0 else len(items)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's this change?

Copy link
Member Author
@ilevkivskyi ilevkivskyi Oct 10, 2017

Choose a reason for hiding this comment

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

This bug was exposed by one of the tests. It's quite surprising that it never appeared before, since the old logic took first types after nr_right_of_star, instead of the same number counting from the end of items.

EDIT: fixed wording.

mypy/checker.py Outdated
rvalue_type: UnionType, context: Context,
infer_lvalue_type: bool) -> None:
transposed = tuple([] for _ in lvalues) # type: Tuple[List[Type], ...]
with self.binder.accumulate_type_assignments() as assignments:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add comment about why we have this.

mypy/checker.py Outdated
infer_lvalue_type: bool) -> None:
transposed = tuple([] for _ in lvalues) # type: Tuple[List[Type], ...]
with self.binder.accumulate_type_assignments() as assignments:
for item in rvalue_type.items:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add short comment describing what we are doing here. For example, something like "Type check the assignment separately for each union item and collect the inferred lvalue types for each union item.".

mypy/checker.py Outdated
for t, lv in zip(transposed, lvalues):
t.append(self.type_map.pop(lv, AnyType(TypeOfAny.special_form)))
union_types = tuple(UnionType.make_simplified_union(col) for col in transposed)
for expr, items in assignments.items():
Copy link
Collaborator

Choose a reason for hiding this comment

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

Again, add comment that describes the purpose of this look in one sentence.

mypy/checker.py Outdated
UnionType.make_simplified_union(types),
UnionType.make_simplified_union(declared_types),
False)
for union, lv in zip(union_types, lvalues):
Copy link
Collaborator

Choose a reason for hiding this comment

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

One more comment for this loop.

c, *d = x
reveal_type(c) # E: Revealed type is 'builtins.int'
reveal_type(d) # E: Revealed type is 'builtins.list[builtins.int*]'
[builtins fixtures/tuple.pyi]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Ideas for additional test cases:

  • Multiple lvalues such as <first> = <second> = <rvalue>.
  • Multiple assignment in a for loop (for x, y in ...).
  • None of the union items are iterable.
  • None of the union items have the correct number of items (try with too few and too many).

@ilevkivskyi
Copy link
Member Author

@JukkaL Thanks for review! I think I addressed all comments in new commits.

Copy link
Collaborator
@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! Looks mostly good now, just a few remaining minor things.

mypy/checker.py Outdated
@@ -1639,18 +1639,35 @@ def check_multi_assignment(self, lvalues: List[Lvalue],
def check_multi_assignment_from_union(self, lvalues: List[Expression], rvalue: Expression,
rvalue_type: UnionType, context: Context,
infer_lvalue_type: bool) -> None:
"""Check assignment to multiple lavlue targets when rvalue type is a Union[...].
Copy link
Collaborator

Choose a reason for hiding this comment

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

Typo: lavlue -> lvalue.

mypy/binder.py Outdated
If this map is not None, actual binding is deferred until all items in
the union are processed (a union of collected items is later bound
manually by the caller).
"""
self.type_assignments = defaultdict(list)
yield self.type_assignments
self.type_assignments = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about things like (x, (y, z)) = ... where there are two levels of union type multiple assignment? Would we need to restore old mapping perhaps? This would be worth testing as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you are totally right. Will push a commit in a second. Also testing this uncovered another flaw in my implementation: I need to iterate over nested lvalues in for loops (added a test for this as well).

Copy link
Member Author

Choose a reason for hiding this comment

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

Done!

@JukkaL
Copy link
Collaborator
JukkaL commented Oct 11, 2017

This causes a crash on internal Dropbox code. It happens on a line like this:

            for error_label, error_type in error_set:

I haven't isolated a small repro yet, but here's the traceback:

Traceback (most recent call last):
  File "/Users/jukka/src/review-mypy/scripts/mypy", line 6, in <module>
    main(__file__)
  File "/Users/jukka/src/review-mypy/mypy/main.py", line 50, in main
    res = type_check_only(sources, bin_dir, options)
  File "/Users/jukka/src/review-mypy/mypy/main.py", line 103, in type_check_only
    options=options)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 199, in build
    graph = dispatch(sources, manager)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 1865, in dispatch
    process_graph(graph, manager)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 2115, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 2218, in process_stale_scc
    graph[id].type_check_first_pass()
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 1775, in type_check_first_pass
    self.type_checker.check_first_pass()
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 194, in check_first_pass
    self.accept(d)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 690, in accept
    return visitor.visit_class_def(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 1184, in visit_class_def
    self.accept(defn.defs)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 750, in accept
    return visitor.visit_block(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 1304, in visit_block
    self.accept(s)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 496, in accept
    return visitor.visit_func_def(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 530, in visit_func_def
    self.check_func_item(defn, name=defn.name())
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 590, in check_func_item
    self.check_func_def(defn, typ, name)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 750, in check_func_def
    self.accept(item.body)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 750, in accept
    return visitor.visit_block(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 1304, in visit_block
    self.accept(s)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 851, in accept
    return visitor.visit_for_stmt(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2390, in visit_for_stmt
    self.accept_loop(s.body, s.else_body)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 298, in accept_loop
    self.accept(body)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 750, in accept
    return visitor.visit_block(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 1304, in visit_block
    self.accept(s)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 851, in accept
    return visitor.visit_for_stmt(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2388, in visit_for_stmt
    item_type = self.analyze_iterable_item_type(s.expr)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2424, in analyze_iterable_item_type
    expr, messages.ITERABLE_EXPECTED)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2584, in check_subtype
    if is_subtype(subtype, supertype):
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 83, in is_subtype
    ignore_declared_variance=ignore_declared_variance))
  File "/Users/jukka/src/review-mypy/mypy/types.py", line 1227, in accept
    return visitor.visit_union_type(self)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 312, in visit_union_type
    for item in left.items)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 312, in <genexpr>
    for item in left.items)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 83, in is_subtype
    ignore_declared_variance=ignore_declared_variance))
  File "/Users/jukka/src/review-mypy/mypy/types.py", line 1286, in accept
    return visitor.visit_partial_type(self)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 316, in visit_partial_type
    raise RuntimeError
RuntimeError:

@JukkaL
Copy link
Collaborator
JukkaL commented Oct 11, 2017

The type of error_set is Union[builtins.set[Tuple[builtins.str, builtins.str]], <partial None>]. It looks like a partial None type is leaking to a union, which shouldn't happen. So apparently this PR triggers the crash, but the underlying problem is elsewhere. I'll try to understand and fix the partial None issue.

@JukkaL
Copy link
Collaborator
JukkaL commented Oct 11, 2017

The inferred partial None seems to be caused by this PR. Here's a short repro (use --strict-optional):

from typing import Dict, Tuple, Set, Any

a: Any
d: Dict[str, Tuple[Set[Tuple[str, str]], str]]
x, _ = d.get(a, (None, None))
reveal_type(x)

for y in x: pass

Output from mypy:

$ mypy --show-traceback --strict-optional rt2.py
rt2.py:6: error: Revealed type is 'Union[builtins.set[Tuple[builtins.str, builtins.str]], <partial None>]'
rt2.py:8: error: INTERNAL ERROR -- please report a bug at https://github.com/python/mypy/issues version: 0.540-dev-ee93a9b96abdb1a4a61ca1b97fa29eed2ec1d73f
Traceback (most recent call last):
  File "/Users/jukka/src/mypy/scripts/mypy", line 6, in <module>
    main(__file__)
  File "/Users/jukka/src/review-mypy/mypy/main.py", line 50, in main
    res = type_check_only(sources, bin_dir, options)
  File "/Users/jukka/src/review-mypy/mypy/main.py", line 103, in type_check_only
    options=options)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 199, in build
    graph = dispatch(sources, manager)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 1865, in dispatch
    process_graph(graph, manager)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 2115, in process_graph
    process_stale_scc(graph, scc, manager)
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 2218, in process_stale_scc
    graph[id].type_check_first_pass()
  File "/Users/jukka/src/review-mypy/mypy/build.py", line 1775, in type_check_first_pass
    self.type_checker.check_first_pass()
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 194, in check_first_pass
    self.accept(d)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 282, in accept
    stmt.accept(self)
  File "/Users/jukka/src/review-mypy/mypy/nodes.py", line 851, in accept
    return visitor.visit_for_stmt(self)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2388, in visit_for_stmt
    item_type = self.analyze_iterable_item_type(s.expr)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2424, in analyze_iterable_item_type
    expr, messages.ITERABLE_EXPECTED)
  File "/Users/jukka/src/review-mypy/mypy/checker.py", line 2584, in check_subtype
    if is_subtype(subtype, supertype):
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 83, in is_subtype
    ignore_declared_variance=ignore_declared_variance))
  File "/Users/jukka/src/review-mypy/mypy/types.py", line 1227, in accept
    return visitor.visit_union_type(self)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 312, in visit_union_type
    for item in left.items)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 312, in <genexpr>
    for item in left.items)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 83, in is_subtype
    ignore_declared_variance=ignore_declared_variance))
  File "/Users/jukka/src/review-mypy/mypy/types.py", line 1286, in accept
    return visitor.visit_partial_type(self)
  File "/Users/jukka/src/review-mypy/mypy/subtypes.py", line 316, in visit_partial_type
    raise RuntimeError
RuntimeError:

@ilevkivskyi
Copy link
Member Author

OK, I will take a look now.

@ilevkivskyi
Copy link
Member Author

It looks like the simplest solution is to not infer partial types while unpacking unions, anyway the resulting type will be Union[None, <other types...>].

@JukkaL JukkaL merged commit 77ce5e2 into python:master Oct 11, 2017
@ilevkivskyi ilevkivskyi deleted the multi-assign-from-union branch October 11, 2017 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants
0