Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/computation/ops.py: 39%

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

275 statements  

1""" 

2Operator classes for eval. 

3""" 

4 

5from __future__ import annotations 

6 

7from datetime import datetime 

8from functools import partial 

9import operator 

10from typing import ( 

11 TYPE_CHECKING, 

12 Literal, 

13) 

14 

15import numpy as np 

16 

17from pandas._libs.tslibs import Timestamp 

18 

19from pandas.core.dtypes.common import ( 

20 is_list_like, 

21 is_scalar, 

22) 

23from pandas.core.dtypes.dtypes import ( 

24 IntervalDtype, 

25 PeriodDtype, 

26) 

27 

28import pandas.core.common as com 

29from pandas.core.computation.common import ( 

30 ensure_decoded, 

31 result_type_many, 

32) 

33from pandas.core.computation.scope import DEFAULT_GLOBALS 

34 

35from pandas.io.formats.printing import ( 

36 pprint_thing, 

37 pprint_thing_encoded, 

38) 

39 

40if TYPE_CHECKING: 

41 from collections.abc import ( 

42 Callable, 

43 Iterable, 

44 Iterator, 

45 ) 

46 

47REDUCTIONS = ("sum", "prod", "min", "max") 

48 

49_unary_math_ops = ( 

50 "sin", 

51 "cos", 

52 "tan", 

53 "exp", 

54 "log", 

55 "expm1", 

56 "log1p", 

57 "sqrt", 

58 "sinh", 

59 "cosh", 

60 "tanh", 

61 "arcsin", 

62 "arccos", 

63 "arctan", 

64 "arccosh", 

65 "arcsinh", 

66 "arctanh", 

67 "abs", 

68 "log10", 

69 "floor", 

70 "ceil", 

71) 

72_binary_math_ops = ("arctan2",) 

73 

74MATHOPS = _unary_math_ops + _binary_math_ops 

75 

76 

77LOCAL_TAG = "__pd_eval_local_" 

78 

79 

80class Term: 

81 def __new__(cls, name, env, side=None, encoding=None): 

82 klass = Constant if not isinstance(name, str) else cls 

83 supr_new = super(Term, klass).__new__ 

84 return supr_new(klass) 

85 

86 is_local: bool 

87 

88 def __init__(self, name, env, side=None, encoding=None) -> None: 

89 # name is a str for Term, but may be something else for subclasses 

90 self._name = name 

91 self.env = env 

92 self.side = side 

93 tname = str(name) 

94 self.is_local = tname.startswith(LOCAL_TAG) or tname in DEFAULT_GLOBALS 

95 self._value = self._resolve_name() 

96 self.encoding = encoding 

97 

98 @property 

99 def local_name(self) -> str: 

100 return self.name.replace(LOCAL_TAG, "") 

101 

102 def __repr__(self) -> str: 

103 return pprint_thing(self.name) 

104 

105 def __call__(self, *args, **kwargs): 

106 return self.value 

107 

108 def evaluate(self, *args, **kwargs) -> Term: 

109 return self 

110 

111 def _resolve_name(self): 

112 local_name = str(self.local_name) 

113 is_local = self.is_local 

114 if ( 

115 not str(self.name).startswith(LOCAL_TAG) 

116 and local_name in self.env.scope 

117 and isinstance(self.env.scope[local_name], type) 

118 ): 

119 is_local = False 

120 

121 res = self.env.resolve(local_name, is_local=is_local) 

122 self.update(res) 

123 

124 if hasattr(res, "ndim") and isinstance(res.ndim, int) and res.ndim > 2: 

125 raise NotImplementedError( 

126 "N-dimensional objects, where N > 2, are not supported with eval" 

127 ) 

128 return res 

129 

130 def update(self, value) -> None: 

131 """ 

132 search order for local (i.e., @variable) variables: 

133 

134 scope, key_variable 

135 [('locals', 'local_name'), 

136 ('globals', 'local_name'), 

137 ('locals', 'key'), 

138 ('globals', 'key')] 

139 """ 

140 key = self.name 

141 

142 # if it's a variable name (otherwise a constant) 

143 if isinstance(key, str): 

144 self.env.swapkey(self.local_name, key, new_value=value) 

145 

146 self.value = value 

147 

148 @property 

149 def is_scalar(self) -> bool: 

150 return is_scalar(self._value) 

151 

152 @property 

153 def type(self): 

154 try: 

