Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pandas/core/computation/scope.py : 29%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Module for scope operations
3"""
5import datetime
6import inspect
7from io import StringIO
8import itertools
9import pprint
10import struct
11import sys
12from typing import List
14import numpy as np
16from pandas._libs.tslibs import Timestamp
17from pandas.compat.chainmap import DeepChainMap
20def ensure_scope(
21 level: int, global_dict=None, local_dict=None, resolvers=(), target=None, **kwargs
22) -> "Scope":
23 """Ensure that we are grabbing the correct scope."""
24 return Scope(
25 level + 1,
26 global_dict=global_dict,
27 local_dict=local_dict,
28 resolvers=resolvers,
29 target=target,
30 )
33def _replacer(x) -> str:
34 """Replace a number with its hexadecimal representation. Used to tag
35 temporary variables with their calling scope's id.
36 """
37 # get the hex repr of the binary char and remove 0x and pad by pad_size
38 # zeros
39 try:
40 hexin = ord(x)
41 except TypeError:
42 # bytes literals masquerade as ints when iterating in py3
43 hexin = x
45 return hex(hexin)
48def _raw_hex_id(obj) -> str:
49 """Return the padded hexadecimal id of ``obj``."""
50 # interpret as a pointer since that's what really what id returns
51 packed = struct.pack("@P", id(obj))
52 return "".join(_replacer(x) for x in packed)
55_DEFAULT_GLOBALS = {
56 "Timestamp": Timestamp,
57 "datetime": datetime.datetime,
58 "True": True,
59 "False": False,
60 "list": list,
61 "tuple": tuple,
62 "inf": np.inf,
63 "Inf": np.inf,
64}
67def _get_pretty_string(obj) -> str:
68 """
69 Return a prettier version of obj.
71 Parameters
72 ----------
73 obj : object
74 Object to pretty print
76 Returns
77 -------
78 str
79 Pretty print object repr
80 """
81 sio = StringIO()
82 pprint.pprint(obj, stream=sio)
83 return sio.getvalue()
86class Scope:
87 """
88 Object to hold scope, with a few bells to deal with some custom syntax
89 and contexts added by pandas.
91 Parameters
92 ----------
93 level : int
94 global_dict : dict or None, optional, default None
95 local_dict : dict or Scope or None, optional, default None
96 resolvers : list-like or None, optional, default None
97 target : object
99 Attributes
100 ----------
101 level : int
102 scope : DeepChainMap
103 target : object
104 temps : dict
105 """
107 __slots__ = ["level", "scope", "target", "resolvers", "temps"]
109 def __init__(
110 self, level, global_dict=None, local_dict=None, resolvers=(), target=None
111 ):
112 self.level = level + 1
114 # shallow copy because we don't want to keep filling this up with what
115 # was there before if there are multiple calls to Scope/_ensure_scope
116 self.scope = DeepChainMap(_DEFAULT_GLOBALS.copy())
117 self.target = target
119 if isinstance(local_dict, Scope):
120 self.scope.update(local_dict.scope)
121 if local_dict.target is not None:
122 self.target = local_dict.target
123 self._update(local_dict.level)
125 frame = sys._getframe(self.level)
127 try:
128 # shallow copy here because we don't want to replace what's in
129 # scope when we align terms (alignment accesses the underlying
130 # numpy array of pandas objects)
131 self.scope = self.scope.new_child((global_dict or frame.f_globals).copy())
132 if not isinstance(local_dict, Scope):
133 self.scope = self.scope.new_child((local_dict or frame.f_locals).copy())
134 finally:
135 del frame
137 # assumes that resolvers are going from outermost scope to inner
138 if isinstance(local_dict, Scope):
139 resolvers += tuple(local_dict.resolvers.maps)
140 self.resolvers = DeepChainMap(*resolvers)
141 self.temps = {}
143 def __repr__(self) -> str:
144 scope_keys = _get_pretty_string(list(self.scope.keys()))
145 res_keys = _get_pretty_string(list(self.resolvers.keys()))
146 unicode_str = f"{type(self).__name__}(scope={scope_keys}, resolvers={res_keys})"
147 return unicode_str
149 @property
150 def has_resolvers(self) -> bool:
151 """
152 Return whether we have any extra scope.
154 For example, DataFrames pass Their columns as resolvers during calls to
155 ``DataFrame.eval()`` and ``DataFrame.query()``.
157 Returns
158 -------
159 hr : bool
160 """
161 return bool(len(self.resolvers))
163 def resolve(self, key: str, is_local: bool):
164 """
165 Resolve a variable name in a possibly local context.
167 Parameters
168 ----------
169 key : str
170 A variable name
171 is_local : bool
172 Flag indicating whether the variable is local or not (prefixed with
173 the '@' symbol)
175 Returns
176 -------
177 value : object
178 The value of a particular variable
179 """
180 try:
181 # only look for locals in outer scope
182 if is_local:
183 return self.scope[key]
185 # not a local variable so check in resolvers if we have them
186 if self.has_resolvers:
187 return self.resolvers[key]
189 # if we're here that means that we have no locals and we also have
190 # no resolvers
191 assert not is_local and not self.has_resolvers
192 return self.scope[key]
193 except KeyError:
194 try:
195 # last ditch effort we look in temporaries
196 # these are created when parsing indexing expressions
197 # e.g., df[df > 0]
198 return self.temps[key]
199 except KeyError:
200 # runtime import because ops imports from scope
201 from pandas.core.computation.ops import UndefinedVariableError
203 raise UndefinedVariableError(key, is_local)
205 def swapkey(self, old_key: str, new_key: str, new_value=None):
206 """
207 Replace a variable name, with a potentially new value.
209 Parameters
210 ----------
211 old_key : str
212 Current variable name to replace
213 new_key : str
214 New variable name to replace `old_key` with
215 new_value : object
216 Value to be replaced along with the possible renaming
217 """
218 if self.has_resolvers:
219 maps = self.resolvers.maps + self.scope.maps
220 else:
221 maps = self.scope.maps
223 maps.append(self.temps)
225 for mapping in maps:
226 if old_key in mapping:
227 mapping[new_key] = new_value
228 return
230 def _get_vars(self, stack, scopes: List[str]):
231 """
232 Get specifically scoped variables from a list of stack frames.
234 Parameters
235 ----------
236 stack : list
237 A list of stack frames as returned by ``inspect.stack()``
238 scopes : sequence of strings
239 A sequence containing valid stack frame attribute names that
240 evaluate to a dictionary. For example, ('locals', 'globals')
241 """
242 variables = itertools.product(scopes, stack)
243 for scope, (frame, _, _, _, _, _) in variables:
244 try:
245 d = getattr(frame, "f_" + scope)
246 self.scope = self.scope.new_child(d)
247 finally:
248 # won't remove it, but DECREF it
249 # in Py3 this probably isn't necessary since frame won't be
250 # scope after the loop
251 del frame
253 def _update(self, level: int):
254 """
255 Update the current scope by going back `level` levels.
257 Parameters
258 ----------
259 level : int
260 """
261 sl = level + 1
263 # add sl frames to the scope starting with the
264 # most distant and overwriting with more current
265 # makes sure that we can capture variable scope
266 stack = inspect.stack()
268 try:
269 self._get_vars(stack[:sl], scopes=["locals"])
270 finally:
271 del stack[:], stack
273 def add_tmp(self, value) -> str:
274 """
275 Add a temporary variable to the scope.
277 Parameters
278 ----------
279 value : object
280 An arbitrary object to be assigned to a temporary variable.
282 Returns
283 -------
284 str
285 The name of the temporary variable created.
286 """
287 name = f"{type(value).__name__}_{self.ntemps}_{_raw_hex_id(self)}"
289 # add to inner most scope
290 assert name not in self.temps
291 self.temps[name] = value
292 assert name in self.temps
294 # only increment if the variable gets put in the scope
295 return name
297 @property
298 def ntemps(self) -> int:
299 """The number of temporary variables in this scope"""
300 return len(self.temps)
302 @property
303 def full_scope(self):
304 """
305 Return the full scope for use with passing to engines transparently
306 as a mapping.
308 Returns
309 -------
310 vars : DeepChainMap
311 All variables in this scope.
312 """
313 maps = [self.temps] + self.resolvers.maps + self.scope.maps
314 return DeepChainMap(*maps)