Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-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

363 statements  

1""" 

2:func:`~pandas.eval` parsers. 

3""" 

4 

5from __future__ import annotations 

6 

7import ast 

8from functools import ( 

9 partial, 

10 reduce, 

11) 

12from keyword import iskeyword 

13import tokenize 

14from typing import ( 

15 TYPE_CHECKING, 

16 ClassVar, 

17 TypeVar, 

18) 

19 

20import numpy as np 

21 

22from pandas.errors import UndefinedVariableError 

23 

24from pandas.core.dtypes.common import is_string_dtype 

25 

26import pandas.core.common as com 

27from pandas.core.computation.ops import ( 

28 ARITH_OPS_SYMS, 

29 BOOL_OPS_SYMS, 

30 CMP_OPS_SYMS, 

31 LOCAL_TAG, 

32 MATHOPS, 

33 REDUCTIONS, 

34 UNARY_OPS_SYMS, 

35 BinOp, 

36 Constant, 

37 FuncNode, 

38 Op, 

39 Term, 

40 UnaryOp, 

41 is_term, 

42) 

43from pandas.core.computation.parsing import ( 

44 clean_backtick_quoted_toks, 

45 tokenize_string, 

46) 

47from pandas.core.computation.scope import Scope 

48 

49from pandas.io.formats import printing 

50 

51if TYPE_CHECKING: 

52 from collections.abc import Callable 

53 

54 

55def _rewrite_assign(tok: tuple[int, str]) -> tuple[int, str]: 

56 """ 

57 Rewrite the assignment operator for PyTables expressions that use ``=`` 

58 as a substitute for ``==``. 

59 

60 Parameters 

61 ---------- 

62 tok : tuple of int, str 

63 ints correspond to the all caps constants in the tokenize module 

64 

65 Returns 

66 ------- 

67 tuple of int, str 

68 Either the input or token or the replacement values 

69 """ 

70 toknum, tokval = tok 

71 return toknum, "==" if tokval == "=" else tokval 

72 

73 

74def _replace_booleans(tok: tuple[int, str]) -> tuple[int, str]: 

75 """ 

76 Replace ``&`` with ``and`` and ``|`` with ``or`` so that bitwise 

77 precedence is changed to boolean precedence. 

78 

79 Parameters 

80 ---------- 

81 tok : tuple of int, str 

82 ints correspond to the all caps constants in the tokenize module 

83 

84 Returns 

85 ------- 

86 tuple of int, str 

87 Either the input or token or the replacement values 

88 """ 

89 toknum, tokval = tok 

90 if toknum == tokenize.OP: 

91 if tokval == "&": 

92 return tokenize.NAME, "and" 

93 elif tokval == "|": 

94 return tokenize.NAME, "or" 

95 return toknum, tokval 

96 return toknum, tokval 

97 

98 

99def _replace_locals(tok: tuple[int, str]) -> tuple[int, str]: 

100 """ 

101 Replace local variables with a syntactically valid name. 

102 

103 Parameters 

104 ---------- 

105 tok : tuple of int, str 

106 ints correspond to the all caps constants in the tokenize module 

107 

108 Returns 

109 ------- 

110 tuple of int, str 

111 Either the input or token or the replacement values 

112 

113 Notes 

114 ----- 

115 This is somewhat of a hack in that we rewrite a string such as ``'@a'`` as 

116 ``'__pd_eval_local_a'`` by telling the tokenizer that ``__pd_eval_local_`` 

117 is a ``tokenize.OP`` and to replace the ``'@'`` symbol with it. 

118 """ 

119 toknum, tokval = tok 

120 if toknum == tokenize.OP and tokval == "@": 

121 return tokenize.OP, LOCAL_TAG 

122 return toknum, tokval 

123 

124 

125def _compose2(f, g): 

126 """ 

127 Compose 2 callables. 

128 """ 

129 return lambda *args, **kwargs: f(g(*args, **kwargs)) 

130 

131 

132def _compose(*funcs): 

133 """ 

134 Compose 2 or more callables. 

135 """ 

136 assert len(funcs) > 1, "At least 2 callables must be passed to compose" 

137 return reduce(_compose2, funcs) 

138 

139 

