Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/core/computation/expr.py: 36%
361 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2:func:`~pandas.eval` parsers.
3"""
4from __future__ import annotations
6import ast
7from functools import (
8 partial,
9 reduce,
10)
11from keyword import iskeyword
12import tokenize
13from typing import (
14 Callable,
15 TypeVar,
16)
18import numpy as np
20from pandas.compat import PY39
21from pandas.errors import UndefinedVariableError
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
47import pandas.io.formats.printing as printing
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 ``==``.
55 Parameters
56 ----------
57 tok : tuple of int, str
58 ints correspond to the all caps constants in the tokenize module
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
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.
74 Parameters
75 ----------
76 tok : tuple of int, str
77 ints correspond to the all caps constants in the tokenize module
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
94def _replace_locals(tok: tuple[int, str]) -> tuple[int, str]:
95 """
96 Replace local variables with a syntactically valid name.
98 Parameters
99 ----------
100 tok : tuple of int, str
101 ints correspond to the all caps constants in the tokenize module
103 Returns
104 -------
105 tuple of int, str
106 Either the input or token or the replacement values
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
120def _compose2(f, g):
121 """
122 Compose 2 callables.
123 """
124 return lambda *args, **kwargs: f(g(*args, **kwargs)) 124 ↛ exitline 124 didn't run the lambda on line 124
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)
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.
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``.
154 Returns
155 -------
156 str
157 Valid Python source code
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))
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) 173 ↛ exitline 173 didn't run the lambda on line 173
176_is_list = _is_type(list)
177_is_str = _is_type(str)
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)
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)
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)
211if not PY39: 211 ↛ 212line 211 didn't jump to line 212, because the condition on line 211 was never true
212 _slice_nodes = _filter_nodes(ast.slice)
215# nodes that we don't support directly but are needed for parsing
216_hacked_nodes = frozenset(["Assign", "Module", "Expr"])
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)
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
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
256def _node_not_implemented(node_name: str) -> Callable[..., None]:
257 """
258 Return a function that raises a NotImplementedError with a passed node name.
259 """
261 def f(self, *args, **kwargs):
262 raise NotImplementedError(f"'{node_name}' nodes are not implemented")
264 return f
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")
273def disallow(nodes: set[str]) -> Callable[[type[_T]], type[_T]]:
274 """
275 Decorator to disallow certain nodes from parsing. Raises a
276 NotImplementedError instead.
278 Returns
279 -------
280 callable
281 """
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
294 return disallowed
297def _op_maker(op_class, op_symbol):
298 """
299 Return a function to create an op class with its symbol already passed.
301 Returns
302 -------
303 callable
304 """
306 def f(self, node, *args, **kwargs):
307 """
308 Return a partial function with an Op subclass with an operator already passed.
310 Returns
311 -------
312 callable
313 """
314 return partial(op_class, op_symbol, *args, **kwargs)
316 return f
319_op_classes = {"binary": BinOp, "unary": UnaryOp}
322def add_ops(op_classes):
323 """
324 Decorator to add default implementation of ops.
325 """
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
338 return f
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.
348 Parameters
349 ----------
350 env : Scope
351 engine : str
352 parser : str
353 preparser : callable
354 """
356 const_type: type[Term] = Constant
357 term_type = Term
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))
383 unary_ops = UNARY_OPS_SYMS
384 unary_op_nodes = "UAdd", "USub", "Invert", "Not"
385 unary_op_nodes_map = {k: v for k, v in zip(unary_ops, unary_op_nodes)}
387 rewrite_map = {
388 ast.Eq: ast.In,
389 ast.NotEq: ast.NotIn,
390 ast.In: ast.In,
391 ast.NotIn: ast.NotIn,
392 }
394 unsupported_nodes: tuple[str, ...]
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
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
413 method = "visit_" + type(node).__name__
414 visitor = getattr(self, method)
415 return visitor(node, **kwargs)
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)
423 def visit_Expr(self, node, **kwargs):
424 return self.visit(node.value, **kwargs)
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)
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:
434 left_list, right_list = map(_is_list, (left, right))
435 left_str, right_str = map(_is_str, (left, right))
437 # if there are any strings or lists in the expression
438 if left_list or right_list or left_str or right_str:
439 op_instance = self.rewrite_map[op_type]()
441 # pop the string variable out of locals and replace it with a list
442 # of one string, kind of a hack
443 if right_str:
444 name = self.env.add_tmp([right.value])
445 right = self.term_type(name, self.env)
447 if left_str:
448 name = self.env.add_tmp([left.value])
449 left = self.term_type(name, self.env)
451 op = self.visit(op_instance)
452 return op, op_instance, left, right
454 def _maybe_transform_eq_ne(self, node, left=None, right=None):
455 if left is None:
456 left = self.visit(node.left, side="left")
457 if right is None:
458 right = self.visit(node.right, side="right")
459 op, op_class, left, right = self._rewrite_membership_op(node, left, right)
460 return op, op_class, left, right
462 def _maybe_downcast_constants(self, left, right):
463 f32 = np.dtype(np.float32)
464 if (
465 left.is_scalar
466 and hasattr(left, "value")
467 and not right.is_scalar
468 and right.return_type == f32
469 ):
470 # right is a float32 array, left is a scalar
471 name = self.env.add_tmp(np.float32(left.value))
472 left = self.term_type(name, self.env)
473 if (
474 right.is_scalar
475 and hasattr(right, "value")
476 and not left.is_scalar
477 and left.return_type == f32
478 ):
479 # left is a float32 array, right is a scalar
480 name = self.env.add_tmp(np.float32(right.value))
481 right = self.term_type(name, self.env)
483 return left, right
485 def _maybe_eval(self, binop, eval_in_python):
486 # eval `in` and `not in` (for now) in "partial" python space
487 # things that can be evaluated in "eval" space will be turned into
488 # temporary variables. for example,
489 # [1,2] in a + 2 * b
490 # in that case a + 2 * b will be evaluated using numexpr, and the "in"
491 # call will be evaluated using isin (in python space)
492 return binop.evaluate(
493 self.env, self.engine, self.parser, self.term_type, eval_in_python
494 )
496 def _maybe_evaluate_binop(
497 self,
498 op,
499 op_class,
500 lhs,
501 rhs,
502 eval_in_python=("in", "not in"),
503 maybe_eval_in_python=("==", "!=", "<", ">", "<=", ">="),
504 ):
505 res = op(lhs, rhs)
507 if res.has_invalid_return_type:
508 raise TypeError(
509 f"unsupported operand type(s) for {res.op}: "
510 f"'{lhs.type}' and '{rhs.type}'"
511 )
513 if self.engine != "pytables" and (
514 res.op in CMP_OPS_SYMS
515 and getattr(lhs, "is_datetime", False)
516 or getattr(rhs, "is_datetime", False)
517 ):
518 # all date ops must be done in python bc numexpr doesn't work
519 # well with NaT
520 return self._maybe_eval(res, self.binary_ops)
522 if res.op in eval_in_python:
523 # "in"/"not in" ops are always evaluated in python
524 return self._maybe_eval(res, eval_in_python)
525 elif self.engine != "pytables":
526 if (
527 getattr(lhs, "return_type", None) == object
528 or getattr(rhs, "return_type", None) == object
529 ):
530 # evaluate "==" and "!=" in python if either of our operands
531 # has an object return type
532 return self._maybe_eval(res, eval_in_python + maybe_eval_in_python)
533 return res
535 def visit_BinOp(self, node, **kwargs):
536 op, op_class, left, right = self._maybe_transform_eq_ne(node)
537 left, right = self._maybe_downcast_constants(left, right)
538 return self._maybe_evaluate_binop(op, op_class, left, right)
540 def visit_Div(self, node, **kwargs):
541 return lambda lhs, rhs: Div(lhs, rhs)
543 def visit_UnaryOp(self, node, **kwargs):
544 op = self.visit(node.op)
545 operand = self.visit(node.operand)
546 return op(operand)
548 def visit_Name(self, node, **kwargs):
549 return self.term_type(node.id, self.env, **kwargs)
551 def visit_NameConstant(self, node, **kwargs) -> Term:
552 return self.const_type(node.value, self.env)
554 def visit_Num(self, node, **kwargs) -> Term:
555 return self.const_type(node.n, self.env)
557 def visit_Constant(self, node, **kwargs) -> Term:
558 return self.const_type(node.n, self.env)
560 def visit_Str(self, node, **kwargs):
561 name = self.env.add_tmp(node.s)
562 return self.term_type(name, self.env)
564 def visit_List(self, node, **kwargs):
565 name = self.env.add_tmp([self.visit(e)(self.env) for e in node.elts])
566 return self.term_type(name, self.env)
568 visit_Tuple = visit_List
570 def visit_Index(self, node, **kwargs):
571 """df.index[4]"""
572 return self.visit(node.value)
574 def visit_Subscript(self, node, **kwargs):
575 from pandas import eval as pd_eval
577 value = self.visit(node.value)
578 slobj = self.visit(node.slice)
579 result = pd_eval(
580 slobj, local_dict=self.env, engine=self.engine, parser=self.parser
581 )
582 try:
583 # a Term instance
584 v = value.value[result]
585 except AttributeError:
586 # an Op instance
587 lhs = pd_eval(
588 value, local_dict=self.env, engine=self.engine, parser=self.parser
589 )
590 v = lhs[result]
591 name = self.env.add_tmp(v)
592 return self.term_type(name, env=self.env)
594 def visit_Slice(self, node, **kwargs):
595 """df.index[slice(4,6)]"""
596 lower = node.lower
597 if lower is not None:
598 lower = self.visit(lower).value
599 upper = node.upper
600 if upper is not None:
601 upper = self.visit(upper).value
602 step = node.step
603 if step is not None:
604 step = self.visit(step).value
606 return slice(lower, upper, step)
608 def visit_Assign(self, node, **kwargs):
609 """
610 support a single assignment node, like
612 c = a + b
614 set the assigner at the top level, must be a Name node which
615 might or might not exist in the resolvers
617 """
618 if len(node.targets) != 1:
619 raise SyntaxError("can only assign a single expression")
620 if not isinstance(node.targets[0], ast.Name):
621 raise SyntaxError("left hand side of an assignment must be a single name")
622 if self.env.target is None:
623 raise ValueError("cannot assign without a target object")
625 try:
626 assigner = self.visit(node.targets[0], **kwargs)
627 except UndefinedVariableError:
628 assigner = node.targets[0].id
630 self.assigner = getattr(assigner, "name", assigner)
631 if self.assigner is None:
632 raise SyntaxError(
633 "left hand side of an assignment must be a single resolvable name"
634 )
636 return self.visit(node.value, **kwargs)
638 def visit_Attribute(self, node, **kwargs):
639 attr = node.attr
640 value = node.value
642 ctx = node.ctx
643 if isinstance(ctx, ast.Load):
644 # resolve the value
645 resolved = self.visit(value).value
646 try:
647 v = getattr(resolved, attr)
648 name = self.env.add_tmp(v)
649 return self.term_type(name, self.env)
650 except AttributeError:
651 # something like datetime.datetime where scope is overridden
652 if isinstance(value, ast.Name) and value.id == attr:
653 return resolved
654 raise
656 raise ValueError(f"Invalid Attribute context {type(ctx).__name__}")
658 def visit_Call(self, node, side=None, **kwargs):
660 if isinstance(node.func, ast.Attribute) and node.func.attr != "__call__":
661 res = self.visit_Attribute(node.func)
662 elif not isinstance(node.func, ast.Name):
663 raise TypeError("Only named functions are supported")
664 else:
665 try:
666 res = self.visit(node.func)
667 except UndefinedVariableError:
668 # Check if this is a supported function name
669 try:
670 res = FuncNode(node.func.id)
671 except ValueError:
672 # Raise original error
673 raise
675 if res is None:
676 # error: "expr" has no attribute "id"
677 raise ValueError(
678 f"Invalid function call {node.func.id}" # type: ignore[attr-defined]
679 )
680 if hasattr(res, "value"):
681 res = res.value
683 if isinstance(res, FuncNode):
685 new_args = [self.visit(arg) for arg in node.args]
687 if node.keywords:
688 raise TypeError(
689 f'Function "{res.name}" does not support keyword arguments'
690 )
692 return res(*new_args)
694 else:
696 new_args = [self.visit(arg).value for arg in node.args]
698 for key in node.keywords:
699 if not isinstance(key, ast.keyword):
700 # error: "expr" has no attribute "id"
701 raise ValueError(
702 "keyword error in function call " # type: ignore[attr-defined]
703 f"'{node.func.id}'"
704 )
706 if key.arg:
707 kwargs[key.arg] = self.visit(key.value).value
709 name = self.env.add_tmp(res(*new_args, **kwargs))
710 return self.term_type(name=name, env=self.env)
712 def translate_In(self, op):
713 return op
715 def visit_Compare(self, node, **kwargs):
716 ops = node.ops
717 comps = node.comparators
719 # base case: we have something like a CMP b
720 if len(comps) == 1:
721 op = self.translate_In(ops[0])
722 binop = ast.BinOp(op=op, left=node.left, right=comps[0])
723 return self.visit(binop)
725 # recursive case: we have a chained comparison, a CMP b CMP c, etc.
726 left = node.left
727 values = []
728 for op, comp in zip(ops, comps):
729 new_node = self.visit(
730 ast.Compare(comparators=[comp], left=left, ops=[self.translate_In(op)])
731 )
732 left = comp
733 values.append(new_node)
734 return self.visit(ast.BoolOp(op=ast.And(), values=values))
736 def _try_visit_binop(self, bop):
737 if isinstance(bop, (Op, Term)):
738 return bop
739 return self.visit(bop)
741 def visit_BoolOp(self, node, **kwargs):
742 def visitor(x, y):
743 lhs = self._try_visit_binop(x)
744 rhs = self._try_visit_binop(y)
746 op, op_class, lhs, rhs = self._maybe_transform_eq_ne(node, lhs, rhs)
747 return self._maybe_evaluate_binop(op, node.op, lhs, rhs)
749 operands = node.values
750 return reduce(visitor, operands)
753_python_not_supported = frozenset(["Dict", "BoolOp", "In", "NotIn"])
754_numexpr_supported_calls = frozenset(REDUCTIONS + MATHOPS)
757@disallow(
758 (_unsupported_nodes | _python_not_supported)
759 - (_boolop_nodes | frozenset(["BoolOp", "Attribute", "In", "NotIn", "Tuple"]))
760)
761class PandasExprVisitor(BaseExprVisitor):
762 def __init__(
763 self,
764 env,
765 engine,
766 parser,
767 preparser=partial(
768 _preparse,
769 f=_compose(_replace_locals, _replace_booleans, clean_backtick_quoted_toks),
770 ),
771 ) -> None:
772 super().__init__(env, engine, parser, preparser)
775@disallow(_unsupported_nodes | _python_not_supported | frozenset(["Not"]))
776class PythonExprVisitor(BaseExprVisitor):
777 def __init__( 777 ↛ exitline 777 didn't jump to the function exit
778 self, env, engine, parser, preparser=lambda source, f=None: source
779 ) -> None:
780 super().__init__(env, engine, parser, preparser=preparser)
783class Expr:
784 """
785 Object encapsulating an expression.
787 Parameters
788 ----------
789 expr : str
790 engine : str, optional, default 'numexpr'
791 parser : str, optional, default 'pandas'
792 env : Scope, optional, default None
793 level : int, optional, default 2
794 """
796 env: Scope
797 engine: str
798 parser: str
800 def __init__(
801 self,
802 expr,
803 engine: str = "numexpr",
804 parser: str = "pandas",
805 env: Scope | None = None,
806 level: int = 0,
807 ) -> None:
808 self.expr = expr
809 self.env = env or Scope(level=level + 1)
810 self.engine = engine
811 self.parser = parser
812 self._visitor = PARSERS[parser](self.env, self.engine, self.parser)
813 self.terms = self.parse()
815 @property
816 def assigner(self):
817 return getattr(self._visitor, "assigner", None)
819 def __call__(self):
820 return self.terms(self.env)
822 def __repr__(self) -> str:
823 return printing.pprint_thing(self.terms)
825 def __len__(self) -> int:
826 return len(self.expr)
828 def parse(self):
829 """
830 Parse an expression.
831 """
832 return self._visitor.visit(self.expr)
834 @property
835 def names(self):
836 """
837 Get the names in an expression.
838 """
839 if is_term(self.terms):
840 return frozenset([self.terms.name])
841 return frozenset(term.name for term in com.flatten(self.terms))
844PARSERS = {"python": PythonExprVisitor, "pandas": PandasExprVisitor}