1"""
2Top level ``eval`` module.
3"""
4
5from __future__ import annotations
6
7import tokenize
8from typing import (
9 TYPE_CHECKING,
10 Any,
11)
12import warnings
13
14from pandas.util._decorators import set_module
15from pandas.util._exceptions import find_stack_level
16from pandas.util._validators import validate_bool_kwarg
17
18from pandas.core.dtypes.common import (
19 is_extension_array_dtype,
20 is_string_dtype,
21)
22
23from pandas.core.computation.engines import ENGINES
24from pandas.core.computation.expr import (
25 PARSERS,
26 Expr,
27)
28from pandas.core.computation.parsing import tokenize_string
29from pandas.core.computation.scope import ensure_scope
30from pandas.core.generic import NDFrame
31
32from pandas.io.formats.printing import pprint_thing
33
34if TYPE_CHECKING:
35 from pandas.core.computation.ops import BinOp
36
37
38def _check_engine(engine: str | None) -> str:
39 """
40 Make sure a valid engine is passed.
41
42 Parameters
43 ----------
44 engine : str
45 String to validate.
46
47 Raises
48 ------
49 KeyError
50 * If an invalid engine is passed.
51 ImportError
52 * If numexpr was requested but doesn't exist.
53
54 Returns
55 -------
56 str
57 Engine name.
58 """
59 from pandas.core.computation.check import NUMEXPR_INSTALLED
60 from pandas.core.computation.expressions import USE_NUMEXPR
61
62 if engine is None:
63 engine = "numexpr" if USE_NUMEXPR else "python"
64
65 if engine not in ENGINES:
66 valid_engines = list(ENGINES.keys())
67 raise KeyError(
68 f"Invalid engine '{engine}' passed, valid engines are {valid_engines}"
69 )
70
71 # TODO: validate this in a more general way (thinking of future engines
72 # that won't necessarily be import-able)
73 # Could potentially be done on engine instantiation
74 if engine == "numexpr" and not NUMEXPR_INSTALLED:
75 raise ImportError(
76 "'numexpr' is not installed or an unsupported version. Cannot use "
77 "engine='numexpr' for query/eval if 'numexpr' is not installed"
78 )
79
80 return engine
81
82
83def _check_parser(parser: str) -> None:
84 """
85 Make sure a valid parser is passed.
86
87 Parameters
88 ----------
89 parser : str
90
91 Raises
92 ------
93 KeyError
94 * If an invalid parser is passed
95 """
96 if parser not in PARSERS:
97 raise KeyError(
98 f"Invalid parser '{parser}' passed, valid parsers are {PARSERS.keys()}"
99 )
100
101
102def _check_resolvers(resolvers) -> None:
103 if resolvers is not None:
104 for resolver in resolvers:
105 if not hasattr(resolver, "__getitem__"):
106 name = type(resolver).__name__
107 raise TypeError(
108 f"Resolver of type '{name}' does not "
109 "implement the __getitem__ method"
110 )
111
112
113def _check_expression(expr) -> None:
114 """
115 Make sure an expression is not an empty string
116
117 Parameters
118 ----------
119 expr : object
120 An object that can be converted to a string
121
122 Raises
123 ------
124 ValueError
125 * If expr is an empty string
126 """
127 if not expr:
128 raise ValueError("expr cannot be an empty string")
129
130
131def _convert_expression(expr) -> str:
132 """
133 Convert an object to an expression.
134
135 This function converts an object to an expression (a unicode string) and
136 checks to make sure it isn't empty after conversion. This is used to
137 convert operators to their string representation for recursive calls to
138 :func:`~pandas.eval`.
139
140 Parameters
141 ----------
142 expr : object
143 The object to be converted to a string.
144
145 Returns
146 -------
147 str
148 The string representation of an object.
149
150 Raises
151 ------
152 ValueError
153 * If the expression is empty.
154 """
155 s = pprint_thing(expr)
156 _check_expression(s)
157 return s
158
159
160def _check_for_locals(expr: str, stack_level: int, parser: str) -> None:
161 at_top_of_stack = stack_level == 0
162 not_pandas_parser = parser != "pandas"
163
164 if not_pandas_parser:
165 msg = "The '@' prefix is only supported by the pandas parser"
166 elif at_top_of_stack:
167 msg = (
168 "The '@' prefix is not allowed in top-level eval calls.\n"
169 "please refer to your variables by name without the '@' prefix."
170 )
171
172 if at_top_of_stack or not_pandas_parser:
173 for toknum, tokval in tokenize_string(expr):
174 if toknum == tokenize.OP and tokval == "@":
175 raise SyntaxError(msg)
176
177
178@set_module("pandas")
179def eval(
180 expr: str | BinOp, # we leave BinOp out of the docstr bc it isn't for users
181 parser: str = "pandas",
182 engine: str | None = None,
183 local_dict=None,
184 global_dict=None,
185 resolvers=(),
186 level: int = 0,
187 target=None,
188 inplace: bool = False,
189) -> Any:
190 """
191 Evaluate a Python expression as a string using various backends.
192
193 .. warning::
194
195 This function can run arbitrary code which can make you vulnerable to code
196 injection if you pass user input to this function.
197
198 Parameters
199 ----------
200 expr : str
201 The expression to evaluate. This string cannot contain any Python
202 `statements
203 <https://docs.python.org/3/reference/simple_stmts.html#simple-statements>`__,
204 only Python `expressions
205 <https://docs.python.org/3/reference/simple_stmts.html#expression-statements>`__.
206
207 By default, with the numexpr engine, the following operations are supported:
208
209 - Arithmetic operations: ``+``, ``-``, ``*``, ``/``, ``**``, ``%``
210 - Boolean operations: ``|`` (or), ``&`` (and), and ``~`` (not)
211 - Comparison operators: ``<``, ``<=``, ``==``, ``!=``, ``>=``, ``>``
212
213 Furthermore, the following mathematical functions are supported:
214
215 - Trigonometric: ``sin``, ``cos``, ``tan``, ``arcsin``, ``arccos``, \
216 ``arctan``, ``arctan2``, ``sinh``, ``cosh``, ``tanh``, ``arcsinh``, \
217 ``arccosh`` and ``arctanh``
218 - Logarithms: ``log`` natural, ``log10`` base 10, ``log1p`` log(1+x)
219 - Absolute Value ``abs``
220 - Square root ``sqrt``
221 - Exponential ``exp`` and Exponential minus one ``expm1``
222
223 See the numexpr engine `documentation
224 <https://numexpr.readthedocs.io/en/latest/user_guide.html#supported-functions>`__
225 for further function support details.
226
227 Using the ``'python'`` engine allows the use of native Python operators
228 such as floor division ``//``, in addition to built-in and user-defined
229 Python functions.
230
231 Additionally, the ``'pandas'`` parser allows the use of :keyword:`and`,
232 :keyword:`or`, and :keyword:`not` with the same semantics as the
233 corresponding bitwise operators.
234 parser : {'pandas', 'python'}, default 'pandas'
235 The parser to use to construct the syntax tree from the expression. The
236 default of ``'pandas'`` parses code slightly different than standard
237 Python. Alternatively, you can parse an expression using the
238 ``'python'`` parser to retain strict Python semantics. See the
239 :ref:`enhancing performance <enhancingperf.eval>` documentation for
240 more details.
241 engine : {'python', 'numexpr'}, optional, default None
242
243 The engine used to evaluate the expression. Supported engines are
244
245 - None : tries to use ``numexpr``, falls back to ``python``
246 - ``'numexpr'`` : This is the default engine when ``numexpr`` is installed.
247 Evaluates pandas objects using numexpr for large speed ups in complex
248 expressions with large frames.
249 - ``'python'`` : Performs operations as if you had ``eval``'d in top
250 level python. This engine is generally not that useful.
251
252 More backends may be available in the future.
253 local_dict : dict or None, optional
254 A dictionary of local variables, taken from locals() by default.
255 global_dict : dict or None, optional
256 A dictionary of global variables, taken from globals() by default.
257 resolvers : list of dict-like or None, optional
258 A list of objects implementing the ``__getitem__`` special method that
259 you can use to inject an additional collection of namespaces to use for
260 variable lookup. For example, this is used in the
261 :meth:`~DataFrame.query` method to inject the
262 ``DataFrame.index`` and ``DataFrame.columns``
263 variables that refer to their respective :class:`~pandas.DataFrame`
264 instance attributes.
265 level : int, optional
266 The number of prior stack frames to traverse and add to the current
267 scope. Most users will **not** need to change this parameter.
268 target : object, optional, default None
269 This is the target object for assignment. It is used when there is
270 variable assignment in the expression. If so, then `target` must
271 support item assignment with string keys, and if a copy is being
272 returned, it must also support `.copy()`.
273 inplace : bool, default False
274 If `target` is provided, and the expression mutates `target`, whether
275 to modify `target` inplace. Otherwise, return a copy of `target` with
276 the mutation.
277
278 Returns
279 -------
280 ndarray, numeric scalar, DataFrame, Series, or None
281 The completion value of evaluating the given code or None if ``inplace=True``.
282
283 Raises
284 ------
285 ValueError
286 There are many instances where such an error can be raised:
287
288 - `target=None`, but the expression is multiline.
289 - The expression is multiline, but not all them have item assignment.
290 An example of such an arrangement is this:
291
292 a = b + 1
293 a + 2
294
295 Here, there are expressions on different lines, making it multiline,
296 but the last line has no variable assigned to the output of `a + 2`.
297 - `inplace=True`, but the expression is missing item assignment.
298 - Item assignment is provided, but the `target` does not support
299 string item assignment.
300 - Item assignment is provided and `inplace=False`, but the `target`
301 does not support the `.copy()` method
302
303 See Also
304 --------
305 DataFrame.query : Evaluates a boolean expression to query the columns
306 of a frame.
307 DataFrame.eval : Evaluate a string describing operations on
308 DataFrame columns.
309
310 Notes
311 -----
312 The ``dtype`` of any objects involved in an arithmetic ``%`` operation are
313 recursively cast to ``float64``.
314
315 See the :ref:`enhancing performance <enhancingperf.eval>` documentation for
316 more details.
317
318 Examples
319 --------
320 >>> df = pd.DataFrame({"animal": ["dog", "pig"], "age": [10, 20]})
321 >>> df
322 animal age
323 0 dog 10
324 1 pig 20
325
326 We can add a new column using ``pd.eval``:
327
328 >>> pd.eval("double_age = df.age * 2", target=df)
329 animal age double_age
330 0 dog 10 20
331 1 pig 20 40
332 """
333 inplace = validate_bool_kwarg(inplace, "inplace")
334
335 exprs: list[str | BinOp]
336 if isinstance(expr, str):
337 _check_expression(expr)
338 exprs = [e.strip() for e in expr.splitlines() if e.strip() != ""]
339 else:
340 # ops.BinOp; for internal compat, not intended to be passed by users
341 exprs = [expr]
342 multi_line = len(exprs) > 1
343
344 if multi_line and target is None:
345 raise ValueError(
346 "multi-line expressions are only valid in the "
347 "context of data, use DataFrame.eval"
348 )
349 engine = _check_engine(engine)
350 _check_parser(parser)
351 _check_resolvers(resolvers)
352
353 ret = None
354 first_expr = True
355 target_modified = False
356
357 for expr in exprs:
358 expr = _convert_expression(expr)
359 _check_for_locals(expr, level, parser)
360
361 # get our (possibly passed-in) scope
362 env = ensure_scope(
363 level + 1,
364 global_dict=global_dict,
365 local_dict=local_dict,
366 resolvers=resolvers,
367 target=target,
368 )
369
370 parsed_expr = Expr(expr, engine=engine, parser=parser, env=env)
371
372 if engine == "numexpr" and (
373 (
374 is_extension_array_dtype(parsed_expr.terms.return_type)
375 and not is_string_dtype(parsed_expr.terms.return_type)
376 )
377 or (
378 getattr(parsed_expr.terms, "operand_types", None) is not None
379 and any(
380 (is_extension_array_dtype(elem) and not is_string_dtype(elem))
381 for elem in parsed_expr.terms.operand_types
382 )
383 )
384 ):
385 warnings.warn(
386 "Engine has switched to 'python' because numexpr does not support "
387 "extension array dtypes. Please set your engine to python manually.",
388 RuntimeWarning,
389 stacklevel=find_stack_level(),
390 )
391 engine = "python"
392
393 # construct the engine and evaluate the parsed expression
394 eng = ENGINES[engine]
395 eng_inst = eng(parsed_expr)
396 ret = eng_inst.evaluate()
397
398 if parsed_expr.assigner is None:
399 if multi_line:
400 raise ValueError(
401 "Multi-line expressions are only valid "
402 "if all expressions contain an assignment"
403 )
404 if inplace:
405 raise ValueError("Cannot operate inplace if there is no assignment")
406
407 # assign if needed
408 assigner = parsed_expr.assigner
409 if env.target is not None and assigner is not None:
410 target_modified = True
411
412 # if returning a copy, copy only on the first assignment
413 if not inplace and first_expr:
414 try:
415 target = env.target
416 if isinstance(target, NDFrame):
417 target = target.copy(deep=False)
418 else:
419 target = target.copy()
420 except AttributeError as err:
421 raise ValueError("Cannot return a copy of the target") from err
422 else:
423 target = env.target
424
425 # TypeError is most commonly raised (e.g. int, list), but you
426 # get IndexError if you try to do this assignment on np.ndarray.
427 # we will ignore numpy warnings here; e.g. if trying
428 # to use a non-numeric indexer
429 try:
430 if inplace and isinstance(target, NDFrame):
431 target.loc[:, assigner] = ret
432 else:
433 target[assigner] = ret # pyright: ignore[reportIndexIssue]
434 except (TypeError, IndexError) as err:
435 raise ValueError("Cannot assign expression output to target") from err
436
437 if not resolvers:
438 resolvers = ({assigner: ret},)
439 else:
440 # existing resolver needs updated to handle
441 # case of mutating existing column in copy
442 for resolver in resolvers:
443 if assigner in resolver:
444 resolver[assigner] = ret
445 break
446 else:
447 resolvers += ({assigner: ret},)
448
449 ret = None
450 first_expr = False
451
452 # We want to exclude `inplace=None` as being False.
453 if inplace is False:
454 return target if target_modified else ret