1"""
2:func:`~pandas.eval` parsers.
3"""
4
5from __future__ import annotations
6
7import ast
8from functools import (
9 partial,
10 reduce,
11)
12from keyword import iskeyword
13import tokenize
14from typing import (
15 TYPE_CHECKING,
16 ClassVar,
17 TypeVar,
18)
19
20import numpy as np
21
22from pandas.errors import UndefinedVariableError
23
24from pandas.core.dtypes.common import is_string_dtype
25
26import pandas.core.common as com
27from pandas.core.computation.ops import (
28 ARITH_OPS_SYMS,
29 BOOL_OPS_SYMS,
30 CMP_OPS_SYMS,
31 LOCAL_TAG,
32 MATHOPS,
33 REDUCTIONS,
34 UNARY_OPS_SYMS,
35 BinOp,
36 Constant,
37 FuncNode,
38 Op,
39 Term,
40 UnaryOp,
41 is_term,
42)
43from pandas.core.computation.parsing import (
44 clean_backtick_quoted_toks,
45 tokenize_string,
46)
47from pandas.core.computation.scope import Scope
48
49from pandas.io.formats import printing
50
51if TYPE_CHECKING:
52 from collections.abc import Callable
53
54
55def _rewrite_assign(tok: tuple[int, str]) -> tuple[int, str]:
56 """
57 Rewrite the assignment operator for PyTables expressions that use ``=``
58 as a substitute for ``==``.
59
60 Parameters
61 ----------
62 tok : tuple of int, str
63 ints correspond to the all caps constants in the tokenize module
64
65 Returns
66 -------
67 tuple of int, str
68 Either the input or token or the replacement values
69 """
70 toknum, tokval = tok
71 return toknum, "==" if tokval == "=" else tokval
72
73
74def _replace_booleans(tok: tuple[int, str]) -> tuple[int, str]:
75 """
76 Replace ``&`` with ``and`` and ``|`` with ``or`` so that bitwise
77 precedence is changed to boolean precedence.
78
79 Parameters
80 ----------
81 tok : tuple of int, str
82 ints correspond to the all caps constants in the tokenize module
83
84 Returns
85 -------
86 tuple of int, str
87 Either the input or token or the replacement values
88 """
89 toknum, tokval = tok
90 if toknum == tokenize.OP:
91 if tokval == "&":
92 return tokenize.NAME, "and"
93 elif tokval == "|":
94 return tokenize.NAME, "or"
95 return toknum, tokval
96 return toknum, tokval
97
98
99def _replace_locals(tok: tuple[int, str]) -> tuple[int, str]:
100 """
101 Replace local variables with a syntactically valid name.
102
103 Parameters
104 ----------
105 tok : tuple of int, str
106 ints correspond to the all caps constants in the tokenize module
107
108 Returns
109 -------
110 tuple of int, str
111 Either the input or token or the replacement values
112
113 Notes
114 -----
115 This is somewhat of a hack in that we rewrite a string such as ``'@a'`` as
116 ``'__pd_eval_local_a'`` by telling the tokenizer that ``__pd_eval_local_``
117 is a ``tokenize.OP`` and to replace the ``'@'`` symbol with it.
118 """
119 toknum, tokval = tok
120 if toknum == tokenize.OP and tokval == "@":
121 return tokenize.OP, LOCAL_TAG
122 return toknum, tokval
123
124
125def _compose2(f, g):
126 """
127 Compose 2 callables.
128 """
129 return lambda *args, **kwargs: f(g(*args, **kwargs))
130
131
132def _compose(*funcs):
133 """
134 Compose 2 or more callables.
135 """
136 assert len(funcs) > 1, "At least 2 callables must be passed to compose"
137 return reduce(_compose2, funcs)
138
139
140def _preparse(
141 source: str,
142 f=_compose(
143 _replace_locals, _replace_booleans, _rewrite_assign, clean_backtick_quoted_toks
144 ),
145) -> str:
146 """
147 Compose a collection of tokenization functions.
148
149 Parameters
150 ----------
151 source : str
152 A Python source code string
153 f : callable
154 This takes a tuple of (toknum, tokval) as its argument and returns a
155 tuple with the same structure but possibly different elements. Defaults
156 to the composition of ``_rewrite_assign``, ``_replace_booleans``, and
157 ``_replace_locals``.
158
159 Returns
160 -------
161 str
162 Valid Python source code
163
164 Notes
165 -----
166 The `f` parameter can be any callable that takes *and* returns input of the
167 form ``(toknum, tokval)``, where ``toknum`` is one of the constants from
168 the ``tokenize`` module and ``tokval`` is a string.
169 """
170 assert callable(f), "f must be callable"
171 return tokenize.untokenize(
172 f(x)
173 for x in tokenize_string(source) # pyright: ignore[reportArgumentType]
174 )
175
176
177def _is_type(t):
178 """
179 Factory for a type checking function of type ``t`` or tuple of types.
180 """
181 return lambda x: isinstance(x.value, t)
182
183
184_is_list = _is_type(list)
185_is_str = _is_type(str)
186
187
188# partition all AST nodes
189_all_nodes = frozenset(
190 node
191 for node in (getattr(ast, name) for name in dir(ast))
192 if isinstance(node, type) and issubclass(node, ast.AST)
193)
194
195
196def _filter_nodes(superclass, all_nodes=_all_nodes):
197 """
198 Filter out AST nodes that are subclasses of ``superclass``.
199 """
200 node_names = (node.__name__ for node in all_nodes if issubclass(node, superclass))
201 return frozenset(node_names)
202
203
204_all_node_names = frozenset(x.__name__ for x in _all_nodes)
205_mod_nodes = _filter_nodes(ast.mod)
206_stmt_nodes = _filter_nodes(ast.stmt)
207_expr_nodes = _filter_nodes(ast.expr)
208_expr_context_nodes = _filter_nodes(ast.expr_context)
209_boolop_nodes = _filter_nodes(ast.boolop)
210_operator_nodes = _filter_nodes(ast.operator)
211_unary_op_nodes = _filter_nodes(ast.unaryop)
212_cmp_op_nodes = _filter_nodes(ast.cmpop)
213_comprehension_nodes = _filter_nodes(ast.comprehension)
214_handler_nodes = _filter_nodes(ast.excepthandler)
215_arguments_nodes = _filter_nodes(ast.arguments)
216_keyword_nodes = _filter_nodes(ast.keyword)
217_alias_nodes = _filter_nodes(ast.alias)
218
219
220# nodes that we don't support directly but are needed for parsing
221_hacked_nodes = frozenset(["Assign", "Module", "Expr"])
222
223
224_unsupported_expr_nodes = frozenset(
225 [
226 "Yield",
227 "GeneratorExp",
228 "IfExp",
229 "DictComp",
230 "SetComp",
231 "Repr",
232 "Lambda",
233 "Set",
234 "AST",
235 "Is",
236 "IsNot",
237 ]
238)
239
240# these nodes are low priority or won't ever be supported (e.g., AST)
241_unsupported_nodes = (
242 _stmt_nodes
243 | _mod_nodes
244 | _handler_nodes
245 | _arguments_nodes
246 | _keyword_nodes
247 | _alias_nodes
248 | _expr_context_nodes
249 | _unsupported_expr_nodes
250) - _hacked_nodes
251
252# we're adding a different assignment in some cases to be equality comparison
253# and we don't want `stmt` and friends in their so get only the class whose
254# names are capitalized
255_base_supported_nodes = (_all_node_names - _unsupported_nodes) | _hacked_nodes
256intersection = _unsupported_nodes & _base_supported_nodes
257_msg = f"cannot both support and not support {intersection}"
258assert not intersection, _msg
259
260
261def _node_not_implemented(node_name: str) -> Callable[..., None]:
262 """
263 Return a function that raises a NotImplementedError with a passed node name.
264 """
265
266 def f(self, *args, **kwargs):
267 raise NotImplementedError(f"'{node_name}' nodes are not implemented")
268
269 return f
270
271
272# should be bound by BaseExprVisitor but that creates a circular dependency:
273# _T is used in disallow, but disallow is used to define BaseExprVisitor
274# https://github.com/microsoft/pyright/issues/2315
275_T = TypeVar("_T")
276
277
278def disallow(nodes: set[str]) -> Callable[[type[_T]], type[_T]]:
279 """
280 Decorator to disallow certain nodes from parsing. Raises a
281 NotImplementedError instead.
282
283 Returns
284 -------
285 callable
286 """
287
288 def disallowed(cls: type[_T]) -> type[_T]:
289 # error: "Type[_T]" has no attribute "unsupported_nodes"
290 cls.unsupported_nodes = () # type: ignore[attr-defined]
291 for node in nodes:
292 new_method = _node_not_implemented(node)
293 name = f"visit_{node}"
294 # error: "Type[_T]" has no attribute "unsupported_nodes"
295 cls.unsupported_nodes += (name,) # type: ignore[attr-defined]
296 setattr(cls, name, new_method)
297 return cls
298
299 return disallowed
300
301
302def _op_maker(op_class, op_symbol):
303 """
304 Return a function to create an op class with its symbol already passed.
305
306 Returns
307 -------
308 callable
309 """
310
311 def f(self, node, *args, **kwargs):
312 """
313 Return a partial function with an Op subclass with an operator already passed.
314
315 Returns
316 -------
317 callable
318 """
319 return partial(op_class, op_symbol, *args, **kwargs)
320
321 return f
322
323
324_op_classes = {"binary": BinOp, "unary": UnaryOp}
325
326
327def add_ops(op_classes):
328 """
329 Decorator to add default implementation of ops.
330 """
331
332 def f(cls):
333 for op_attr_name, op_class in op_classes.items():
334 ops = getattr(cls, f"{op_attr_name}_ops")
335 ops_map = getattr(cls, f"{op_attr_name}_op_nodes_map")
336 for op in ops:
337 op_node = ops_map[op]
338 if op_node is not None:
339 made_op = _op_maker(op_class, op)
340 setattr(cls, f"visit_{op_node}", made_op)
341 return cls
342
343 return f
344
345
346@disallow(_unsupported_nodes)
347@add_ops(_op_classes)
348class BaseExprVisitor(ast.NodeVisitor):
349 """
350 Custom ast walker. Parsers of other engines should subclass this class
351 if necessary.
352
353 Parameters
354 ----------
355 env : Scope
356 engine : str
357 parser : str
358 preparser : callable
359 """
360
361 const_type: ClassVar[type[Term]] = Constant
362 term_type: ClassVar[type[Term]] = Term
363
364 binary_ops = CMP_OPS_SYMS + BOOL_OPS_SYMS + ARITH_OPS_SYMS
365 binary_op_nodes = (
366 "Gt",
367 "Lt",
368 "GtE",
369 "LtE",
370 "Eq",
371 "NotEq",
372 "In",
373 "NotIn",
374 "BitAnd",
375 "BitOr",
376 "And",
377 "Or",
378 "Add",
379 "Sub",
380 "Mult",
381 "Div",
382 "Pow",
383 "FloorDiv",
384 "Mod",
385 )
386 binary_op_nodes_map = dict(zip(binary_ops, binary_op_nodes, strict=True))
387
388 unary_ops = UNARY_OPS_SYMS
389 unary_op_nodes = "UAdd", "USub", "Invert", "Not"
390 unary_op_nodes_map = dict(zip(unary_ops, unary_op_nodes, strict=True))
391
392 rewrite_map = {
393 ast.Eq: ast.In,
394 ast.NotEq: ast.NotIn,
395 ast.In: ast.In,
396 ast.NotIn: ast.NotIn,
397 }
398
399 unsupported_nodes: tuple[str, ...]
400
401 def __init__(self, env, engine, parser, preparser=_preparse) -> None:
402 self.env = env
403 self.engine = engine
404 self.parser = parser
405 self.preparser = preparser
406 self.assigner = None
407
408 def visit(self, node, **kwargs):
409 if isinstance(node, str):
410 clean = self.preparser(node)
411 try:
412 node = ast.fix_missing_locations(ast.parse(clean))
413 except SyntaxError as e:
414 if any(iskeyword(x) for x in clean.split()):
415 e.msg = "Python keyword not valid identifier in numexpr query"
416 raise e
417
418 method = f"visit_{type(node).__name__}"
419 visitor = getattr(self, method)
420 return visitor(node, **kwargs)
421
422 def visit_Module(self, node, **kwargs):
423 if len(node.body) != 1:
424 raise SyntaxError("only a single expression is allowed")
425 expr = node.body[0]
426 return self.visit(expr, **kwargs)
427
428 def visit_Expr(self, node, **kwargs):
429 return self.visit(node.value, **kwargs)
430
431 def _rewrite_membership_op(self, node, left, right):
432 # the kind of the operator (is actually an instance)
433 op_instance = node.op
434 op_type = type(op_instance)
435
436 # must be two terms and the comparison operator must be ==/!=/in/not in
437 if is_term(left) and is_term(right) and op_type in self.rewrite_map:
438 left_list, right_list = map(_is_list, (left, right))
439 left_str, right_str = map(_is_str, (left, right))
440
441 # if there are any strings or lists in the expression
442 if left_list or right_list or left_str or right_str:
443 op_instance = self.rewrite_map[op_type]()
444
445 # pop the string variable out of locals and replace it with a list
446 # of one string, kind of a hack
447 if right_str:
448 name = self.env.add_tmp([right.value])
449 right = self.term_type(name, self.env)
450
451 if left_str:
452 name = self.env.add_tmp([left.value])
453 left = self.term_type(name, self.env)
454
455 op = self.visit(op_instance)
456 return op, op_instance, left, right
457
458 def _maybe_transform_eq_ne(self, node, left=None, right=None):
459 if left is None:
460 left = self.visit(node.left, side="left")
461 if right is None:
462 right = self.visit(node.right, side="right")
463 op, op_class, left, right = self._rewrite_membership_op(node, left, right)
464 return op, op_class, left, right
465
466 def _maybe_downcast_constants(self, left, right):
467 f32 = np.dtype(np.float32)
468 if (
469 left.is_scalar
470 and hasattr(left, "value")
471 and not right.is_scalar
472 and right.return_type == f32
473 ):
474 # right is a float32 array, left is a scalar
475 name = self.env.add_tmp(np.float32(left.value))
476 left = self.term_type(name, self.env)
477 if (
478 right.is_scalar
479 and hasattr(right, "value")
480 and not left.is_scalar
481 and left.return_type == f32
482 ):
483 # left is a float32 array, right is a scalar
484 name = self.env.add_tmp(np.float32(right.value))
485 right = self.term_type(name, self.env)
486
487 return left, right
488
489 def _maybe_eval(self, binop, eval_in_python):
490 # eval `in` and `not in` (for now) in "partial" python space
491 # things that can be evaluated in "eval" space will be turned into
492 # temporary variables. for example,
493 # [1,2] in a + 2 * b
494 # in that case a + 2 * b will be evaluated using numexpr, and the "in"
495 # call will be evaluated using isin (in python space)
496 return binop.evaluate(
497 self.env, self.engine, self.parser, self.term_type, eval_in_python
498 )
499
500 def _maybe_evaluate_binop(
501 self,
502 op,
503 op_class,
504 lhs,
505 rhs,
506 eval_in_python=("in", "not in"),
507 maybe_eval_in_python=("==", "!=", "<", ">", "<=", ">="),
508 ):
509 res = op(lhs, rhs)
510
511 if res.has_invalid_return_type:
512 raise TypeError(
513 f"unsupported operand type(s) for {res.op}: "
514 f"'{lhs.type}' and '{rhs.type}'"
515 )
516
517 if self.engine != "pytables" and (
518 (res.op in CMP_OPS_SYMS and getattr(lhs, "is_datetime", False))
519 or getattr(rhs, "is_datetime", False)
520 ):
521 # all date ops must be done in python bc numexpr doesn't work
522 # well with NaT
523 return self._maybe_eval(res, self.binary_ops)
524
525 if res.op in eval_in_python:
526 # "in"/"not in" ops are always evaluated in python
527 return self._maybe_eval(res, eval_in_python)
528 elif self.engine != "pytables":
529 if (
530 getattr(lhs, "return_type", None) == object
531 or is_string_dtype(getattr(lhs, "return_type", None))
532 or getattr(rhs, "return_type", None) == object
533 or is_string_dtype(getattr(rhs, "return_type", None))
534 ):
535 # evaluate "==" and "!=" in python if either of our operands
536 # has an object or string return type
537 return self._maybe_eval(res, eval_in_python + maybe_eval_in_python)
538 return res
539
540 def visit_BinOp(self, node, **kwargs):
541 op, op_class, left, right = self._maybe_transform_eq_ne(node)
542 left, right = self._maybe_downcast_constants(left, right)
543 return self._maybe_evaluate_binop(op, op_class, left, right)
544
545 def visit_UnaryOp(self, node, **kwargs):
546 op = self.visit(node.op)
547 operand = self.visit(node.operand)
548 return op(operand)
549
550 def visit_Name(self, node, **kwargs) -> Term:
551 return self.term_type(node.id, self.env, **kwargs)
552
553 # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min
554 def visit_NameConstant(self, node, **kwargs) -> Term:
555 return self.const_type(node.value, self.env)
556
557 # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min
558 def visit_Num(self, node, **kwargs) -> Term:
559 return self.const_type(node.value, self.env)
560
561 def visit_Constant(self, node, **kwargs) -> Term:
562 return self.const_type(node.value, self.env)
563
564 # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min
565 def visit_Str(self, node, **kwargs) -> Term:
566 name = self.env.add_tmp(node.s)
567 return self.term_type(name, self.env)
568
569 def visit_List(self, node, **kwargs) -> Term:
570 name = self.env.add_tmp([self.visit(e)(self.env) for e in node.elts])
571 return self.term_type(name, self.env)
572
573 visit_Tuple = visit_List
574
575 def visit_Index(self, node, **kwargs):
576 """df.index[4]"""
577 return self.visit(node.value)
578
579 def visit_Subscript(self, node, **kwargs) -> Term:
580 from pandas import eval as pd_eval
581
582 value = self.visit(node.value)
583 if isinstance(node.slice, ast.Tuple):
584 # visit_Tuple builds a list, which __getitem__ reads as a single
585 # axis-0 selector rather than a multi-axis key (GH#49905)
586 raise NotImplementedError("multi-dimensional subscripts are not supported")
587 slobj = self.visit(node.slice)
588 if isinstance(slobj, slice):
589 # visit_Slice returns a bare slice; re-parsing it would stringify it into
590 # a slice(...) call, which is not a supported function (GH#49905)
591 result = slobj
592 elif is_term(slobj):
593 # already resolved; re-parsing pushes it back through numexpr
594 result = slobj.value
595 else:
596 # an Op still needs the engine, which aligns Series operands
597 result = pd_eval(
598 slobj, local_dict=self.env, engine=self.engine, parser=self.parser
599 )
600 try:
601 # a Term instance
602 v = value.value[result]
603 except AttributeError:
604 # an Op instance
605 lhs = pd_eval(
606 value, local_dict=self.env, engine=self.engine, parser=self.parser
607 )
608 v = lhs[result]
609 name = self.env.add_tmp(v)
610 return self.term_type(name, env=self.env)
611
612 def visit_Slice(self, node, **kwargs) -> slice:
613 """df.index[slice(4,6)]"""
614 lower = node.lower
615 if lower is not None:
616 lower = self.visit(lower)(self.env)
617 upper = node.upper
618 if upper is not None:
619 upper = self.visit(upper)(self.env)
620 step = node.step
621 if step is not None:
622 step = self.visit(step)(self.env)
623
624 return slice(lower, upper, step)
625
626 def visit_Assign(self, node, **kwargs):
627 """
628 support a single assignment node, like
629
630 c = a + b
631
632 set the assigner at the top level, must be a Name node which
633 might or might not exist in the resolvers
634
635 """
636 if len(node.targets) != 1:
637 raise SyntaxError("can only assign a single expression")
638 if not isinstance(node.targets[0], ast.Name):
639 raise SyntaxError("left hand side of an assignment must be a single name")
640 if self.env.target is None:
641 raise ValueError("cannot assign without a target object")
642
643 try:
644 assigner = self.visit(node.targets[0], **kwargs)
645 except UndefinedVariableError:
646 assigner = node.targets[0].id
647
648 self.assigner = getattr(assigner, "name", assigner)
649 if self.assigner is None:
650 raise SyntaxError(
651 "left hand side of an assignment must be a single resolvable name"
652 )
653
654 return self.visit(node.value, **kwargs)
655
656 def visit_Attribute(self, node, **kwargs):
657 attr = node.attr
658 value = node.value
659
660 ctx = node.ctx
661 if isinstance(ctx, ast.Load):
662 # resolve the value
663 visited_value = self.visit(value)
664 if hasattr(visited_value, "value"):
665 resolved = visited_value.value
666 else:
667 resolved = visited_value(self.env)
668 try:
669 v = getattr(resolved, attr)
670 name = self.env.add_tmp(v)
671 return self.term_type(name, self.env)
672 except AttributeError:
673 # something like datetime.datetime where scope is overridden
674 if isinstance(value, ast.Name) and value.id == attr:
675 return resolved
676 raise
677
678 raise ValueError(f"Invalid Attribute context {type(ctx).__name__}")
679
680 def visit_Call(self, node, side=None, **kwargs):
681 if isinstance(node.func, ast.Attribute) and node.func.attr != "__call__":
682 res = self.visit_Attribute(node.func)
683 elif not isinstance(node.func, ast.Name):
684 raise TypeError("Only named functions are supported")
685 else:
686 try:
687 res = self.visit(node.func)
688 except UndefinedVariableError:
689 # Check if this is a supported function name
690 try:
691 res = FuncNode(node.func.id)
692 except ValueError:
693 # Raise original error
694 raise
695
696 if res is None:
697 # error: "expr" has no attribute "id"
698 raise ValueError(
699 f"Invalid function call {node.func.id}" # type: ignore[union-attr]
700 )
701 if hasattr(res, "value"):
702 res = res.value
703
704 if isinstance(res, FuncNode):
705 new_args = [self.visit(arg) for arg in node.args]
706
707 if node.keywords:
708 raise TypeError(
709 f'Function "{res.name}" does not support keyword arguments'
710 )
711
712 return res(*new_args)
713
714 else:
715 new_args = [self.visit(arg)(self.env) for arg in node.args]
716
717 for key in node.keywords:
718 if not isinstance(key, ast.keyword):
719 # error: Item "Attribute" of "Attribute | Name" has no
720 # attribute "id"
721 raise ValueError(
722 f"keyword error in function call '{node.func.id}'" # type: ignore[union-attr]
723 )
724
725 if key.arg:
726 kwargs[key.arg] = self.visit(key.value)(self.env)
727
728 name = self.env.add_tmp(res(*new_args, **kwargs))
729 return self.term_type(name=name, env=self.env)
730
731 def translate_In(self, op):
732 return op
733
734 def visit_Compare(self, node, **kwargs):
735 ops = node.ops
736 comps = node.comparators
737
738 # base case: we have something like a CMP b
739 if len(comps) == 1:
740 op = self.translate_In(ops[0])
741 # GH#65357: Unroll `in` / `not in` into row-wise comparisons (== / !=)
742 # if the RHS tuple contains variables. This prevents passing Series
743 # objects to isin(), which strictly expects scalar iterables.
744 if isinstance(ops[0], (ast.In, ast.NotIn)) and isinstance(
745 comps[0], (ast.Tuple, ast.List)
746 ):
747 elts = comps[0].elts
748 if any(isinstance(e, ast.Name) for e in elts):
749 cmp_op = ast.Eq() if isinstance(ops[0], ast.In) else ast.NotEq()
750 bool_op = ast.Or() if isinstance(ops[0], ast.In) else ast.And()
751 new_values: list[ast.expr] = [
752 ast.Compare(left=node.left, ops=[cmp_op], comparators=[elt])
753 for elt in elts
754 ]
755 return self.visit(ast.BoolOp(op=bool_op, values=new_values))
756
757 binop = ast.BinOp(op=op, left=node.left, right=comps[0])
758 return self.visit(binop)
759
760 # recursive case: we have a chained comparison, a CMP b CMP c, etc.
761 left = node.left
762 values = []
763 for op, comp in zip(ops, comps, strict=True):
764 new_node = self.visit(
765 ast.Compare(comparators=[comp], left=left, ops=[self.translate_In(op)])
766 )
767 left = comp
768 values.append(new_node)
769 return self.visit(ast.BoolOp(op=ast.And(), values=values))
770
771 def _try_visit_binop(self, bop):
772 if isinstance(bop, (Op, Term)):
773 return bop
774 return self.visit(bop)
775
776 def visit_BoolOp(self, node, **kwargs):
777 def visitor(x, y):
778 lhs = self._try_visit_binop(x)
779 rhs = self._try_visit_binop(y)
780
781 op, op_class, lhs, rhs = self._maybe_transform_eq_ne(node, lhs, rhs)
782 return self._maybe_evaluate_binop(op, node.op, lhs, rhs)
783
784 operands = node.values
785 return reduce(visitor, operands)
786
787
788_python_not_supported = frozenset(["Dict", "BoolOp", "In", "NotIn"])
789_numexpr_supported_calls = frozenset(REDUCTIONS + MATHOPS)
790
791
792@disallow(
793 (_unsupported_nodes | _python_not_supported)
794 - (_boolop_nodes | frozenset(["BoolOp", "Attribute", "In", "NotIn", "Tuple"]))
795)
796class PandasExprVisitor(BaseExprVisitor):
797 def __init__(
798 self,
799 env,
800 engine,
801 parser,
802 preparser=partial(
803 _preparse,
804 f=_compose(_replace_locals, _replace_booleans, clean_backtick_quoted_toks),
805 ),
806 ) -> None:
807 super().__init__(env, engine, parser, preparser)
808
809
810@disallow(_unsupported_nodes | _python_not_supported | frozenset(["Not"]))
811class PythonExprVisitor(BaseExprVisitor):
812 def __init__(
813 self, env, engine, parser, preparser=lambda source, f=None: source
814 ) -> None:
815 super().__init__(env, engine, parser, preparser=preparser)
816
817
818class Expr:
819 """
820 Object encapsulating an expression.
821
822 Parameters
823 ----------
824 expr : str
825 engine : str, optional, default 'numexpr'
826 parser : str, optional, default 'pandas'
827 env : Scope, optional, default None
828 level : int, optional, default 2
829 """
830
831 env: Scope
832 engine: str
833 parser: str
834
835 def __init__(
836 self,
837 expr,
838 engine: str = "numexpr",
839 parser: str = "pandas",
840 env: Scope | None = None,
841 level: int = 0,
842 ) -> None:
843 self.expr = expr
844 self.env = env or Scope(level=level + 1)
845 self.engine = engine
846 self.parser = parser
847 self._visitor = PARSERS[parser](self.env, self.engine, self.parser)
848 self.terms = self.parse()
849
850 @property
851 def assigner(self):
852 return getattr(self._visitor, "assigner", None)
853
854 def __call__(self):
855 return self.terms(self.env)
856
857 def __repr__(self) -> str:
858 return printing.pprint_thing(self.terms)
859
860 def __len__(self) -> int:
861 return len(self.expr)
862
863 def parse(self):
864 """
865 Parse an expression.
866 """
867 return self._visitor.visit(self.expr)
868
869 @property
870 def names(self):
871 """
872 Get the names in an expression.
873 """
874 if is_term(self.terms):
875 return frozenset([self.terms.name])
876 return frozenset(term.name for term in com.flatten(self.terms))
877
878
879PARSERS = {"python": PythonExprVisitor, "pandas": PandasExprVisitor}