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 slobj = self.visit(node.slice)
584 result = pd_eval(
585 slobj, local_dict=self.env, engine=self.engine, parser=self.parser
586 )
587 try:
588 # a Term instance
589 v = value.value[result]
590 except AttributeError:
591 # an Op instance
592 lhs = pd_eval(
593 value, local_dict=self.env, engine=self.engine, parser=self.parser
594 )
595 v = lhs[result]
596 name = self.env.add_tmp(v)
597 return self.term_type(name, env=self.env)
598
599 def visit_Slice(self, node, **kwargs) -> slice:
600 """df.index[slice(4,6)]"""
601 lower = node.lower
602 if lower is not None:
603 lower = self.visit(lower).value
604 upper = node.upper
605 if upper is not None:
606 upper = self.visit(upper).value
607 step = node.step
608 if step is not None:
609 step = self.visit(step).value
610
611 return slice(lower, upper, step)
612
613 def visit_Assign(self, node, **kwargs):
614 """
615 support a single assignment node, like
616
617 c = a + b
618
619 set the assigner at the top level, must be a Name node which
620 might or might not exist in the resolvers
621
622 """
623 if len(node.targets) != 1:
624 raise SyntaxError("can only assign a single expression")
625 if not isinstance(node.targets[0], ast.Name):
626 raise SyntaxError("left hand side of an assignment must be a single name")
627 if self.env.target is None:
628 raise ValueError("cannot assign without a target object")
629
630 try:
631 assigner = self.visit(node.targets[0], **kwargs)
632 except UndefinedVariableError:
633 assigner = node.targets[0].id
634
635 self.assigner = getattr(assigner, "name", assigner)
636 if self.assigner is None:
637 raise SyntaxError(
638 "left hand side of an assignment must be a single resolvable name"
639 )
640
641 return self.visit(node.value, **kwargs)
642
643 def visit_Attribute(self, node, **kwargs):
644 attr = node.attr
645 value = node.value
646
647 ctx = node.ctx
648 if isinstance(ctx, ast.Load):
649 # resolve the value
650 visited_value = self.visit(value)
651 if hasattr(visited_value, "value"):
652 resolved = visited_value.value
653 else:
654 resolved = visited_value(self.env)
655 try:
656 v = getattr(resolved, attr)
657 name = self.env.add_tmp(v)
658 return self.term_type(name, self.env)
659 except AttributeError:
660 # something like datetime.datetime where scope is overridden
661 if isinstance(value, ast.Name) and value.id == attr:
662 return resolved
663 raise
664
665 raise ValueError(f"Invalid Attribute context {type(ctx).__name__}")
666
667 def visit_Call(self, node, side=None, **kwargs):
668 if isinstance(node.func, ast.Attribute) and node.func.attr != "__call__":
669 res = self.visit_Attribute(node.func)
670 elif not isinstance(node.func, ast.Name):
671 raise TypeError("Only named functions are supported")
672 else:
673 try:
674 res = self.visit(node.func)
675 except UndefinedVariableError:
676 # Check if this is a supported function name
677 try:
678 res = FuncNode(node.func.id)
679 except ValueError:
680 # Raise original error
681 raise
682
683 if res is None:
684 # error: "expr" has no attribute "id"
685 raise ValueError(
686 f"Invalid function call {node.func.id}" # type: ignore[union-attr]
687 )
688 if hasattr(res, "value"):
689 res = res.value
690
691 if isinstance(res, FuncNode):
692 new_args = [self.visit(arg) for arg in node.args]
693
694 if node.keywords:
695 raise TypeError(
696 f'Function "{res.name}" does not support keyword arguments'
697 )
698
699 return res(*new_args)
700
701 else:
702 new_args = [self.visit(arg)(self.env) for arg in node.args]
703
704 for key in node.keywords:
705 if not isinstance(key, ast.keyword):
706 # error: Item "Attribute" of "Attribute | Name" has no
707 # attribute "id"
708 raise ValueError(
709 f"keyword error in function call '{node.func.id}'" # type: ignore[union-attr]
710 )
711
712 if key.arg:
713 kwargs[key.arg] = self.visit(key.value)(self.env)
714
715 name = self.env.add_tmp(res(*new_args, **kwargs))
716 return self.term_type(name=name, env=self.env)
717
718 def translate_In(self, op):
719 return op
720
721 def visit_Compare(self, node, **kwargs):
722 ops = node.ops
723 comps = node.comparators
724
725 # base case: we have something like a CMP b
726 if len(comps) == 1:
727 op = self.translate_In(ops[0])
728 binop = ast.BinOp(op=op, left=node.left, right=comps[0])
729 return self.visit(binop)
730
731 # recursive case: we have a chained comparison, a CMP b CMP c, etc.
732 left = node.left
733 values = []
734 for op, comp in zip(ops, comps, strict=True):
735 new_node = self.visit(
736 ast.Compare(comparators=[comp], left=left, ops=[self.translate_In(op)])
737 )
738 left = comp
739 values.append(new_node)
740 return self.visit(ast.BoolOp(op=ast.And(), values=values))
741
742 def _try_visit_binop(self, bop):
743 if isinstance(bop, (Op, Term)):
744 return bop
745 return self.visit(bop)
746
747 def visit_BoolOp(self, node, **kwargs):
748 def visitor(x, y):
749 lhs = self._try_visit_binop(x)
750 rhs = self._try_visit_binop(y)
751
752 op, op_class, lhs, rhs = self._maybe_transform_eq_ne(node, lhs, rhs)
753 return self._maybe_evaluate_binop(op, node.op, lhs, rhs)
754
755 operands = node.values
756 return reduce(visitor, operands)
757
758
759_python_not_supported = frozenset(["Dict", "BoolOp", "In", "NotIn"])
760_numexpr_supported_calls = frozenset(REDUCTIONS + MATHOPS)
761
762
763@disallow(
764 (_unsupported_nodes | _python_not_supported)
765 - (_boolop_nodes | frozenset(["BoolOp", "Attribute", "In", "NotIn", "Tuple"]))
766)
767class PandasExprVisitor(BaseExprVisitor):
768 def __init__(
769 self,
770 env,
771 engine,
772 parser,
773 preparser=partial(
774 _preparse,
775 f=_compose(_replace_locals, _replace_booleans, clean_backtick_quoted_toks),
776 ),
777 ) -> None:
778 super().__init__(env, engine, parser, preparser)
779
780
781@disallow(_unsupported_nodes | _python_not_supported | frozenset(["Not"]))
782class PythonExprVisitor(BaseExprVisitor):
783 def __init__(
784 self, env, engine, parser, preparser=lambda source, f=None: source
785 ) -> None:
786 super().__init__(env, engine, parser, preparser=preparser)
787
788
789class Expr:
790 """
791 Object encapsulating an expression.
792
793 Parameters
794 ----------
795 expr : str
796 engine : str, optional, default 'numexpr'
797 parser : str, optional, default 'pandas'
798 env : Scope, optional, default None
799 level : int, optional, default 2
800 """
801
802 env: Scope
803 engine: str
804 parser: str
805
806 def __init__(
807 self,
808 expr,
809 engine: str = "numexpr",
810 parser: str = "pandas",
811 env: Scope | None = None,
812 level: int = 0,
813 ) -> None:
814 self.expr = expr
815 self.env = env or Scope(level=level + 1)
816 self.engine = engine
817 self.parser = parser
818 self._visitor = PARSERS[parser](self.env, self.engine, self.parser)
819 self.terms = self.parse()
820
821 @property
822 def assigner(self):
823 return getattr(self._visitor, "assigner", None)
824
825 def __call__(self):
826 return self.terms(self.env)
827
828 def __repr__(self) -> str:
829 return printing.pprint_thing(self.terms)
830
831 def __len__(self) -> int:
832 return len(self.expr)
833
834 def parse(self):
835 """
836 Parse an expression.
837 """
838 return self._visitor.visit(self.expr)
839
840 @property
841 def names(self):
842 """
843 Get the names in an expression.
844 """
845 if is_term(self.terms):
846 return frozenset([self.terms.name])
847 return frozenset(term.name for term in com.flatten(self.terms))
848
849
850PARSERS = {"python": PythonExprVisitor, "pandas": PandasExprVisitor}