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

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 

47import pandas.io.formats.printing as 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)) 124 ↛ exitline 124 didn't run the lambda on line 124

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) 173 ↛ exitline 173 didn't run the lambda on line 173

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: 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) 

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 = {k: v for k, v in 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 = "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 

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

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

436 

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]() 

440 

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) 

446 

447 if left_str: 

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

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

450 

451 op = self.visit(op_instance) 

452 return op, op_instance, left, right 

453 

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 

461 

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) 

482 

483 return left, right 

484 

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 ) 

495 

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) 

506 

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 ) 

512 

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) 

521 

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 

534 

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) 

539 

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

541 return lambda lhs, rhs: Div(lhs, rhs) 

542 

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

544 op = self.visit(node.op) 

545 operand = self.visit(node.operand) 

546 return op(operand) 

547 

548 def visit_Name(self, node, **kwargs): 

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

550 

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

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

553 

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

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

556 

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

558 return self.const_type(node.n, self.env) 

559 

560 def visit_Str(self, node, **kwargs): 

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

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

563 

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) 

567 

568 visit_Tuple = visit_List 

569 

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

571 """df.index[4]""" 

572 return self.visit(node.value) 

573 

574 def visit_Subscript(self, node, **kwargs): 

575 from pandas import eval as pd_eval 

576 

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) 

593 

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 

605 

606 return slice(lower, upper, step) 

607 

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

609 """ 

610 support a single assignment node, like 

611 

612 c = a + b 

613 

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

615 might or might not exist in the resolvers 

616 

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") 

624 

625 try: 

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

627 except UndefinedVariableError: 

628 assigner = node.targets[0].id 

629 

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 ) 

635 

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

637 

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

639 attr = node.attr 

640 value = node.value 

641 

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 

655 

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

657 

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

659 

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 

674 

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 

682 

683 if isinstance(res, FuncNode): 

684 

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

686 

687 if node.keywords: 

688 raise TypeError( 

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

690 ) 

691 

692 return res(*new_args) 

693 

694 else: 

695 

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

697 

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 ) 

705 

706 if key.arg: 

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

708 

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

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

711 

712 def translate_In(self, op): 

713 return op 

714 

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

716 ops = node.ops 

717 comps = node.comparators 

718 

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) 

724 

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)) 

735 

736 def _try_visit_binop(self, bop): 

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

738 return bop 

739 return self.visit(bop) 

740 

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) 

745 

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) 

748 

749 operands = node.values 

750 return reduce(visitor, operands) 

751 

752 

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

754_numexpr_supported_calls = frozenset(REDUCTIONS + MATHOPS) 

755 

756 

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) 

773 

774 

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) 

781 

782 

783class Expr: 

784 """ 

785 Object encapsulating an expression. 

786 

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 """ 

795 

796 env: Scope 

797 engine: str 

798 parser: str 

799 

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() 

814 

815 @property 

816 def assigner(self): 

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

818 

819 def __call__(self): 

820 return self.terms(self.env) 

821 

822 def __repr__(self) -> str: 

823 return printing.pprint_thing(self.terms) 

824 

825 def __len__(self) -> int: 

826 return len(self.expr) 

827 

828 def parse(self): 

829 """ 

830 Parse an expression. 

831 """ 

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

833 

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)) 

842 

843 

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