Skip to content

Commit afdf19c

Browse files
committed
Add pragma: no mutate block and document no mutate class
Support ignoring entire indentation blocks via `# pragma: no mutate block`. When placed on its own line, all lines at the same or deeper indent are skipped. When placed inline, only deeper-indented lines are skipped. Also makes pragma parsing more flexible with multiple comma-separated pragmas on a single line, and documents both the block and class pragmas in the README.
1 parent bc70d52 commit afdf19c

3 files changed

Lines changed: 191 additions & 9 deletions

File tree

README.rst

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,73 @@ This is useful for functions that:
268268
- Are known to cause issues with the mutation testing framework
269269

270270

271+
Skipping Entire Classes
272+
~~~~~~~~~~~~~~~~~~~~~~~
273+
274+
You can skip an entire class (including all of its methods) from mutation
275+
using ``# pragma: no mutate class``:
276+
277+
.. code-block:: python
278+
279+
class MySettings: # pragma: no mutate class
280+
DEBUG = True
281+
MAX_RETRIES = 3
282+
283+
Both syntax styles are supported:
284+
285+
- ``# pragma: no mutate class``
286+
- ``# pragma: no mutate: class``
287+
288+
This is useful for configuration classes, constants containers, or any class
289+
where mutations would not produce meaningful test failures.
290+
291+
292+
Skipping Code Blocks
293+
~~~~~~~~~~~~~~~~~~~~
294+
295+
You can skip an entire indentation block from mutation using
296+
``# pragma: no mutate block``. The pragma suppresses mutations for all
297+
subsequent lines at the same or deeper indentation level, until the
298+
indentation drops back.
299+
300+
When placed on its own line, it ignores everything at the same indent level
301+
and below:
302+
303+
.. code-block:: python
304+
305+
def foo():
306+
# pragma: no mutate block
307+
x = 1
308+
y = complex_calculation()
309+
z = x + y
310+
311+
def bar():
312+
# this function is still mutated normally
313+
return 42
314+
315+
When placed inline after code, it ignores only the indented body:
316+
317+
.. code-block:: python
318+
319+
if error_condition: # pragma: no mutate block
320+
log_error()
321+
send_alert()
322+
else:
323+
# the else branch is still mutated normally
324+
handle_success()
325+
326+
Both syntax styles are supported:
327+
328+
- ``# pragma: no mutate block``
329+
- ``# pragma: no mutate: block``
330+
331+
This is useful for:
332+
333+
- Error-handling branches that are hard to unit test in isolation
334+
- Logging or telemetry blocks that don't affect program correctness
335+
- Generated or boilerplate code within an otherwise mutable function
336+
337+
271338
Modifying pytest arguments
272339
~~~~~~~~~~~~~~~~~~~~~~~~~~
273340

