Skip to content

Commit 95ce712

Browse files
committed
ENH: backport Python 3.3 ChainMap
ChainMap implements a list of mappings that effectively functions as a single dictionary. This class is very useful for implementing scope. This commit also adds a DeepChainMap subclass of ChainMap for writing and deleting keys.
1 parent 150f323 commit 95ce712

File tree

8 files changed

+418
-55
lines changed

8 files changed

+418
-55
lines changed

pandas/compat/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import pickle as cPickle
5555
import http.client as httplib
5656

57+
from chainmap import DeepChainMap
58+
5759

5860
if PY3:
5961
def isidentifier(s):

pandas/compat/chainmap.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
try:
2+
from collections import ChainMap
3+
except ImportError:
4+
from chainmap_impl import ChainMap
5+
6+
7+
class DeepChainMap(ChainMap):
8+
def __setitem__(self, key, value):
9+
for mapping in self.maps:
10+
if key in mapping:
11+
mapping[key] = value
12+
return
13+
self.maps[0][key] = value
14+
15+
def __delitem__(self, key):
16+
for mapping in self.maps:
17+
if key in mapping:
18+
del mapping[key]
19+
return
20+
raise KeyError(key)

pandas/compat/chainmap_impl.py

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from collections import MutableMapping
2+
from thread import get_ident
3+
4+
5+
def recursive_repr(fillvalue='...'):
6+
'Decorator to make a repr function return fillvalue for a recursive call'
7+
8+
def decorating_function(user_function):
9+
repr_running = set()
10+
11+
def wrapper(self):
12+
key = id(self), get_ident()
13+
if key in repr_running:
14+
return fillvalue
15+
repr_running.add(key)
16+
try:
17+
result = user_function(self)
18+
finally:
19+
repr_running.discard(key)
20+
return result
21+
22+
# Can't use functools.wraps() here because of bootstrap issues
23+
wrapper.__module__ = getattr(user_function, '__module__')
24+
wrapper.__doc__ = getattr(user_function, '__doc__')
25+
wrapper.__name__ = getattr(user_function, '__name__')
26+
return wrapper
27+
28+
return decorating_function
29+
30+
31+
class ChainMap(MutableMapping):
32+
''' A ChainMap groups multiple dicts (or other mappings) together
33+
to create a single, updateable view.
34+
35+
The underlying mappings are stored in a list. That list is public and can
36+
accessed or updated using the *maps* attribute. There is no other state.
37+
38+
Lookups search the underlying mappings successively until a key is found.
39+
In contrast, writes, updates, and deletions only operate on the first
40+
mapping.
41+
42+
'''
43+
44+
def __init__(self, *maps):
45+
'''Initialize a ChainMap by setting *maps* to the given mappings.
46+
If no mappings are provided, a single empty dictionary is used.
47+
48+
'''
49+
self.maps = list(maps) or [{}] # always at least one map
50+
51+
def __missing__(self, key):
52+
raise KeyError(key)
53+
54+
def __getitem__(self, key):
55+
for mapping in self.maps:
56+
try:
57+
return mapping[key] # can't use 'key in mapping' with defaultdict
58+
except KeyError:
59+
pass
60+
return self.__missing__(key) # support subclasses that define __missing__
61+
62+
def get(self, key, default=None):
63+
return self[key] if key in self else default
64+
65+
def __len__(self):
66+
return len(set().union(*self.maps)) # reuses stored hash values if possible
67+
68+
def __iter__(self):
69+
return iter(set().union(*self.maps))
70+
71+
def __contains__(self, key):
72+
return any(key in m for m in self.maps)
73+
74+
def __bool__(self):
75+
return any(self.maps)
76+
77+
@recursive_repr()
78+
def __repr__(self):
79+
return '{0.__class__.__name__}({1})'.format(
80+
self, ', '.join(repr(m) for m in self.maps))
81+
82+
@classmethod
83+
def fromkeys(cls, iterable, *args):
84+
'Create a ChainMap with a single dict created from the iterable.'
85+
return cls(dict.fromkeys(iterable, *args))
86+
87+
def copy(self):
88+
'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]'
89+
return self.__class__(self.maps[0].copy(), *self.maps[1:])
90+
91+
__copy__ = copy
92+
93+
def new_child(self, m=None): # like Django's Context.push()
94+
'''
95+
New ChainMap with a new map followed by all previous maps. If no
96+
map is provided, an empty dict is used.
97+
'''
98+
if m is None:
99+
m = {}
100+
return self.__class__(m, *self.maps)
101+
102+
@property
103+
def parents(self): # like Django's Context.pop()
104+
'New ChainMap from maps[1:].'
105+
return self.__class__(*self.maps[1:])
106+
107+
def __setitem__(self, key, value):
108+
self.maps[0][key] = value
109+
110+
def __delitem__(self, key):
111+
try:
112+
del self.maps[0][key]
113+
except KeyError:
114+
raise KeyError('Key not found in the first mapping: {!r}'.format(key))
115+
116+
def popitem(self):
117+
'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.'
118+
try:
119+
return self.maps[0].popitem()
120+
except KeyError:
121+
raise KeyError('No keys found in the first mapping.')
122+
123+
def pop(self, key, *args):
124+
'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].'
125+
try:
126+
return self.maps[0].pop(key, *args)
127+
except KeyError:
128+
raise KeyError('Key not found in the first mapping: {!r}'.format(key))
129+
130+
def clear(self):
131+
'Clear maps[0], leaving maps[1:] intact.'
132+
self.maps[0].clear()