140def _preparse( 

141 source: str, 

142 f=_compose( 

143 _replace_locals, _replace_booleans, _rewrite_assign, clean_backtick_quoted_toks 

144 ), 

145) -> str: 

146 """ 

147 Compose a collection of tokenization functions. 

148 

149 Parameters 

150 ---------- 

151 source : str 

152 A Python source code string 

153 f : callable 

154 This takes a tuple of (toknum, tokval) as its argument and returns a 

155 tuple with the same structure but possibly different elements. Defaults 

156 to the composition of ``_rewrite_assign``, ``_replace_booleans``, and 

157 ``_replace_locals``. 

158 

159 Returns 

160 ------- 

161 str 

162 Valid Python source code 

163 

164 Notes 

165 ----- 

166 The `f` parameter can be any callable that takes *and* returns input of the 

167 form ``(toknum, tokval)``, where ``toknum`` is one of the constants from 

168 the ``tokenize`` module and ``tokval`` is a string. 

169 """ 

170 assert callable(f), "f must be callable" 

171 return tokenize.untokenize( 

172 f(x) 

173 for x in tokenize_string(source) # pyright: ignore[reportArgumentType] 

174 ) 

175 

176 

177def _is_type(t): 

178 """ 

179 Factory for a type checking function of type ``t`` or tuple of types. 

180 """ 

181 return lambda x: isinstance(x.value, t) 

182 

183 

184_is_list = _is_type(list) 

185_is_str = _is_type(str) 

186 

187 

188# partition all AST nodes 

189_all_nodes = frozenset( 

190 node 

191 for node in (getattr(ast, name) for name in dir(ast)) 

192 if isinstance(node, type) and issubclass(node, ast.AST) 

193) 

194 

195 

196def _filter_nodes(superclass, all_nodes=_all_nodes): 

197 """ 

198 Filter out AST nodes that are subclasses of ``superclass``. 

199 """ 

200 node_names = (node.__name__ for node in all_nodes if issubclass(node, superclass)) 

201 return frozenset(node_names) 

202 

203 

204_all_node_names = frozenset(x.__name__ for x in _all_nodes) 

205_mod_nodes = _filter_nodes(ast.mod) 

206_stmt_nodes = _filter_nodes(ast.stmt) 

207_expr_nodes = _filter_nodes(ast.expr) 

208_expr_context_nodes = _filter_nodes(ast.expr_context) 

209_boolop_nodes = _filter_nodes(ast.boolop) 

210_operator_nodes = _filter_nodes(ast.operator) 

211_unary_op_nodes = _filter_nodes(ast.unaryop) 

212_cmp_op_nodes = _filter_nodes(ast.cmpop) 

213_comprehension_nodes = _filter_nodes(ast.comprehension) 

214_handler_nodes = _filter_nodes(ast.excepthandler) 

215_arguments_nodes = _filter_nodes(ast.arguments) 

216_keyword_nodes = _filter_nodes(ast.keyword) 

217_alias_nodes = _filter_nodes(ast.alias) 

218 

219 

220# nodes that we don't support directly but are needed for parsing 

221_hacked_nodes = frozenset(["Assign", "Module", "Expr"]) 

222 

223 

224_unsupported_expr_nodes = frozenset( 

225 [ 

226 "Yield", 

227 "GeneratorExp", 

228 "IfExp", 

229 "DictComp", 

230 "SetComp", 

231 "Repr", 

232 "Lambda", 

233 "Set", 

234 "AST", 

235 "Is", 

236 "IsNot", 

237 ] 

238) 

239 

240# these nodes are low priority or won't ever be supported (e.g., AST) 

241_unsupported_nodes = ( 

242 _stmt_nodes 

243 | _mod_nodes 

244 | _handler_nodes 

245 | _arguments_nodes 

246 | _keyword_nodes 

247 | _alias_nodes 

248 | _expr_context_nodes 

249 | _unsupported_expr_nodes 

250) - _hacked_nodes 

251 

252# we're adding a different assignment in some cases to be equality comparison 

253# and we don't want `stmt` and friends in their so get only the class whose 

254# names are capitalized 

255_base_supported_nodes = (_all_node_names - _unsupported_nodes) | _hacked_nodes 

