Skip to content

Commit fc84fde

Browse files
committed
CLN/ENH: use Scope.swapkey() to update names
Also useful for replacing local variable names with their mangled versions.
1 parent 95ce712 commit fc84fde

File tree

3 files changed

+28
-304
lines changed

3 files changed

+28
-304
lines changed

pandas/computation/eval.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
"""Top level ``eval`` module.
44
"""
55

6-
6+
import sys
77
from pandas.core import common as com
8-
from pandas.computation.expr import Expr, _parsers, _ensure_scope
8+
from pandas.computation.expr import Expr, _parsers
9+
from pandas.computation.scope import _ensure_scope
10+
from pandas.compat import DeepChainMap, builtins
911
from pandas.computation.engines import _engines
1012
from distutils.version import LooseVersion
1113

@@ -117,7 +119,7 @@ def _convert_expression(expr):
117119

118120

119121
def eval(expr, parser='pandas', engine='numexpr', truediv=True,
120-
local_dict=None, global_dict=None, resolvers=None, level=2,
122+
local_dict=None, global_dict=None, resolvers=(), level=0,
121123
target=None):
122124
"""Evaluate a Python expression as a string using various backends.
123125
@@ -200,8 +202,10 @@ def eval(expr, parser='pandas', engine='numexpr', truediv=True,
200202
_check_resolvers(resolvers)
201203

202204
# get our (possibly passed-in) scope
203-
env = _ensure_scope(global_dict=global_dict, local_dict=local_dict,
204-
resolvers=resolvers, level=level, target=target)
205+
level += 1
206+
env = _ensure_scope(level, global_dict=global_dict,
207+
local_dict=local_dict, resolvers=resolvers,
208+
target=target)
205209

206210
parsed_expr = Expr(expr, engine=engine, parser=parser, env=env,
207211
truediv=truediv)

pandas/computation/expr.py

+7-252
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import inspect
88
import tokenize
99
import datetime
10-
import struct
1110

1211
from functools import partial
1312

@@ -16,225 +15,12 @@
1615
from pandas.compat import StringIO, zip, reduce, string_types
1716
from pandas.core.base import StringMixin
1817
from pandas.core import common as com
19-
from pandas.computation.common import NameResolutionError
2018
from pandas.computation.ops import (_cmp_ops_syms, _bool_ops_syms,
2119
_arith_ops_syms, _unary_ops_syms, is_term)
2220
from pandas.computation.ops import _reductions, _mathops, _LOCAL_TAG
2321
from pandas.computation.ops import Op, BinOp, UnaryOp, Term, Constant, Div
2422
from pandas.computation.ops import UndefinedVariableError
25-
26-
27-
def _ensure_scope(level=2, global_dict=None, local_dict=None, resolvers=None,
28-
target=None, **kwargs):
29-
"""Ensure that we are grabbing the correct scope."""
30-
return Scope(gbls=global_dict, lcls=local_dict, level=level,
31-
resolvers=resolvers, target=target)
32-
33-
34-
def _check_disjoint_resolver_names(resolver_keys, local_keys, global_keys):
35-
"""Make sure that variables in resolvers don't overlap with locals or
36-
globals.
37-
"""
38-
res_locals = list(com.intersection(resolver_keys, local_keys))
39-
if res_locals:
40-
msg = "resolvers and locals overlap on names {0}".format(res_locals)
41-
raise NameResolutionError(msg)
42-
43-
res_globals = list(com.intersection(resolver_keys, global_keys))
44-
if res_globals:
45-
msg = "resolvers and globals overlap on names {0}".format(res_globals)
46-
raise NameResolutionError(msg)
47-
48-
49-
def _replacer(x, pad_size):
50-
"""Replace a number with its padded hexadecimal representation. Used to tag
51-
temporary variables with their calling scope's id.
52-
"""
53-
# get the hex repr of the binary char and remove 0x and pad by pad_size
54-
# zeros
55-
try:
56-
hexin = ord(x)
57-
except TypeError:
58-
# bytes literals masquerade as ints when iterating in py3
59-
hexin = x
60-
61-
return hex(hexin).replace('0x', '').rjust(pad_size, '0')
62-
63-
64-
def _raw_hex_id(obj, pad_size=2):
65-
"""Return the padded hexadecimal id of ``obj``."""
66-
# interpret as a pointer since that's what really what id returns
67-
packed = struct.pack('@P', id(obj))
68-
69-
return ''.join(_replacer(x, pad_size) for x in packed)
70-
71-
72-
class Scope(StringMixin):
73-
74-
"""Object to hold scope, with a few bells to deal with some custom syntax
75-
added by pandas.
76-
77-
Parameters
78-
----------
79-
gbls : dict or None, optional, default None
80-
lcls : dict or Scope or None, optional, default None
81-
level : int, optional, default 1
82-
resolvers : list-like or None, optional, default None
83-
84-
Attributes
85-
----------
86-
globals : dict
87-
locals : dict
88-
level : int
89-
resolvers : tuple
90-
resolver_keys : frozenset
91-
"""
92-
__slots__ = ('globals', 'locals', 'resolvers', '_global_resolvers',
93-
'resolver_keys', '_resolver', 'level', 'ntemps', 'target')
94-
95-
def __init__(self, gbls=None, lcls=None, level=1, resolvers=None,
96-
target=None):
97-
self.level = level
98-
self.resolvers = tuple(resolvers or [])
99-
self.globals = dict()
100-
self.locals = dict()
101-
self.target = target
102-
self.ntemps = 1 # number of temporary variables in this scope
103-
104-
if isinstance(lcls, Scope):
105-
ld, lcls = lcls, dict()
106-
self.locals.update(ld.locals.copy())
107-
self.globals.update(ld.globals.copy())
108-
self.resolvers += ld.resolvers
109-
if ld.target is not None:
110-
self.target = ld.target
111-
self.update(ld.level)
112-
113-
frame = sys._getframe(level)
114-
try:
115-
self.globals.update(gbls or frame.f_globals)
116-
self.locals.update(lcls or frame.f_locals)
117-
finally:
118-
del frame
119-
120-
# add some useful defaults
121-
self.globals['Timestamp'] = pd.lib.Timestamp
122-
self.globals['datetime'] = datetime
123-
124-
# SUCH a hack
125-
self.globals['True'] = True
126-
self.globals['False'] = False
127-
128-
# function defs
129-
self.globals['list'] = list
130-
self.globals['tuple'] = tuple
131-
132-
res_keys = (list(o.keys()) for o in self.resolvers)
133-
self.resolver_keys = frozenset(reduce(operator.add, res_keys, []))
134-
self._global_resolvers = self.resolvers + (self.locals, self.globals)
135-
self._resolver = None
136-
137-
self.resolver_dict = {}
138-
for o in self.resolvers:
139-
self.resolver_dict.update(dict(o))
140-
141-
def __unicode__(self):
142-
return com.pprint_thing(
143-
'locals: {0}\nglobals: {0}\nresolvers: '
144-
'{0}\ntarget: {0}'.format(list(self.locals.keys()),
145-
list(self.globals.keys()),
146-
list(self.resolver_keys),
147-
self.target))
148-
149-
def __getitem__(self, key):
150-
return self.resolve(key, globally=False)
151-
152-
def resolve(self, key, globally=False):
153-
resolvers = self.locals, self.globals
154-
if globally:
155-
resolvers = self._global_resolvers
156-
157-
for resolver in resolvers:
158-
try:
159-
return resolver[key]
160-
except KeyError:
161-
pass
162-
163-
def update(self, level=None):
164-
"""Update the current scope by going back `level` levels.
165-
166-
Parameters
167-
----------
168-
level : int or None, optional, default None
169-
"""
170-
# we are always 2 levels below the caller
171-
# plus the caller may be below the env level
172-
# in which case we need addtl levels
173-
sl = 2
174-
if level is not None:
175-
sl += level
176-
177-
# add sl frames to the scope starting with the
178-
# most distant and overwritting with more current
179-
# makes sure that we can capture variable scope
180-
frame = inspect.currentframe()
181-
try:
182-
frames = []
183-
while sl >= 0:
184-
frame = frame.f_back
185-
sl -= 1
186-
if frame is None:
187-
break
188-
frames.append(frame)
189-
for f in frames[::-1]:
190-
self.locals.update(f.f_locals)
191-
self.globals.update(f.f_globals)
192-
finally:
193-
del frame, frames
194-
195-
def add_tmp(self, value, where='locals'):
196-
"""Add a temporary variable to the scope.
197-
198-
Parameters
199-
----------
200-
value : object
201-
An arbitrary object to be assigned to a temporary variable.
202-
where : basestring, optional, default 'locals', {'locals', 'globals'}
203-
What scope to add the value to.
204-
205-
Returns
206-
-------
207-
name : basestring
208-
The name of the temporary variable created.
209-
"""
210-
d = getattr(self, where, None)
211-
212-
if d is None:
213-
raise AttributeError("Cannot add value to non-existent scope "
214-
"{0!r}".format(where))
215-
if not isinstance(d, dict):
216-
raise TypeError("Cannot add value to object of type {0!r}, "
217-
"scope must be a dictionary"
218-
"".format(type(d).__name__))
219-
name = 'tmp_var_{0}_{1}_{2}'.format(type(value).__name__, self.ntemps,
220-
_raw_hex_id(self))
221-
d[name] = value
222-
223-
# only increment if the variable gets put in the scope
224-
self.ntemps += 1
225-
return name
226-
227-
def remove_tmp(self, name, where='locals'):
228-
d = getattr(self, where, None)
229-
if d is None:
230-
raise AttributeError("Cannot remove value from non-existent scope "
231-
"{0!r}".format(where))
232-
if not isinstance(d, dict):
233-
raise TypeError("Cannot remove value from object of type {0!r}, "
234-
"scope must be a dictionary"
235-
"".format(type(d).__name__))
236-
del d[name]
237-
self.ntemps -= 1
23+
from pandas.computation.scope import Scope, _ensure_scope
23824

23925

24026
def _rewrite_assign(source):
@@ -549,8 +335,8 @@ def visit_BinOp(self, node, **kwargs):
549335
return self._possibly_evaluate_binop(op, op_class, left, right)
550336

551337
def visit_Div(self, node, **kwargs):
552-
return lambda lhs, rhs: Div(lhs, rhs,
553-
truediv=self.env.locals['truediv'])
338+
truediv = self.env.scope['truediv']
339+
return lambda lhs, rhs: Div(lhs, rhs, truediv)
554340

555341
def visit_UnaryOp(self, node, **kwargs):
556342
op = self.visit(node.op)
@@ -631,15 +417,14 @@ def visit_Assign(self, node, **kwargs):
631417

632418
try:
633419
assigner = self.visit(node.targets[0], **kwargs)
634-
except (UndefinedVariableError, KeyError):
420+
except UndefinedVariableError:
635421
assigner = node.targets[0].id
636422

637423
self.assigner = getattr(assigner, 'name', assigner)
638424
if self.assigner is None:
639425
raise SyntaxError('left hand side of an assignment must be a '
640426
'single resolvable name')
641427

642-
import ipdb; ipdb.set_trace()
643428
return self.visit(node.value, **kwargs)
644429

645430
def visit_Attribute(self, node, **kwargs):
@@ -769,21 +554,20 @@ class Expr(StringMixin):
769554
"""
770555

