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