256intersection = _unsupported_nodes & _base_supported_nodes 

257_msg = f"cannot both support and not support {intersection}" 

258assert not intersection, _msg 

259 

260 

261def _node_not_implemented(node_name: str) -> Callable[..., None]: 

262 """ 

263 Return a function that raises a NotImplementedError with a passed node name. 

264 """ 

265 

266 def f(self, *args, **kwargs): 

267 raise NotImplementedError(f"'{node_name}' nodes are not implemented") 

268 

269 return f 

270 

271 

272# should be bound by BaseExprVisitor but that creates a circular dependency: 

273# _T is used in disallow, but disallow is used to define BaseExprVisitor 

274# https://github.com/microsoft/pyright/issues/2315 

275_T = TypeVar("_T") 

276 

277 

278def disallow(nodes: set[str]) -> Callable[[type[_T]], type[_T]]: 

279 """ 

280 Decorator to disallow certain nodes from parsing. Raises a 

281 NotImplementedError instead. 

282 

283 Returns 

284 ------- 

285 callable 

286 """ 

287 

288 def disallowed(cls: type[_T]) -> type[_T]: 

289 # error: "Type[_T]" has no attribute "unsupported_nodes" 

290 cls.unsupported_nodes = () # type: ignore[attr-defined] 

291 for node in nodes: 

292 new_method = _node_not_implemented(node) 

293 name = f"visit_{node}" 

294 # error: "Type[_T]" has no attribute "unsupported_nodes" 

295 cls.unsupported_nodes += (name,) # type: ignore[attr-defined] 

296 setattr(cls, name, new_method) 

297 return cls 

298 

299 return disallowed 

300 

301 

302def _op_maker(op_class, op_symbol): 

303 """ 

304 Return a function to create an op class with its symbol already passed. 

305 

306 Returns 

307 ------- 

308 callable 

309 """ 

310 

311 def f(self, node, *args, **kwargs): 

312 """ 

313 Return a partial function with an Op subclass with an operator already passed. 

314 

315 Returns 

316 ------- 

317 callable 

318 """ 

319 return partial(op_class, op_symbol, *args, **kwargs) 

320 

321 return f 

322 

323 

324_op_classes = {"binary": BinOp, "unary": UnaryOp} 

325 

326 

327def add_ops(op_classes): 

328 """ 

329 Decorator to add default implementation of ops. 

330 """ 

331 

332 def f(cls): 

333 for op_attr_name, op_class in op_classes.items(): 

334 ops = getattr(cls, f"{op_attr_name}_ops") 

335 ops_map = getattr(cls, f"{op_attr_name}_op_nodes_map") 

336 for op in ops: 

337 op_node = ops_map[op] 

338 if op_node is not None: 

339 made_op = _op_maker(op_class, op) 

340 setattr(cls, f"visit_{op_node}", made_op) 

341 return cls 

342 

343 return f 

344 

345 

346@disallow(_unsupported_nodes) 

347@add_ops(_op_classes) 

348class BaseExprVisitor(ast.NodeVisitor): 

349 """ 

350 Custom ast walker. Parsers of other engines should subclass this class 

351 if necessary. 

352 

353 Parameters 

354 ---------- 

355 env : Scope 

356 engine : str 

357 parser : str 

358 preparser : callable 

359 """ 

360 

361 const_type: ClassVar[type[Term]] = Constant 

362 term_type: ClassVar[type[Term]] = Term 

363 

364 binary_ops = CMP_OPS_SYMS + BOOL_OPS_SYMS + ARITH_OPS_SYMS 

365 binary_op_nodes = ( 

366 "Gt", 

367 "Lt", 

368 "GtE", 

369 "LtE", 

370 "Eq", 

371 "NotEq", 

372 "In", 

373 "NotIn", 

374 "BitAnd", 

375 "BitOr", 

376 "And", 

377 "Or", 

378 "Add", 

379 "Sub", 

380 "Mult", 

381 "Div", 

382 "Pow", 

383 "FloorDiv", 

384 "Mod", 

385 ) 

386 binary_op_nodes_map = dict(zip(binary_ops, binary_op_nodes, strict=True)) 

387 

388 unary_ops = UNARY_OPS_SYMS 

