Removing `not` from a parenthesized multi-line expression drops the parentheses (InvalidGeneratedSyntaxException, 3.8.0)
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
Research direction
The bug is in operator_remove_unary_ops in src/mutmut/mutation/mutators.py, which yields the operand of a not or ~ expression and drops the lpar/rpar on the UnaryOperation node. Reproduce with the short mutate_file_contents snippet in the issue and check that ast.parse fails on the mutant. Done when the operand keeps the outer parentheses, the mutant parses, and the example in the issue produces valid mutants.
Written by the indexing model from the issue text.
Description
🤖 Written by Claude (AI assistant). The reproduction below was run on a clean environment.
Summary
operator_remove_unary_ops replaces a not / ~ expression with its operand. In libcst, the parentheses that wrap the whole unary expression belong to the UnaryOperation node (lpar / rpar), so they are dropped together with the operator. When the expression spans several lines (which is how black and ruff format a long boolean return), the mutant is invalid Python and mutmut run aborts with InvalidGeneratedSyntaxException, so no mutant of the run gets tested.
Reproduction (mutmut 3.8.0, Python 3.12)
mylib/core.py:
def is_not_before(a, b):
return (
not a
< b
)
tests/test_core.py:
from mylib.core import is_not_before
def test_is_not_before():
assert is_not_before(2, 1) is True
assert is_not_before(1, 2) is False
pyproject.toml:
[tool.mutmut]
source_paths = ["mylib/"]
pytest_add_cli_args_test_selection = ["tests/"]
mutmut run:
mutmut.__main__.InvalidGeneratedSyntaxException: Mutmut generated invalid python syntax for Mutmut generated invalid python syntax for mutants/mylib/core.py. If the original file has valid python syntax, please file an issue with a minimal reproducible example file.. If the original file has valid python syntax, please file an issue with a minimal reproducible example file.
It also reproduces without running any test:
import ast
from mutmut.mutation.file_mutation import mutate_file_contents
mf = mutate_file_contents("mylib/core.py", open("mylib/core.py").read())
ast.parse(mf.code) # SyntaxError: unexpected indent
The generated mutant:
def x_is_not_before__mutmut_1(a, b):
return a
< b
Cause
src/mutmut/mutation/mutators.py (same on main):
def operator_remove_unary_ops(node: cst.UnaryOperation) -> Iterable[cst.BaseExpression]:
if isinstance(node.operator, cst.Not | cst.BitInvert):
yield node.expression
In (not a\n < b), the parentheses are node.lpar / node.rpar, and node.expression is the Comparison without them.
Suggested fix
Carry the parentheses over to the operand:
def operator_remove_unary_ops(node: cst.UnaryOperation) -> Iterable[cst.BaseExpression]:
if isinstance(node.operator, cst.Not | cst.BitInvert):
yield node.expression.with_changes(
lpar=[*node.lpar, *node.expression.lpar],
rpar=[*node.expression.rpar, *node.rpar],
)
With this change, the example above generates 2 valid mutants (1 killed, 1 survived), and mutmut show mylib.core.x_is_not_before__mutmut_1 gives:
def is_not_before(a, b):
return (
- not a
+ a
< b
)
Side note: the exception message is printed twice ("Mutmut generated invalid python syntax for Mutmut generated invalid python syntax for ...").
- Dominant language
- Python
- Stars
- 1.5k
- Forks
- 181
- PR merge metrics
- No merged PRs in 30d
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from boxed/mutmut
-
`run` with a `name__mutmut_*` pattern runs the whole test suite in the clean-test checkPossibly taken @Ishaan400 claimed this 5 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Invalid unary plus generated in match statementPossibly taken @GhostCoder6969 claimed this 14 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
Similar issues
-
Agentic
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Imbad0202/academic-research-skills#979 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
reflex-dev/reflex#7561 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
prometheus-community/ansible#906 · 2 comments ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Deltares/imod-python#1940 ·
Maintainers usually reply within 1 day