Hide keyboard shortcuts

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""" 

4 

5import datetime 

6import inspect 

7from io import StringIO 

8import itertools 

9import pprint 

10import struct 

11import sys 

12from typing import List 

13 

14import numpy as np 

15 

16from pandas._libs.tslibs import Timestamp 

17from pandas.compat.chainmap import DeepChainMap 

18 

19 

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 ) 

31 

32 

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 

44 

45 return hex(hexin) 

46 

47 

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) 

53 

54 

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} 

65 

66 

67def _get_pretty_string(obj) -> str: 

68 """ 

69 Return a prettier version of obj. 

70 

71 Parameters 

72 ---------- 

73 obj : object 

74 Object to pretty print 

75 

76 Returns 

77 ------- 

78 str 

79 Pretty print object repr 

80 """ 

81 sio = StringIO() 

82 pprint.pprint(obj, stream=sio) 

83 return sio.getvalue() 

84 

85 

86class Scope: 

87 """ 

88 Object to hold scope, with a few bells to deal with some custom syntax 

89 and contexts added by pandas. 

90 

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 

98 

99 Attributes 

100 ---------- 

101 level : int 

102 scope : DeepChainMap 

103 target : object 

104 temps : dict 

105 """ 

106 

107 __slots__ = ["level", "scope", "target", "resolvers", "temps"] 

108 

109 def __init__( 

110 self, level, global_dict=None, local_dict=None, resolvers=(), target=None 

111 ): 

112 self.level = level + 1 

113 

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 

118 

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) 

124 

125 frame = sys._getframe(self.level) 

126 

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 

136 

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 = {} 

142 

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 

148 

149 @property 

150 def has_resolvers(self) -> bool: 

151 """ 

152 Return whether we have any extra scope. 

153 

154 For example, DataFrames pass Their columns as resolvers during calls to 

155 ``DataFrame.eval()`` and ``DataFrame.query()``. 

156 

157 Returns 

158 ------- 

159 hr : bool 

160 """ 

161 return bool(len(self.resolvers)) 

162 

163 def resolve(self, key: str, is_local: bool): 

164 """ 

165 Resolve a variable name in a possibly local context. 

166 

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) 

174 

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] 

184 

185 # not a local variable so check in resolvers if we have them 

186 if self.has_resolvers: 

187 return self.resolvers[key] 

188 

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 

202 

203 raise UndefinedVariableError(key, is_local) 

204 

205 def swapkey(self, old_key: str, new_key: str, new_value=None): 

206 """ 

207 Replace a variable name, with a potentially new value. 

208 

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 

222 

223 maps.append(self.temps) 

224 

225 for mapping in maps: 

226 if old_key in mapping: 

227 mapping[new_key] = new_value 

228 return 

229 

230 def _get_vars(self, stack, scopes: List[str]): 

231 """ 

232 Get specifically scoped variables from a list of stack frames. 

233 

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 

252 

253 def _update(self, level: int): 

254 """ 

255 Update the current scope by going back `level` levels. 

256 

257 Parameters 

258 ---------- 

259 level : int 

260 """ 

261 sl = level + 1 

262 

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() 

267 

268 try: 

269 self._get_vars(stack[:sl], scopes=["locals"]) 

270 finally: 

271 del stack[:], stack 

272 

273 def add_tmp(self, value) -> str: 

274 """ 

275 Add a temporary variable to the scope. 

276 

277 Parameters 

278 ---------- 

279 value : object 

280 An arbitrary object to be assigned to a temporary variable. 

281 

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)}" 

288 

289 # add to inner most scope 

290 assert name not in self.temps 

291 self.temps[name] = value 

292 assert name in self.temps 

293 

294 # only increment if the variable gets put in the scope 

295 return name 

296 

297 @property 

298 def ntemps(self) -> int: 

299 """The number of temporary variables in this scope""" 

300 return len(self.temps) 

301 

302 @property 

303 def full_scope(self): 

304 """ 

305 Return the full scope for use with passing to engines transparently 

306 as a mapping. 

307 

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)