scope.py 10.0 KB

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