389 unary_op_nodes = "UAdd", "USub", "Invert", "Not" 

390 unary_op_nodes_map = dict(zip(unary_ops, unary_op_nodes, strict=True)) 

391 

392 rewrite_map = { 

393 ast.Eq: ast.In, 

394 ast.NotEq: ast.NotIn, 

395 ast.In: ast.In, 

396 ast.NotIn: ast.NotIn, 

397 } 

398 

399 unsupported_nodes: tuple[str, ...] 

400 

401 def __init__(self, env, engine, parser, preparser=_preparse) -> None: 

402 self.env = env 

403 self.engine = engine 

404 self.parser = parser 

405 self.preparser = preparser 

406 self.assigner = None 

407 

408 def visit(self, node, **kwargs): 

409 if isinstance(node, str): 

410 clean = self.preparser(node) 

411 try: 

412 node = ast.fix_missing_locations(ast.parse(clean)) 

413 except SyntaxError as e: 

414 if any(iskeyword(x) for x in clean.split()): 

415 e.msg = "Python keyword not valid identifier in numexpr query" 

416 raise e 

417 

418 method = f"visit_{type(node).__name__}" 

419 visitor = getattr(self, method) 

420 return visitor(node, **kwargs) 

421 

422 def visit_Module(self, node, **kwargs): 

423 if len(node.body) != 1: 

424 raise SyntaxError("only a single expression is allowed") 

425 expr = node.body[0] 

426 return self.visit(expr, **kwargs) 

427 

428 def visit_Expr(self, node, **kwargs): 

429 return self.visit(node.value, **kwargs) 

430 

431 def _rewrite_membership_op(self, node, left, right): 

432 # the kind of the operator (is actually an instance) 

433 op_instance = node.op 

434 op_type = type(op_instance) 

435 

436 # must be two terms and the comparison operator must be ==/!=/in/not in 

437 if is_term(left) and is_term(right) and op_type in self.rewrite_map: 

438 left_list, right_list = map(_is_list, (left, right)) 

439 left_str, right_str = map(_is_str, (left, right)) 

440 

441 # if there are any strings or lists in the expression 

442 if left_list or right_list or left_str or right_str: 

443 op_instance = self.rewrite_map[op_type]() 

444 

445 # pop the string variable out of locals and replace it with a list 

446 # of one string, kind of a hack 

447 if right_str: 

448 name = self.env.add_tmp([right.value]) 

449 right = self.term_type(name, self.env) 

450 

451 if left_str: 

452 name = self.env.add_tmp([left.value]) 

453 left = self.term_type(name, self.env) 

454 

455 op = self.visit(op_instance) 

456 return op, op_instance, left, right 

457 

458 def _maybe_transform_eq_ne(self, node, left=None, right=None): 

459 if left is None: 

460 left = self.visit(node.left, side="left") 

461 if right is None: 

462 right = self.visit(node.right, side="right") 

463 op, op_class, left, right = self._rewrite_membership_op(node, left, right) 

464 return op, op_class, left, right 

465 

466 def _maybe_downcast_constants(self, left, right): 

467 f32 = np.dtype(np.float32) 

468 if ( 

469 left.is_scalar 

470 and hasattr(left, "value") 

471 and not right.is_scalar 

472 and right.return_type == f32 

473 ): 

474 # right is a float32 array, left is a scalar 

475 name = self.env.add_tmp(np.float32(left.value)) 

476 left = self.term_type(name, self.env) 

477 if ( 

478 right.is_scalar 

479 and hasattr(right, "value") 

480 and not left.is_scalar 

481 and left.return_type == f32 

482 ): 

483 # left is a float32 array, right is a scalar 

484 name = self.env.add_tmp(np.float32(right.value)) 

485 right = self.term_type(name, self.env) 

486 

487 return left, right 

488 

489 def _maybe_eval(self, binop, eval_in_python): 

490 # eval `in` and `not in` (for now) in "partial" python space 

491 # things that can be evaluated in "eval" space will be turned into 

492 # temporary variables. for example, 

493 # [1,2] in a + 2 * b 

494 # in that case a + 2 * b will be evaluated using numexpr, and the "in" 

495 # call will be evaluated using isin (in python space) 

