Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/pandas/core/computation/expr.py: 40%

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

356 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 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}