1"""
2Engine classes for :func:`~pandas.eval`
3"""
4
5from __future__ import annotations
6
7import abc
8from typing import TYPE_CHECKING
9
10from pandas.errors import NumExprClobberingError
11
12from pandas.core.computation.align import (
13 align_terms,
14 reconstruct_object,
15)
16from pandas.core.computation.expressions import normalize_numexpr_result
17from pandas.core.computation.ops import (
18 MATHOPS,
19 REDUCTIONS,
20)
21
22from pandas.io.formats import printing
23
24if TYPE_CHECKING:
25 from pandas.core.computation.expr import Expr
26
27_ne_builtins = frozenset(MATHOPS + REDUCTIONS)
28
29
30def _check_ne_builtin_clash(expr: Expr) -> None:
31 """
32 Attempt to prevent foot-shooting in a helpful way.
33
34 Parameters
35 ----------
36 expr : Expr
37 Terms can contain
38 """
39 names = expr.names
40 overlap = names & _ne_builtins
41
42 if overlap:
43 s = ", ".join([repr(x) for x in overlap])
44 raise NumExprClobberingError(
45 f'Variables in expression "{expr}" overlap with builtins: ({s})'
46 )
47
48
49class AbstractEngine(metaclass=abc.ABCMeta):
50 """Object serving as a base class for all engines."""
51
52 has_neg_frac = False
53
54 def __init__(self, expr) -> None:
55 self.expr = expr
56 self.aligned_axes = None
57 self.result_type = None
58 self.result_name = None
59
60 def convert(self) -> str:
61 """
62 Convert an expression for evaluation.
63
64 Defaults to return the expression as a string.
65 """
66 return printing.pprint_thing(self.expr)
67
68 def evaluate(self) -> object:
69 """
70 Run the engine on the expression.
71
72 This method performs alignment which is necessary no matter what engine
73 is being used, thus its implementation is in the base class.
74
75 Returns
76 -------
77 object
78 The result of the passed expression.
79 """
80 if not self._is_aligned:
81 self.result_type, self.aligned_axes, self.result_name = align_terms(
82 self.expr.terms
83 )
84
85 # make sure no names in resolvers and locals/globals clash
86 res = self._evaluate()
87 return reconstruct_object(
88 self.result_type,
89 res,
90 self.aligned_axes,
91 self.expr.terms.return_type,
92 self.result_name,
93 )
94
95 @property
96 def _is_aligned(self) -> bool:
97 return self.aligned_axes is not None and self.result_type is not None
98
99 @abc.abstractmethod
100 def _evaluate(self):
101 """
102 Return an evaluated expression.
103
104 Notes
105 -----
106 Must be implemented by subclasses.
107 """
108
109
110class NumExprEngine(AbstractEngine):
111 """NumExpr engine class"""
112
113 has_neg_frac = True
114
115 def _evaluate(self):
116 import numexpr as ne
117
118 # convert the expression to a valid numexpr expression
119 s = self.convert()
120
121 env = self.expr.env
122 scope = env.full_scope
123 _check_ne_builtin_clash(self.expr)
124 return normalize_numexpr_result(ne.evaluate(s, local_dict=scope))
125
126
127class PythonEngine(AbstractEngine):
128 """
129 Evaluate an expression in Python space.
130
131 Mostly for testing purposes.
132 """
133
134 has_neg_frac = False
135
136 def evaluate(self):
137 return self.expr()
138
139 def _evaluate(self) -> None:
140 pass
141
142
143ENGINES: dict[str, type[AbstractEngine]] = {
144 "numexpr": NumExprEngine,
145 "python": PythonEngine,
146}