496 return binop.evaluate( 

497 self.env, self.engine, self.parser, self.term_type, eval_in_python 

498 ) 

499 

500 def _maybe_evaluate_binop( 

501 self, 

502 op, 

503 op_class, 

504 lhs, 

505 rhs, 

506 eval_in_python=("in", "not in"), 

507 maybe_eval_in_python=("==", "!=", "<", ">", "<=", ">="), 

508 ): 

509 res = op(lhs, rhs) 

510 

511 if res.has_invalid_return_type: 

512 raise TypeError( 

513 f"unsupported operand type(s) for {res.op}: " 

514 f"'{lhs.type}' and '{rhs.type}'" 

515 ) 

516 

517 if self.engine != "pytables" and ( 

518 (res.op in CMP_OPS_SYMS and getattr(lhs, "is_datetime", False)) 

519 or getattr(rhs, "is_datetime", False) 

520 ): 

521 # all date ops must be done in python bc numexpr doesn't work 

522 # well with NaT 

523 return self._maybe_eval(res, self.binary_ops) 

524 

525 if res.op in eval_in_python: 

526 # "in"/"not in" ops are always evaluated in python 

527 return self._maybe_eval(res, eval_in_python) 

528 elif self.engine != "pytables": 

529 if ( 

530 getattr(lhs, "return_type", None) == object 

531 or is_string_dtype(getattr(lhs, "return_type", None)) 

532 or getattr(rhs, "return_type", None) == object 

533 or is_string_dtype(getattr(rhs, "return_type", None)) 

534 ): 

535 # evaluate "==" and "!=" in python if either of our operands 

536 # has an object or string return type 

537 return self._maybe_eval(res, eval_in_python + maybe_eval_in_python) 

538 return res 

539 

540 def visit_BinOp(self, node, **kwargs): 

541 op, op_class, left, right = self._maybe_transform_eq_ne(node) 

542 left, right = self._maybe_downcast_constants(left, right) 

543 return self._maybe_evaluate_binop(op, op_class, left, right) 

544 

545 def visit_UnaryOp(self, node, **kwargs): 

546 op = self.visit(node.op) 

547 operand = self.visit(node.operand) 

548 return op(operand) 

549 

550 def visit_Name(self, node, **kwargs) -> Term: 

551 return self.term_type(node.id, self.env, **kwargs) 

552 

553 # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min 

554 def visit_NameConstant(self, node, **kwargs) -> Term: 

555 return self.const_type(node.value, self.env) 

556 

557 # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min 

558 def visit_Num(self, node, **kwargs) -> Term: 

559 return self.const_type(node.value, self.env) 

560 

561 def visit_Constant(self, node, **kwargs) -> Term: 

562 return self.const_type(node.value, self.env) 

563 

564 # TODO(py314): deprecated since Python 3.8. Remove after Python 3.14 is min 

565 def visit_Str(self, node, **kwargs) -> Term: 

566 name = self.env.add_tmp(node.s) 

567 return self.term_type(name, self.env) 

568 

569 def visit_List(self, node, **kwargs) -> Term: 

570 name = self.env.add_tmp([self.visit(e)(self.env) for e in node.elts]) 

571 return self.term_type(name, self.env) 

572 

573 visit_Tuple = visit_List 

574 

575 def visit_Index(self, node, **kwargs): 

576 """df.index[4]""" 

577 return self.visit(node.value) 

578 

579 def visit_Subscript(self, node, **kwargs) -> Term: 

580 from pandas import eval as pd_eval 

581 

582 value = self.visit(node.value) 

583 slobj = self.visit(node.slice) 

584 result = pd_eval( 

585 slobj, local_dict=self.env, engine=self.engine, parser=self.parser 

586 ) 

587 try: 

588 # a Term instance 

589 v = value.value[result] 

590 except AttributeError: 

591 # an Op instance 

592 lhs = pd_eval( 

593 value, local_dict=self.env, engine=self.engine, parser=self.parser 

594 ) 

595 v = lhs[result] 

596 name = self.env.add_tmp(v) 

597 return self.term_type(name, env=self.env) 

598 

599 def visit_Slice(self, node, **kwargs) -> slice: 

600 """df.index[slice(4,6)]""" 

