Skip to content

Change ConditionalTypeBinder to mainly use with statements #1731

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 28 commits into from
Jun 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8cd215e
Change ConditionalTypeBinder usage to mainly use with statements.
ecprice Jun 22, 2016
186dfbf
Add clear_breaking option to type binder context manager.
ecprice Jun 22, 2016
d539e6d
Fix bug with type binder and for/else
ecprice Jun 22, 2016
cfc3f2b
Fix bug with ConditionalTypeBinder and if/else.
ecprice Jun 22, 2016
09dc8f5
Remove canskip option from ConditionalTypeBinder
ecprice Jun 22, 2016
5eab5ed
Rename fallthrough to fall_through for consistency.
ecprice Jun 22, 2016
9d14b23
Comment explaining the frame_context() function.
ecprice Jun 22, 2016
a9f8ced
Keep track of whether a try/except block can fall off its end.
ecprice Jun 22, 2016
9c21fe3
Cleaner frame_context() interface.
ecprice Jun 22, 2016
88650b7
Remove unused functions
ecprice Jun 22, 2016
031c851
Switch binder contextmanager to use contextlib
ecprice Jun 22, 2016
01d98a9
Explanatory comments
ecprice Jun 23, 2016
296746b
Remove collapse_ancestry option, and always do it
ecprice Jun 23, 2016
2dc37f3
Fixes for try/finally binder
ecprice Jun 23, 2016
23e2444
Updates to binder
ecprice Jun 23, 2016
b910c49
Add another test for dependencies
ecprice Jun 23, 2016
dcae0e9
Cleaner binder dependencies
ecprice Jun 23, 2016
0d830dd
Remove unused binder function update_expand
ecprice Jun 23, 2016
fc40c75
Remove broken/changed from Frame, place in binder.last_pop_*
ecprice Jun 23, 2016
0d9bcef
Move options_on_return into binder, making Frame empty
ecprice Jun 23, 2016
a396173
Formatting improvements
ecprice Jun 23, 2016
edba00f
Always clear breaking_out in pop_frame()
ecprice Jun 23, 2016
c6f1a29
cleanups
ecprice Jun 23, 2016
737ccb3
More comments
ecprice Jun 23, 2016
6a67b83
Don't crash on the function redefinition example
ecprice Jun 23, 2016
26d4ce5
Test changes
ecprice Jun 24, 2016
151dae8
Move binder into its own file.
ecprice Jun 24, 2016
458dbba
Have frame_context() preserve original binder.breaking_out
ecprice Jun 24, 2016
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
264 changes: 264 additions & 0 deletions mypy/binder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
from typing import (Any, Dict, List, Set, Iterator)
from contextlib import contextmanager

from mypy.types import Type, AnyType, PartialType
from mypy.nodes import (Node, Var)

from mypy.subtypes import is_subtype
from mypy.join import join_simple
from mypy.sametypes import is_same_type


class Frame(Dict[Any, Type]):
pass


class Key(AnyType):
pass


class ConditionalTypeBinder:
"""Keep track of conditional types of variables.

NB: Variables are tracked by literal expression, so it is possible
to confuse the binder; for example,

```
class A:
a = None # type: Union[int, str]
x = A()
lst = [x]
reveal_type(x.a) # Union[int, str]
x.a = 1
reveal_type(x.a) # int
reveal_type(lst[0].a) # Union[int, str]
lst[0].a = 'a'
reveal_type(x.a) # int
reveal_type(lst[0].a) # str
```
"""

def __init__(self) -> None:
# The set of frames currently used. These map
# expr.literal_hash -- literals like 'foo.bar' --
# to types.
self.frames = [Frame()]

# For frames higher in the stack, we record the set of
# Frames that can escape there
self.options_on_return = [] # type: List[List[Frame]]

# Maps expr.literal_hash] to get_declaration(expr)
# for every expr stored in the binder
self.declarations = Frame()
# Set of other keys to invalidate if a key is changed, e.g. x -> {x.a, x[0]}
# Whenever a new key (e.g. x.a.b) is added, we update this
self.dependencies = {} # type: Dict[Key, Set[Key]]

# breaking_out is set to True on return/break/continue/raise
# It is cleared on pop_frame() and placed in last_pop_breaking_out
# Lines of code after breaking_out = True are unreachable and not
# typechecked.
self.breaking_out = False

# Whether the last pop changed the newly top frame on exit
self.last_pop_changed = False
# Whether the last pop was necessarily breaking out, and couldn't fall through
self.last_pop_breaking_out = False

self.try_frames = set() # type: Set[int]
self.loop_frames = [] # type: List[int]

def _add_dependencies(self, key: Key, value: Key = None) -> None:
if value is None:
value = key
else:
self.dependencies.setdefault(key, set()).add(value)
if isinstance(key, tuple):
for elt in key:
self._add_dependencies(elt, value)

def push_frame(self) -> Frame:
"""Push a new frame into the binder."""
f = Frame()
self.frames.append(f)
self.options_on_return.append([])
return f