155 # GH#35247 avoid .values which boxes PeriodArray/IntervalArray 

156 dtype = self._value.dtype 

157 except AttributeError: 

158 try: 

159 return self._value.values.dtype # DataFrame 

160 except AttributeError: 

161 return type(self._value) # scalar 

162 

163 # Match .values.dtype behavior for types that external_values boxes 

164 if isinstance(dtype, (PeriodDtype, IntervalDtype)): 

165 return np.dtype("object") 

166 return dtype 

167 

168 return_type = type 

169 

170 @property 

171 def raw(self) -> str: 

172 return f"{type(self).__name__}(name={self.name!r}, type={self.type})" 

173 

174 @property 

175 def is_datetime(self) -> bool: 

176 try: 

177 t = self.type.type 

178 except AttributeError: 

179 t = self.type 

180 

181 return issubclass(t, (datetime, np.datetime64)) 

182 

183 @property 

184 def value(self): 

185 return self._value 

186 

187 @value.setter 

188 def value(self, new_value) -> None: 

189 self._value = new_value 

190 

191 @property 

192 def name(self): 

193 return self._name 

194 

195 @property 

196 def ndim(self) -> int: 

197 return self._value.ndim 

198 

199 

200class Constant(Term): 

201 def _resolve_name(self): 

202 return self._name 

203 

204 @property 

205 def name(self): 

206 return self.value 

207 

208 def __repr__(self) -> str: 

209 return repr(self.name) 

210 

211 

212_bool_op_map = {"not": "~", "and": "&", "or": "|"} 

213 

214 

215class Op: 

216 """ 

217 Hold an operator of arbitrary arity. 

218 """ 

219 

220 op: str 

221 

222 def __init__(self, op: str, operands: Iterable[Term | Op], encoding=None) -> None: 

223 self.op = _bool_op_map.get(op, op) 

224 self.operands = operands 

225 self.encoding = encoding 

226 

227 def __iter__(self) -> Iterator[Term | Op]: 

228 return iter(self.operands) 

229 

230 def __repr__(self) -> str: 

231 """ 

232 Print a generic n-ary operator and its operands using infix notation. 

233 """ 

234 # recurse over the operands 

235 parened = (f"({pprint_thing(opr)})" for opr in self.operands) 

236 return pprint_thing(f" {self.op} ".join(parened)) 

237 

238 @property 

239 def return_type(self): 

240 # clobber types to bool if the op is a boolean operator 

241 if self.op in (CMP_OPS_SYMS + BOOL_OPS_SYMS): 

242 return np.bool_ 

243 return result_type_many(*(term.type for term in com.flatten(self))) 

244 

245 @property 

246 def has_invalid_return_type(self) -> bool: 

247 types = self.operand_types 

248 obj_dtype_set = frozenset([np.dtype("object")]) 

249 return self.return_type == object and types - obj_dtype_set 

250 

251 @property 

252 def operand_types(self): 

253 return frozenset(term.type for term in com.flatten(self)) 

254 

255 @property 

256 def is_scalar(self) -> bool: 

257 return all(operand.is_scalar for operand in self.operands) 

258 

259 @property 

260 def is_datetime(self) -> bool: 

261 try: 

262 t = self.return_type.type 

263 except AttributeError: 

264 t = self.return_type 

265 

266 return issubclass(t, (datetime, np.datetime64)) 

267 

268 

269def _in(x, y): 

270 """ 

271 Compute the vectorized membership of ``x in y`` if possible, otherwise 

272 use Python. 

273 """ 

274 try: 

275 return x.isin(y) 

276 except AttributeError: 

277 if is_list_like(x): 

278 try: 

279 return y.isin(x) 

280 except AttributeError: 

281 pass 

282 return x in y 

283 

284 

285def _not_in(x, y): 

286 """ 

287 Compute the vectorized membership of ``x not in y`` if possible, 

288 otherwise use Python. 

289 """ 

290 try: 

291 return ~x.isin(y) 

292 except AttributeError: 

293 if is_list_like(x): 

294 try: 

295 return ~y.isin(x) 

296 except AttributeError: 

297 pass 

298 return x not in y 

299 

300 

301CMP_OPS_SYMS = (">", "<", ">=", "<=", "==", "!=", "in", "not in") 

302_cmp_ops_funcs = ( 

303 operator.gt, 

304 operator.lt, 

305 operator.ge, 

306 operator.le, 

307 operator.eq, 

308 operator.ne, 

309 _in, 

310 _not_in, 

311) 

