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