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