312_cmp_ops_dict = dict(zip(CMP_OPS_SYMS, _cmp_ops_funcs, strict=True)) 

313 

314BOOL_OPS_SYMS = ("&", "|", "and", "or") 

315_bool_ops_funcs = (operator.and_, operator.or_, operator.and_, operator.or_) 

316_bool_ops_dict = dict(zip(BOOL_OPS_SYMS, _bool_ops_funcs, strict=True)) 

317 

318ARITH_OPS_SYMS = ("+", "-", "*", "/", "**", "//", "%") 

319_arith_ops_funcs = ( 

320 operator.add, 

321 operator.sub, 

322 operator.mul, 

323 operator.truediv, 

324 operator.pow, 

325 operator.floordiv, 

326 operator.mod, 

327) 

328_arith_ops_dict = dict(zip(ARITH_OPS_SYMS, _arith_ops_funcs, strict=True)) 

329 

330_binary_ops_dict = {} 

331 

332for d in (_cmp_ops_dict, _bool_ops_dict, _arith_ops_dict): 

333 _binary_ops_dict.update(d) 

334 

335 

336def is_term(obj) -> bool: 

337 return isinstance(obj, Term) 

338 

339 

340class BinOp(Op): 

341 """ 

342 Hold a binary operator and its operands. 

343 

344 Parameters 

345 ---------- 

346 op : str 

347 lhs : Term or Op 

348 rhs : Term or Op 

349 """ 

350 

351 def __init__(self, op: str, lhs, rhs) -> None: 

352 super().__init__(op, (lhs, rhs)) 

353 self.lhs = lhs 

354 self.rhs = rhs 

355 

356 self._disallow_scalar_only_bool_ops() 

357 

358 self.convert_values() 

359 

360 try: 

361 self.func = _binary_ops_dict[op] 

362 except KeyError as err: 

363 # has to be made a list for python3 

364 keys = list(_binary_ops_dict.keys()) 

365 raise ValueError( 

366 f"Invalid binary operator {op!r}, valid operators are {keys}" 

367 ) from err 

368 

369 def __call__(self, env): 

370 """ 

371 Recursively evaluate an expression in Python space. 

372 

373 Parameters 

374 ---------- 

375 env : Scope 

376 

377 Returns 

378 ------- 

379 object 

380 The result of an evaluated expression. 

381 """ 

382 # recurse over the left/right nodes 

383 left = self.lhs(env) 

384 right = self.rhs(env) 

385 

386 return self.func(left, right) 

387 

388 def evaluate(self, env, engine: str, parser, term_type, eval_in_python): 

389 """ 

390 Evaluate a binary operation *before* being passed to the engine. 

391 

392 Parameters 

393 ---------- 

394 env : Scope 

395 engine : str 

396 parser : str 

397 term_type : type 

398 eval_in_python : list 

399 

400 Returns 

401 ------- 

402 term_type 

403 The "pre-evaluated" expression as an instance of ``term_type`` 

404 """ 

405 if engine == "python": 

406 res = self(env) 

407 else: 

408 # recurse over the left/right nodes 

409 

410 left = self.lhs.evaluate( 

411 env, 

412 engine=engine, 

413 parser=parser, 

414 term_type=term_type, 

415 eval_in_python=eval_in_python, 

416 ) 

417 

418 right = self.rhs.evaluate( 

419 env, 

420 engine=engine, 

421 parser=parser, 

422 term_type=term_type, 

423 eval_in_python=eval_in_python, 

424 ) 

425 

426 # base cases 

427 if self.op in eval_in_python: 

428 res = self.func(left.value, right.value) 

429 else: 

430 from pandas.core.computation.eval import eval 

431 

432 res = eval(self, local_dict=env, engine=engine, parser=parser) 

433 

434 name = env.add_tmp(res) 

435 return term_type(name, env=env) 

436 

437 def convert_values(self) -> None: 

438 """ 

439 Convert datetimes to a comparable value in an expression. 

440 """ 

441 

442 def stringify(value): 

443 encoder: Callable[[object], str | bytes] 

444 if self.encoding is not None: 

445 encoder = partial(pprint_thing_encoded, encoding=self.encoding) 

446 else: 

447 encoder = pprint_thing 

448 return encoder(value) 

449 

450 lhs, rhs = self.lhs, self.rhs 

