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