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