451 

452 if is_term(lhs) and lhs.is_datetime and is_term(rhs) and rhs.is_scalar: 

453 v = rhs.value 

454 if isinstance(v, (int, float)): 

455 v = stringify(v) 

456 v = Timestamp(ensure_decoded(v)) 

457 if v.tz is not None: 

458 v = v.tz_convert("UTC") 

459 self.rhs.update(v) 

460 

461 if is_term(rhs) and rhs.is_datetime and is_term(lhs) and lhs.is_scalar: 

462 v = lhs.value 

463 if isinstance(v, (int, float)): 

464 v = stringify(v) 

465 v = Timestamp(ensure_decoded(v)) 

466 if v.tz is not None: 

467 v = v.tz_convert("UTC") 

468 self.lhs.update(v) 

469 

470 def _disallow_scalar_only_bool_ops(self) -> None: 

471 rhs = self.rhs 

472 lhs = self.lhs 

473 

474 # GH#24883 unwrap dtype if necessary to ensure we have a type object 

475 rhs_rt = rhs.return_type 

476 rhs_rt = getattr(rhs_rt, "type", rhs_rt) 

477 lhs_rt = lhs.return_type 

478 lhs_rt = getattr(lhs_rt, "type", lhs_rt) 

479 if ( 

480 (lhs.is_scalar or rhs.is_scalar) 

481 and self.op in _bool_ops_dict 

482 and ( 

483 not ( 

484 issubclass(rhs_rt, (bool, np.bool_)) 

485 and issubclass(lhs_rt, (bool, np.bool_)) 

486 ) 

487 ) 

488 ): 

489 raise NotImplementedError("cannot evaluate scalar only bool ops") 

490 

491 

492UNARY_OPS_SYMS = ("+", "-", "~", "not") 

493_unary_ops_funcs = (operator.pos, operator.neg, operator.invert, operator.invert) 

494_unary_ops_dict = dict(zip(UNARY_OPS_SYMS, _unary_ops_funcs, strict=True)) 

495 

496 

497class UnaryOp(Op): 

498 """ 

499 Hold a unary operator and its operands. 

500 

501 Parameters 

502 ---------- 

503 op : str 

504 The token used to represent the operator. 

505 operand : Term or Op 

506 The Term or Op operand to the operator. 

507 

508 Raises 

509 ------ 

510 ValueError 

511 * If no function associated with the passed operator token is found. 

512 """ 

513 

514 def __init__(self, op: Literal["+", "-", "~", "not"], operand) -> None: 

515 super().__init__(op, (operand,)) 

516 self.operand = operand 

517 

518 try: 

519 self.func = _unary_ops_dict[op] 

520 except KeyError as err: 

521 raise ValueError( 

522 f"Invalid unary operator {op!r}, valid operators are {UNARY_OPS_SYMS}" 

523 ) from err 

524 

525 def __call__(self, env) -> MathCall: 

526 operand = self.operand(env) 

527 # error: Cannot call function of unknown type 

528 return self.func(operand) # type: ignore[operator] 

529 

530 def __repr__(self) -> str: 

531 return pprint_thing(f"{self.op}({self.operand})") 

532 

533 @property 

534 def return_type(self) -> np.dtype: 

535 operand = self.operand 

536 if operand.return_type == np.dtype("bool"): 

537 return np.dtype("bool") 

538 if isinstance(operand, Op) and ( 

539 operand.op in _cmp_ops_dict or operand.op in _bool_ops_dict 

540 ): 

541 return np.dtype("bool") 

542 return np.dtype("int") 

543 

544 

545class MathCall(Op): 

546 def __init__(self, func, args) -> None: 

547 super().__init__(func.name, args) 

548 self.func = func 

549 

550 def __call__(self, env): 

551 # error: "Op" not callable 

552 operands = [op(env) for op in self.operands] # type: ignore[operator] 

553 return self.func.func(*operands) 

554 

555 def __repr__(self) -> str: 

556 operands = map(str, self.operands) 

557 return pprint_thing(f"{self.op}({','.join(operands)})") 

558 

559 

560class FuncNode: 

561 def __init__(self, name: str) -> None: 

562 if name not in MATHOPS: 

563 raise ValueError(f'"{name}" is not a supported function') 

564 self.name = name 

565 self.func = getattr(np, name) 

566 

567 def __call__(self, *args) -> MathCall: 

568 return MathCall(self, args)