771556
def __init__(self, expr, engine='numexpr', parser='pandas', env=None,
772-
truediv=True, level=2):
557+
truediv=True, level=0):
773558
self.expr = expr
774-
self.env = _ensure_scope(level=level, local_dict=env)
559+
self.env = env or Scope(level=level + 1)
775560
self.engine = engine
776561
self.parser = parser
562+
self.env.scope['truediv'] = truediv
777563
self._visitor = _parsers[parser](self.env, self.engine, self.parser)
778564
self.terms = self.parse()
779-
self.truediv = truediv
780565

781566
@property
782567
def assigner(self):
783568
return getattr(self._visitor, 'assigner', None)
784569

785570
def __call__(self):
786-
self.env.locals['truediv'] = self.truediv
787571
return self.terms(self.env)
788572

789573
def __unicode__(self):
@@ -807,34 +591,5 @@ def names(self):
807591
return frozenset([self.terms.name])
808592
return frozenset(term.name for term in com.flatten(self.terms))
809593

810-
def check_name_clashes(self):
811-
env = self.env
812-
names = self.names
813-
res_keys = frozenset(env.resolver_dict.keys()) & names
814-
lcl_keys = frozenset(env.locals.keys()) & names
815-
gbl_keys = frozenset(env.globals.keys()) & names
816-
_check_disjoint_resolver_names(res_keys, lcl_keys, gbl_keys)
817-
818-
def add_resolvers_to_locals(self):
819-
"""Add the extra scope (resolvers) to local scope
820-
821-
Notes
822-
-----
823-
This should be done after parsing and pre-evaluation, otherwise
824-
unnecessary name clashes will occur.
825-
"""
826-
self.env.locals.update(self.env.resolver_dict)
827-
828-
829-
def isexpr(s, check_names=True):
830-
"""Strict checking for a valid expression."""
831-
try:
832-
Expr(s, env=_ensure_scope() if check_names else None)
833-
except SyntaxError:
834-
return False
835-
except NameError:
836-
return not check_names
837-
return True
838-
839594

840595
_parsers = {'python': PythonExprVisitor, 'pandas': PandasExprVisitor}

0 commit comments

Comments
 (0)