Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/computation/scope.py: 34%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

127 statements  

1""" 

2Module for scope operations 

3""" 

4 

5from __future__ import annotations 

6 

7from collections import ChainMap 

8import datetime 

9import inspect 

10from io import StringIO 

11import itertools 

12import pprint 

13import struct 

14import sys 

15from typing import TypeVar 

16 

17import numpy as np 

18 

19from pandas._libs.tslibs import Timestamp 

20from pandas.errors import UndefinedVariableError 

21 

22_KT = TypeVar("_KT") 

23_VT = TypeVar("_VT") 

24 

25 

26# https://docs.python.org/3/library/collections.html#chainmap-examples-and-recipes 

27class DeepChainMap(ChainMap[_KT, _VT]): 

28 """ 

29 Variant of ChainMap that allows direct updates to inner scopes. 

30 

31 Only works when all passed mapping are mutable. 

32 """ 

33 

34 def __setitem__(self, key: _KT, value: _VT) -> None: 

35 for mapping in self.maps: 

36 if key in mapping: 

37 mapping[key] = value 

38 return 

39 self.maps[0][key] = value 

40 

41 def __delitem__(self, key: _KT) -> None: 

42 """ 

43 Raises 

44 ------ 

45 KeyError 

46 If `key` doesn't exist. 

47 """ 

48 for mapping in self.maps: 

49 if key in mapping: 

50 del mapping[key] 

51 return 

52 raise KeyError(key) 

53 

54 

55def ensure_scope( 

56 level: int, global_dict=None, local_dict=None, resolvers=(), target=None 

57) -> Scope: 

58 """Ensure that we are grabbing the correct scope.""" 

59 return Scope( 

60 level + 1, 

61 global_dict=global_dict, 

62 local_dict=local_dict, 

63 resolvers=resolvers, 

64 target=target, 

65 ) 

66 

67 

68def _replacer(x) -> str: 

69 """ 

70 Replace a number with its hexadecimal representation. Used to tag 

71 temporary variables with their calling scope's id. 

72 """ 

73 # get the hex repr of the binary char and remove 0x and pad by pad_size 

74 # zeros 

75 try: 

76 hexin = ord(x) 

77 except TypeError: 

78 # bytes literals masquerade as ints when iterating in py3 

79 hexin = x 

80 

81 return hex(hexin) 

82 

83 

84def _raw_hex_id(obj) -> str: 

85 """Return the padded hexadecimal id of ``obj``.""" 

86 # interpret as a pointer since that's what really what id returns 

87 packed = struct.pack("@P", id(obj)) 

88 return "".join([_replacer(x) for x in packed]) 

89 

90 

91DEFAULT_GLOBALS = { 

92 "Timestamp": Timestamp, 

93 "datetime": datetime.datetime, 

94 "True": True, 

95 "False": False, 

96 "list": list, 

97 "tuple": tuple, 

98 "inf": np.inf, 

99 "Inf": np.inf, 

100} 

101 

102 

103def _get_pretty_string(obj) -> str: 

104 """ 

105 Return a prettier version of obj. 

106 

107 Parameters 

108 ---------- 

109 obj : object 

110 Object to pretty print 

111 

112 Returns 

113 ------- 

114 str 

115 Pretty print object repr 

116 """ 

117 sio = StringIO() 

118 pprint.pprint(obj, stream=sio) 

119 return sio.getvalue() 

120 

121 

122class Scope: 

123 """ 

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

125 and contexts added by pandas. 

126 

127 Parameters 

128 ---------- 

129 level : int 

130 global_dict : dict or None, optional, default None 

131 local_dict : dict or Scope or None, optional, default None 

132 resolvers : list-like or None, optional, default None 

133 target : object 

134 

135 Attributes 

136 ---------- 

137 level : int 

138 scope : DeepChainMap 

139 target : object 

140 temps : dict 

141 """ 

142 

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

144 level: int 

145 scope: DeepChainMap 

146 resolvers: DeepChainMap 

147 temps: dict 

148 

149 def __init__( 

150 self, level: int, global_dict=None, local_dict=None, resolvers=(), target=None 

151 ) -> None: 

152 self.level = level + 1 

153 

154 # shallow copy because we don't want to keep filling this up with what 

155 # was there before if there are multiple calls to Scope/_ensure_scope 

156 self.scope = DeepChainMap(DEFAULT_GLOBALS.copy()) 

157 self.target = target 

158 

159 if isinstance(local_dict, Scope): 

160 self.scope.update(local_dict.scope) 

161 if local_dict.target is not None: 

162 self.target = local_dict.target 

163 self._update(local_dict.level) 

164 

165 frame = sys._getframe(self.level) 

166 

167 try: 

168 # shallow copy here because we don't want to replace what's in 

169 # scope when we align terms (alignment accesses the underlying 

170 # numpy array of pandas objects) 

171 scope_global = self.scope.new_child( 

172 (global_dict if global_dict is not None else frame.f_globals).copy() 

173 ) 

174 self.scope = DeepChainMap(scope_global) 

