Transform operators for consistency with query(). query() uses pd.eval() which transforms these operators automatically. We replicate that behavior here so syntax that works in query() also works in eval(). Transformations: 1. 'and'/'or'/'not' -> '&'/'|'/'~' 2. 'a < b < c'
| 50 | |
| 51 | |
| 52 | class LogicalOperatorTransformer(ast.NodeTransformer): |
| 53 | """Transform operators for consistency with query(). |
| 54 | |
| 55 | query() uses pd.eval() which transforms these operators automatically. |
| 56 | We replicate that behavior here so syntax that works in query() also |
| 57 | works in eval(). |
| 58 | |
| 59 | Transformations: |
| 60 | 1. 'and'/'or'/'not' -> '&'/'|'/'~' |
| 61 | 2. 'a < b < c' -> '(a < b) & (b < c)' |
| 62 | |
| 63 | These constructs fail on arrays in standard Python because they call |
| 64 | __bool__(), which is ambiguous for multi-element arrays. |
| 65 | """ |
| 66 | |
| 67 | def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST: |
| 68 | # Transform: a and b -> a & b, a or b -> a | b |
| 69 | self.generic_visit(node) |
| 70 | op: ast.BitAnd | ast.BitOr |
| 71 | if isinstance(node.op, ast.And): |
| 72 | op = ast.BitAnd() |
| 73 | elif isinstance(node.op, ast.Or): |
| 74 | op = ast.BitOr() |
| 75 | else: |
| 76 | return node |
| 77 | |
| 78 | # BoolOp can have multiple values: a and b and c |
| 79 | # Transform to chained BinOp: (a & b) & c |
| 80 | result = node.values[0] |
| 81 | for value in node.values[1:]: |
| 82 | result = ast.BinOp(left=result, op=op, right=value) |
| 83 | return ast.fix_missing_locations(result) |
| 84 | |
| 85 | def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST: |
| 86 | # Transform: not a -> ~a |
| 87 | self.generic_visit(node) |
| 88 | if isinstance(node.op, ast.Not): |
| 89 | return ast.fix_missing_locations( |
| 90 | ast.UnaryOp(op=ast.Invert(), operand=node.operand) |
| 91 | ) |
| 92 | return node |
| 93 | |
| 94 | def visit_Compare(self, node: ast.Compare) -> ast.AST: |
| 95 | # Transform chained comparisons: 1 < x < 5 -> (1 < x) & (x < 5) |
| 96 | # Python's chained comparisons use short-circuit evaluation at runtime, |
| 97 | # which calls __bool__ on intermediate results. This fails for arrays. |
| 98 | # We transform to bitwise AND which works element-wise. |
| 99 | self.generic_visit(node) |
| 100 | |
| 101 | if len(node.ops) == 1: |
| 102 | # Simple comparison, no transformation needed |
| 103 | return node |
| 104 | |
| 105 | # Build individual comparisons and chain with BitAnd |
| 106 | # For: a < b < c < d |
| 107 | # We need: (a < b) & (b < c) & (c < d) |
| 108 | comparisons = [] |
| 109 | left = node.left |
no outgoing calls
no test coverage detected
searching dependent graphs…