601 lower = node.lower 

602 if lower is not None: 

603 lower = self.visit(lower).value 

604 upper = node.upper 

605 if upper is not None: 

606 upper = self.visit(upper).value 

607 step = node.step 

608 if step is not None: 

609 step = self.visit(step).value 

610 

611 return slice(lower, upper, step) 

612 

613 def visit_Assign(self, node, **kwargs): 

614 """ 

615 support a single assignment node, like 

616 

617 c = a + b 

618 

619 set the assigner at the top level, must be a Name node which 

620 might or might not exist in the resolvers 

621 

622 """ 

623 if len(node.targets) != 1: 

624 raise SyntaxError("can only assign a single expression") 

625 if not isinstance(node.targets[0], ast.Name): 

626 raise SyntaxError("left hand side of an assignment must be a single name") 

627 if self.env.target is None: 

628 raise ValueError("cannot assign without a target object") 

629 

630 try: 

631 assigner = self.visit(node.targets[0], **kwargs) 

632 except UndefinedVariableError: 

633 assigner = node.targets[0].id 

634 

635 self.assigner = getattr(assigner, "name", assigner) 

636 if self.assigner is None: 

637 raise SyntaxError( 

638 "left hand side of an assignment must be a single resolvable name" 

639 ) 

640 

641 return self.visit(node.value, **kwargs) 

642 

643 def visit_Attribute(self, node, **kwargs): 

644 attr = node.attr 

645 value = node.value 

646 

647 ctx = node.ctx 

648 if isinstance(ctx, ast.Load): 

649 # resolve the value 

650 visited_value = self.visit(value) 

651 if hasattr(visited_value, "value"): 

652 resolved = visited_value.value 

653 else: 

654 resolved = visited_value(self.env) 

655 try: 

656 v = getattr(resolved, attr) 

657 name = self.env.add_tmp(v) 

658 return self.term_type(name, self.env) 

659 except AttributeError: 

660 # something like datetime.datetime where scope is overridden 

661 if isinstance(value, ast.Name) and value.id == attr: 

662 return resolved 

663 raise 

664 

665 raise ValueError(f"Invalid Attribute context {type(ctx).__name__}") 

666 

667 def visit_Call(self, node, side=None, **kwargs): 

668 if isinstance(node.func, ast.Attribute) and node.func.attr != "__call__": 

669 res = self.visit_Attribute(node.func) 

670 elif not isinstance(node.func, ast.Name): 

671 raise TypeError("Only named functions are supported") 

672 else: 

673 try: 

674 res = self.visit(node.func) 

675 except UndefinedVariableError: 

676 # Check if this is a supported function name 

677 try: 

678 res = FuncNode(node.func.id) 

679 except ValueError: 

680 # Raise original error 

681 raise 

682 

683 if res is None: 

684 # error: "expr" has no attribute "id" 

685 raise ValueError( 

686 f"Invalid function call {node.func.id}" # type: ignore[union-attr] 

687 ) 

688 if hasattr(res, "value"): 

689 res = res.value 

690 

691 if isinstance(res, FuncNode): 

692 new_args = [self.visit(arg) for arg in node.args] 

693 

694 if node.keywords: 

695 raise TypeError( 

696 f'Function "{res.name}" does not support keyword arguments' 

697 ) 

698 

699 return res(*new_args) 

700 

701 else: 

702 new_args = [self.visit(arg)(self.env) for arg in node.args] 

703 

704 for key in node.keywords: 

705 if not isinstance(key, ast.keyword): 

706 # error: Item "Attribute" of "Attribute | Name" has no 

707 # attribute "id" 

708 raise ValueError( 

709 f"keyword error in function call '{node.func.id}'" # type: ignore[union-attr] 

710 ) 

711 

712 if key.arg: 

713 kwargs[key.arg] = self.visit(key.value)(self.env) 

714 

715 name = self.env.add_tmp(res(*new_args, **kwargs)) 

716 return self.term_type(name=name, env=self.env) 

717 

718 def translate_In(self, op): 

719 return op 

720 

721 def visit_Compare(self, node, **kwargs): 

722 ops = node.ops 

723 comps = node.comparators 

724 

725 # base case: we have something like a CMP b 