def _push(self, key: Key, type: Type, index: int=-1) -> None:
self.frames[index][key] = type

def _get(self, key: Key, index: int=-1) -> Type:
if index < 0:
index += len(self.frames)
for i in range(index, -1, -1):
if key in self.frames[i]:
return self.frames[i][key]
return None

def push(self, expr: Node, typ: Type) -> None:
if not expr.literal:
return
key = expr.literal_hash
if key not in self.declarations:
self.declarations[key] = self.get_declaration(expr)
self._add_dependencies(key)
self._push(key, typ)

def get(self, expr: Node) -> Type:
return self._get(expr.literal_hash)

def cleanse(self, expr: Node) -> None:
"""Remove all references to a Node from the binder."""
self._cleanse_key(expr.literal_hash)

def _cleanse_key(self, key: Key) -> None:
"""Remove all references to a key from the binder."""
for frame in self.frames:
if key in frame:
del frame[key]

def update_from_options(self, frames: List[Frame]) -> bool:
"""Update the frame to reflect that each key will be updated
as in one of the frames. Return whether any item changes.

If a key is declared as AnyType, only update it if all the
options are the same.
"""

changed = False
keys = set(key for f in frames for key in f)

for key in keys:
current_value = self._get(key)
resulting_values = [f.get(key, current_value) for f in frames]
if any(x is None for x in resulting_values):
continue

if isinstance(self.declarations.get(key), AnyType):
type = resulting_values[0]
if not all(is_same_type(type, t) for t in resulting_values[1:]):
type = AnyType()
else:
type = resulting_values[0]
for other in resulting_values[1:]:
type = join_simple(self.declarations[key], type, other)
if not is_same_type(type, current_value):
self._push(key, type)
changed = True

return changed

def pop_frame(self, fall_through: int = 0) -> Frame:
"""Pop a frame and return it.

See frame_context() for documentation of fall_through.
"""
if fall_through and not self.breaking_out:
self.allow_jump(-fall_through)

result = self.frames.pop()
options = self.options_on_return.pop()

self.last_pop_changed = self.update_from_options(options)
self.last_pop_breaking_out = self.breaking_out

return result

def get_declaration(self, expr: Any) -> Type:
if hasattr(expr, 'node') and isinstance(expr.node, Var):
type = expr.node.type
if isinstance(type, PartialType):
return None
return type
else:
return None

def assign_type(self, expr: Node,
type: Type,
declared_type: Type,
restrict_any: bool = False) -> None:
if not expr.literal:
return
self.invalidate_dependencies(expr)

if declared_type is None:
# Not sure why this happens. It seems to mainly happen in
# member initialization.
return
if not is_subtype(type, declared_type):
# Pretty sure this is only happens when there's a type error.

# Ideally this function wouldn't be called if the
# expression has a type error, though -- do other kinds of
# errors cause this function to get called at invalid
# times?
return

# If x is Any and y is int, after x = y we do not infer that x is int.
# This could be changed.
# Eric: I'm changing it in weak typing mode, since Any is so common.

if (isinstance(self.most_recent_enclosing_type(expr, type), AnyType)
and not restrict_any):
pass
elif isinstance(type, AnyType):
self.push(expr, declared_type)
else:
self.push(expr, type)

for i in self.try_frames:
# XXX This should probably not copy the entire frame, but
# just copy this variable into a single stored frame.
self.allow_jump(i)

def invalidate_dependencies(self, expr: Node) -> None:
"""Invalidate knowledge of types that include expr, but not expr itself.

For example, when expr is foo.bar, invalidate foo.bar.baz.

It is overly conservative: it invalidates globally, including
in code paths unreachable from here.
"""
for dep in self.dependencies.get(expr.literal_hash, set()):
self._cleanse_key(dep)

def most_recent_enclosing_type(self, expr: Node, type: Type) -> Type:
if isinstance(type, AnyType):
return self.get_declaration(expr)
key = expr.literal_hash
enclosers = ([self.get_declaration(expr)] +
[f[key] for f in self.frames
if key in f and is_subtype(type, f[key])])
return enclosers[-1]

def allow_jump(self, index: int) -> None:
# self.frames and self.options_on_return have different lengths
# so make sure the index is positive
if index < 0:
index += len(self.options_on_return)
frame = Frame()
for f in self.frames[index + 1:]:
frame.update(f)
self.options_on_return[index].append(frame)

def push_loop_frame(self) -> None:
self.loop_frames.append(len(self.frames) - 1)

def pop_loop_frame(self) -> None:
self.loop_frames.pop()

@contextmanager
def frame_context(self, fall_through: int = 0) -> Iterator[Frame]:
"""Return a context manager that pushes/pops frames on enter/exit.

If fall_through > 0, then it will allow the frame to escape to
its ancestor `fall_through` levels higher.

A simple 'with binder.frame_context(): pass' will change the
last_pop_* flags but nothing else.
"""
was_breaking_out = self.breaking_out
yield self.push_frame()
self.pop_frame(fall_through)
self.breaking_out = was_breaking_out
Loading