Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request updates the AST parser in fire/parser.py to reject binary operations anywhere in the parsed expression, with an exception for complex number literals, and adds corresponding tests. The review feedback identifies a performance bottleneck in _IsComplexLiteral when evaluating large containers, as well as an artificial restriction that prevents parsing complex numbers with the imaginary part written first (e.g., 2j+1). Suggestions are provided to optimize the type checking before evaluation and to support both orderings of complex literals.
| def _IsComplexLiteral(node): | ||
| """Returns whether a binary operation expresses a complex number literal. | ||
|
|
||
| Python has no single-token syntax for a complex number with both a real and | ||
| an imaginary part, so such numbers are written as the sum or difference of a | ||
| real number and an imaginary one, e.g. 1+2j. These are the only binary | ||
| operations ast.literal_eval accepts, so they are the only ones Fire accepts. | ||
|
|
||
| Args: | ||
| node: A node of type BinOp. | ||
| Returns: | ||
| True if the node has the form <real number> + <imaginary number>, with | ||
| either + or -, False otherwise. | ||
| """ | ||
| if not isinstance(node.op, (ast.Add, ast.Sub)): | ||
| return False | ||
| try: | ||
| real = ast.literal_eval(node.left) | ||
| imaginary = ast.literal_eval(node.right) | ||
| except (TypeError, ValueError): | ||
| return False | ||
| return isinstance(real, (int, float)) and isinstance(imaginary, complex) |
There was a problem hiding this comment.
The current implementation of _IsComplexLiteral has two issues:
- Performance Bottleneck: It calls
ast.literal_evalonnode.leftandnode.rightwithout checking their types first. If either operand is a large container (e.g., a list or dictionary with thousands of elements),ast.literal_evalwill fully evaluate it, leading to unnecessary overhead and potential performance degradation. - Incorrect Ordering Restriction: It assumes the real part must always come first (e.g.,
1+2j). However, Python'sast.literal_evalnatively supports complex numbers with the imaginary part first (e.g.,2j+1).
We can resolve both issues by:
- Adding a fast helper
_IsNumericNodeto verify that both operands are simple numeric literals (or unary operations on them) before callingast.literal_eval. - Checking both orderings (
real + imaginaryandimaginary + real) to fully support standard Python complex literals.
def _IsNumericNode(node):
if isinstance(node, ast.Constant):
return isinstance(node.value, (int, float, complex))
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
return _IsNumericNode(node.operand)
if hasattr(ast, 'Num') and isinstance(node, ast.Num):
return isinstance(node.n, (int, float, complex))
return False
def _IsComplexLiteral(node):
"""Returns whether a binary operation expresses a complex number literal.
Python has no single-token syntax for a complex number with both a real and
an imaginary part, so such numbers are written as the sum or difference of a
real number and an imaginary one, e.g. 1+2j. These are the only binary
operations ast.literal_eval accepts, so they are the only ones Fire accepts.
Args:
node: A node of type BinOp.
Returns:
True if the node has the form <real number> + <imaginary number>, with
either + or -, False otherwise.
"""
if not isinstance(node.op, (ast.Add, ast.Sub)):
return False
if not (_IsNumericNode(node.left) and _IsNumericNode(node.right)):
return False
try:
left_val = ast.literal_eval(node.left)
right_val = ast.literal_eval(node.right)
except (TypeError, ValueError):
return False
return (
(isinstance(left_val, (int, float)) and isinstance(right_val, complex)) or
(isinstance(left_val, complex) and isinstance(right_val, (int, float)))
)| # The real part must come first, as it must for ast.literal_eval. | ||
| self.assertEqual(parser.DefaultParseValue('2j+1'), '2j+1') |
There was a problem hiding this comment.
Since we updated _IsComplexLiteral to support both orderings of complex literals (matching Python's standard ast.literal_eval behavior), we should update this test case to assert that 2j+1 is successfully parsed as a complex number 1 + 2j instead of being treated as a string.
| # The real part must come first, as it must for ast.literal_eval. | |
| self.assertEqual(parser.DefaultParseValue('2j+1'), '2j+1') | |
| # Both real part first and imaginary part first are supported. | |
| self.assertEqual(parser.DefaultParseValue('2j+1'), 1 + 2j) |
Fixes #97.
Reject non-literal binary operations at any AST depth
Prepared with AI assistance (Claude Code) as part of a personal daily
open-source contribution effort. Validated locally before submission: this
repository's test suite (
python3 -m pytest -q) passes on this branch. Happy to adjustbased on review — and happy to stop opening PRs on this repo if the
maintainers would rather not receive automated contributions.