Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pandas/core/computation/expr.py: 41%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

361 statements  

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}