175 if not isinstance(local_dict, Scope): 

176 scope_local = self.scope.new_child( 

177 (local_dict if local_dict is not None else frame.f_locals).copy() 

178 ) 

179 self.scope = DeepChainMap(scope_local) 

180 finally: 

181 del frame 

182 

183 # assumes that resolvers are going from outermost scope to inner 

184 if isinstance(local_dict, Scope): 

185 resolvers += tuple(local_dict.resolvers.maps) 

186 self.resolvers = DeepChainMap(*resolvers) 

187 self.temps = {} 

188 

189 def __repr__(self) -> str: 

190 scope_keys = _get_pretty_string(list(self.scope.keys())) 

191 res_keys = _get_pretty_string(list(self.resolvers.keys())) 

192 return f"{type(self).__name__}(scope={scope_keys}, resolvers={res_keys})" 

193 

194 @property 

195 def has_resolvers(self) -> bool: 

196 """ 

197 Return whether we have any extra scope. 

198 

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

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

201 

202 Returns 

203 ------- 

204 hr : bool 

205 """ 

206 return bool(len(self.resolvers)) 

207 

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

209 """ 

210 Resolve a variable name in a possibly local context. 

211 

212 Parameters 

213 ---------- 

214 key : str 

215 A variable name 

216 is_local : bool 

217 Flag indicating whether the variable is local or not (prefixed with 

218 the '@' symbol) 

219 

220 Returns 

221 ------- 

222 value : object 

223 The value of a particular variable 

224 """ 

225 try: 

226 # only look for locals in outer scope 

227 if is_local: 

228 return self.scope[key] 

229 

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

231 if self.has_resolvers: 

232 return self.resolvers[key] 

233 

234 # if we're here that means that we have no locals and we also have 

235 # no resolvers 

236 assert not is_local and not self.has_resolvers 

237 return self.scope[key] 

238 except KeyError: 

239 try: 

240 # last ditch effort we look in temporaries 

241 # these are created when parsing indexing expressions 

242 # e.g., df[df > 0] 

243 return self.temps[key] 

244 except KeyError as err: 

245 raise UndefinedVariableError(key, is_local) from err 

246 

247 def swapkey(self, old_key: str, new_key: str, new_value=None) -> None: 

248 """ 

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

250 

251 Parameters 

252 ---------- 

253 old_key : str 

254 Current variable name to replace 

255 new_key : str 

256 New variable name to replace `old_key` with 

257 new_value : object 

258 Value to be replaced along with the possible renaming 

259 """ 

260 if self.has_resolvers: 

261 maps = self.resolvers.maps + self.scope.maps 

262 else: 

263 maps = self.scope.maps 

264 

265 maps.append(self.temps) 

266 

267 for mapping in maps: 

268 if old_key in mapping: 

269 mapping[new_key] = new_value 

270 return 

271 

272 def _get_vars(self, stack, scopes: list[str]) -> None: 

273 """ 

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

275 

276 Parameters 

277 ---------- 

278 stack : list 

279 A list of stack frames as returned by ``inspect.stack()`` 

280 scopes : sequence of strings 

281 A sequence containing valid stack frame attribute names that 

282 evaluate to a dictionary. For example, ('locals', 'globals') 

283 """ 

284 variables = itertools.product(scopes, stack) 

285 for scope, (frame, _, _, _, _, _) in variables: 

286 try: 

287 d = getattr(frame, f"f_{scope}") 

288 self.scope = DeepChainMap(self.scope.new_child(d)) 

289 finally: 

290 # won't remove it, but DECREF it 

291 # in Py3 this probably isn't necessary since frame won't be 

292 # scope after the loop 

293 del frame 

294 

295 def _update(self, level: int) -> None: 

296 """ 

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

298 

299 Parameters 

300 ---------- 

301 level : int 

302 """ 

303 sl = level + 1 

304 

305 # add sl frames to the scope starting with the 

306 # most distant and overwriting with more current 

307 # makes sure that we can capture variable scope 

308 stack = inspect.stack() 

309 

310 try: 

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

312 finally: 

313 del stack[:], stack 

314 

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

316 """ 

317 Add a temporary variable to the scope. 

318 

319 Parameters 

320 ---------- 

321 value : object 

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

323 

324 Returns 

325 ------- 

326 str 

327 The name of the temporary variable created. 

328 """ 

329 name = f"{type(value).__name__}_{self.ntemps}_{_raw_hex_id(self)}" 

330 

331 # add to inner most scope 

332 assert name not in self.temps 

333 self.temps[name] = value 

334 assert name in self.temps 

335 

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

337 return name 

338 

339 @property 

340 def ntemps(self) -> int: 

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

342 return len(self.temps) 

343 

344 @property 

345 def full_scope(self) -> DeepChainMap: 

346 """ 

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

348 as a mapping. 

349 

350 Returns 

351 ------- 

352 vars : DeepChainMap 

353 All variables in this scope. 

354 """ 

355 maps = [self.temps, *self.resolvers.maps, *self.scope.maps] 

356 return DeepChainMap(*maps)