Lines changed: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Pragma comment parsing for mutation control."""
22

3+
from enum import Enum
4+
from enum import auto
5+
36

47
def parse_pragma_lines(source: str) -> tuple[set[int], set[int], set[int]]:
58
"""Parse all pragma: no mutate variants.
@@ -12,30 +15,80 @@ def parse_pragma_lines(source: str) -> tuple[set[int], set[int], set[int]]:
1215
- ``# pragma: no mutate: class`` - skip entire class (alternative syntax)
1316
- ``# pragma: no mutate function`` - skip entire function
1417
- ``# pragma: no mutate: function`` - skip entire function (alternative syntax)
18+
- ``# pragma: no mutate block`` - skip all lines in the current indentation block
19+
- ``# pragma: no mutate: block`` - skip all lines in the current indentation block (alternative syntax)
20+
21+
Block pragma behaviour depends on placement:
22+
- On its own line (``# pragma: no mutate block``): all subsequent lines at
23+
the **same or deeper** indentation are skipped until a dedent.
24+
- Inline after code (``if cond: # pragma: no mutate block``): all lines
25+
indented **deeper** than the pragma line are skipped.
1526
1627
:return: A tuple of (no_mutate_lines, class_lines, function_lines)
1728
"""
29+
30+
class BlockMode(Enum):
31+
INLINE = auto()
32+
INDENT = auto()
33+
1834
no_mutate_lines: set[int] = set()
1935
class_lines: set[int] = set()
2036
function_lines: set[int] = set()
2137

38+
block_level: int | None = None
39+
block_mode: BlockMode | None = None
40+
2241
for i, line in enumerate(source.split("\n")):
42+
line_num = i + 1
43+
44+
if block_level is not None:
45+
stripped = line.lstrip()
46+
if stripped == "":
47+
no_mutate_lines.add(line_num)
48+
continue
49+
50+
curr = len(line) - len(stripped)
51+
if block_mode == BlockMode.INDENT:
52+
filter = curr >= block_level
53+
elif block_mode == BlockMode.INLINE:
54+
filter = curr > block_level
55+
else:
56+
raise ValueError(f"block_mode cannot be {block_mode}")
57+
58+
if filter:
59+
no_mutate_lines.add(line_num)
60+
continue
61+
else:
62+
block_level = None
63+
block_mode = None
64+
2365
if "# pragma:" not in line:
2466
continue
2567

2668
pragma_content = line.partition("# pragma:")[-1]
27-
line_num = i + 1
2869

2970
if "no mutate" not in pragma_content:
3071
continue
3172

32-
# Check for specific variants first (more specific matches)
33-
if "no mutate class" in pragma_content or "no mutate: class" in pragma_content:
34-
class_lines.add(line_num)
35-
elif "no mutate function" in pragma_content or "no mutate: function" in pragma_content:
36-
function_lines.add(line_num)
37-
else:
38-
# Generic "no mutate" (not class or function)
39-
no_mutate_lines.add(line_num)
73+
pragma_content = line.partition("no mutate")[-1].strip()
74+
tokens = pragma_content.lstrip(": ").split(",", 1)[0].split()
75+
tok = tokens[0] if tokens else None
76+
match tok:
77+
# Check for specific variants first (more specific matches)
78+
case "class":
79+
class_lines.add(line_num)
80+
case "function":
81+
function_lines.add(line_num)
82+
# Generic "no mutate" (not class or function)
83+
case "block":
84+
no_mutate_lines.add(line_num)
85+
stripped = line.lstrip()
86+
block_level = len(line) - len(stripped)
87+
if stripped.startswith("# pragma"):
88+
block_mode = BlockMode.INDENT
89+
else:
90+
block_mode = BlockMode.INLINE
91+
case _:
92+
no_mutate_lines.add(line_num)
4093

4194
return no_mutate_lines, class_lines, function_lines

tests/mutation/test_pragma_handling.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,65 @@ def test_other_pragma_ignored(self):
9595
assert no_mutate == set()
9696
assert class_lines == set()
9797
assert function_lines == set()
98+
99+
def test_block_pragma_own_line(self):
100+
source = "if condition:\n # pragma: no mutate block\n x = 1\n y = 2\nz = 3\n"
101+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
102+
assert no_mutate == {2, 3, 4}
103+
assert class_lines == set()
104+
assert function_lines == set()
105+
106+
def test_block_pragma_own_line_with_colon(self):
107+
source = "if condition:\n # pragma: no mutate: block\n x = 1\n y = 2\nz = 3\n"
108+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
109+
assert no_mutate == {2, 3, 4}
110+
assert class_lines == set()
111+
assert function_lines == set()
112+
113+
def test_block_pragma_inline(self):
114+
source = "if condition: # pragma: no mutate block\n x = 1\n y = 2\nz = 3\n"
115+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
116+
assert no_mutate == {1, 2, 3}
117+
assert class_lines == set()
118+
assert function_lines == set()
119+
120+
def test_block_pragma_does_not_affect_code_after_dedent(self):
121+
source = "def foo():\n # pragma: no mutate block\n x = 1\n y = 2\n\ndef bar():\n z = 3\n"
122+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
123+
assert no_mutate == {2, 3, 4, 5}
124+
assert 6 not in no_mutate
125+
assert 7 not in no_mutate
126+
127+
def test_block_pragma_includes_nested_indentation(self):
128+
source = "def foo():\n # pragma: no mutate block\n if True:\n x = 1\n y = 2\nz = 3\n"
129+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
130+
assert no_mutate == {2, 3, 4, 5}
131+
assert 6 not in no_mutate
132+
133+
def test_block_pragma_inline_only_ignores_deeper(self):
134+
"""Inline block pragma on an if-statement: the else branch at the same
135+
indentation is NOT ignored because it is not deeper."""
136+
source = "if condition: # pragma: no mutate block\n x = 1\n y = 2\nelse:\n z = 3\n"
137+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
138+
assert no_mutate == {1, 2, 3}
139+
assert 4 not in no_mutate
140+
assert 5 not in no_mutate
141+
142+
def test_block_pragma_with_other_pragmas(self):
143+
source = (
144+
"class Skipped: # pragma: no mutate class\n"
145+
" pass\n"
146+
"\n"
147+
"def foo():\n"
148+
" # pragma: no mutate block\n"
149+
" x = 1\n"
150+
" y = 2\n"
151+
"\n"
152+
"def bar():\n"
153+
" z = 3 # pragma: no mutate\n"
154+
)
155+
no_mutate, class_lines, function_lines = parse_pragma_lines(source)
156+
assert class_lines == {1}
157+
assert 5 in no_mutate and 6 in no_mutate and 7 in no_mutate
158+
assert 10 in no_mutate
159+
assert 9 not in no_mutate

0 commit comments

Comments
 (0)