726 if len(comps) == 1: 

727 op = self.translate_In(ops[0]) 

728 binop = ast.BinOp(op=op, left=node.left, right=comps[0]) 

729 return self.visit(binop) 

730 

731 # recursive case: we have a chained comparison, a CMP b CMP c, etc. 

732 left = node.left 

733 values = [] 

734 for op, comp in zip(ops, comps, strict=True): 

735 new_node = self.visit( 

736 ast.Compare(comparators=[comp], left=left, ops=[self.translate_In(op)]) 

737 ) 

738 left = comp 

739 values.append(new_node) 

740 return self.visit(ast.BoolOp(op=ast.And(), values=values)) 

741 

742 def _try_visit_binop(self, bop): 

743 if isinstance(bop, (Op, Term)): 

744 return bop 

745 return self.visit(bop) 

746 

747 def visit_BoolOp(self, node, **kwargs): 

748 def visitor(x, y): 

749 lhs = self._try_visit_binop(x) 

750 rhs = self._try_visit_binop(y) 

751 

752 op, op_class, lhs, rhs = self._maybe_transform_eq_ne(node, lhs, rhs) 

753 return self._maybe_evaluate_binop(op, node.op, lhs, rhs) 

754 

755 operands = node.values 

756 return reduce(visitor, operands) 

757 

758 

759_python_not_supported = frozenset(["Dict", "BoolOp", "In", "NotIn"]) 

760_numexpr_supported_calls = frozenset(REDUCTIONS + MATHOPS) 

761 

762 

763@disallow( 

764 (_unsupported_nodes | _python_not_supported) 

765 - (_boolop_nodes | frozenset(["BoolOp", "Attribute", "In", "NotIn", "Tuple"])) 

766) 

767class PandasExprVisitor(BaseExprVisitor): 

768 def __init__( 

769 self, 

770 env, 

771 engine, 

772 parser, 

773 preparser=partial( 

774 _preparse, 

775 f=_compose(_replace_locals, _replace_booleans, clean_backtick_quoted_toks), 

776 ), 

777 ) -> None: 

778 super().__init__(env, engine, parser, preparser) 

779 

780 

781@disallow(_unsupported_nodes | _python_not_supported | frozenset(["Not"])) 

782class PythonExprVisitor(BaseExprVisitor): 

783 def __init__( 

784 self, env, engine, parser, preparser=lambda source, f=None: source 

785 ) -> None: 

786 super().__init__(env, engine, parser, preparser=preparser) 

787 

788 

789class Expr: 

790 """ 

791 Object encapsulating an expression. 

792 

793 Parameters 

794 ---------- 

795 expr : str 

796 engine : str, optional, default 'numexpr' 

797 parser : str, optional, default 'pandas' 

798 env : Scope, optional, default None 

799 level : int, optional, default 2 

800 """ 

801 

802 env: Scope 

803 engine: str 

804 parser: str 

805 

806 def __init__( 

807 self, 

808 expr, 

809 engine: str = "numexpr", 

810 parser: str = "pandas", 

811 env: Scope | None = None, 

812 level: int = 0, 

813 ) -> None: 

814 self.expr = expr 

815 self.env = env or Scope(level=level + 1) 

816 self.engine = engine 

817 self.parser = parser 

818 self._visitor = PARSERS[parser](self.env, self.engine, self.parser) 

819 self.terms = self.parse() 

820 

821 @property 

822 def assigner(self): 

823 return getattr(self._visitor, "assigner", None) 

824 

825 def __call__(self): 

826 return self.terms(self.env) 

827 

828 def __repr__(self) -> str: 

829 return printing.pprint_thing(self.terms) 

830 

831 def __len__(self) -> int: 

832 return len(self.expr) 

833 

834 def parse(self): 

835 """ 

836 Parse an expression. 

837 """ 

838 return self._visitor.visit(self.expr) 

839 

840 @property 

841 def names(self): 

842 """ 

843 Get the names in an expression. 

844 """ 

845 if is_term(self.terms): 

846 return frozenset([self.terms.name]) 

847 return frozenset(term.name for term in com.flatten(self.terms)) 

848 

849 

850PARSERS = {"python": PythonExprVisitor, "pandas": PandasExprVisitor}