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