pandas/computation/engines.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import abc
55

66
from pandas import compat
7+
from pandas.compat import DeepChainMap
78
from pandas.core import common as com
89
from pandas.computation.align import _align, _reconstruct_object
910
from pandas.computation.ops import UndefinedVariableError
@@ -29,9 +30,6 @@ def convert(self):
2930
"""
3031
return com.pprint_thing(self.expr)
3132

32-
def pre_evaluate(self):
33-
self.expr.check_name_clashes()
34-
3533
def evaluate(self):
3634
"""Run the engine on the expression
3735
@@ -47,7 +45,6 @@ def evaluate(self):
4745
self.result_type, self.aligned_axes = _align(self.expr.terms)
4846

4947
# make sure no names in resolvers and locals/globals clash
50-
self.pre_evaluate()
5148
res = self._evaluate()
5249
return _reconstruct_object(self.result_type, res, self.aligned_axes,
5350
self.expr.terms.return_type)
@@ -87,16 +84,14 @@ def convert(self):
8784
def _evaluate(self):
8885
import numexpr as ne
8986

90-
# add the resolvers to locals
91-
self.expr.add_resolvers_to_locals()
92-
9387
# convert the expression to a valid numexpr expression
9488
s = self.convert()
9589

9690
try:
97-
return ne.evaluate(s, local_dict=self.expr.env.locals,
98-
global_dict=self.expr.env.globals,
99-
truediv=self.expr.truediv)
91+
env = self.expr.env
92+
full_scope = DeepChainMap(*(env.resolvers.maps + env.scope.maps))
93+
return ne.evaluate(s, local_dict=full_scope,
94+
truediv=env.scope['truediv'])
10095
except KeyError as e:
10196
# python 3 compat kludge
10297
try:
@@ -118,7 +113,6 @@ def __init__(self, expr):
118113
super(PythonEngine, self).__init__(expr)
119114

120115
def evaluate(self):
121-
self.pre_evaluate()
122116
return self.expr()
123117

124118
def _evaluate(self):

pandas/computation/expr.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -631,14 +631,15 @@ def visit_Assign(self, node, **kwargs):
631631

632632
try:
633633
assigner = self.visit(node.targets[0], **kwargs)
634-
except UndefinedVariableError:
634+
except (UndefinedVariableError, KeyError):
635635
assigner = node.targets[0].id
636636

637637
self.assigner = getattr(assigner, 'name', assigner)
638638
if self.assigner is None:
639639
raise SyntaxError('left hand side of an assignment must be a '
640640
'single resolvable name')
641641

642+
import ipdb; ipdb.set_trace()
642643
return self.visit(node.value, **kwargs)
643644

644645
def visit_Attribute(self, node, **kwargs):

0 commit comments

Comments
 (0)