mutmut 3.5.0__tar.gz → 3.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {mutmut-3.5.0 → mutmut-3.6.0}/HISTORY.rst +42 -1
  2. {mutmut-3.5.0 → mutmut-3.6.0}/PKG-INFO +185 -16
  3. {mutmut-3.5.0 → mutmut-3.6.0}/README.rst +184 -15
  4. {mutmut-3.5.0 → mutmut-3.6.0}/pyproject.toml +36 -3
  5. mutmut-3.6.0/src/mutmut/__init__.py +42 -0
  6. mutmut-3.6.0/src/mutmut/__main__.py +1550 -0
  7. {mutmut-3.5.0 → mutmut-3.6.0}/src/mutmut/code_coverage.py +32 -25
  8. mutmut-3.6.0/src/mutmut/configuration.py +207 -0
  9. mutmut-3.6.0/src/mutmut/mutation/__init__.py +0 -0
  10. mutmut-3.6.0/src/mutmut/mutation/data.py +61 -0
  11. mutmut-3.6.0/src/mutmut/mutation/file_mutation.py +508 -0
  12. mutmut-3.5.0/src/mutmut/node_mutation.py → mutmut-3.6.0/src/mutmut/mutation/mutators.py +65 -71
  13. mutmut-3.6.0/src/mutmut/mutation/pragma_handling.py +229 -0
  14. mutmut-3.6.0/src/mutmut/mutation/trampoline.py +105 -0
  15. mutmut-3.6.0/src/mutmut/mutation/trampoline_templates.py +54 -0
  16. mutmut-3.6.0/src/mutmut/py.typed +0 -0
  17. mutmut-3.6.0/src/mutmut/skills/kill-survivors.md +18 -0
  18. mutmut-3.6.0/src/mutmut/threading/__init__.py +0 -0
  19. mutmut-3.6.0/src/mutmut/threading/timeout.py +57 -0
  20. mutmut-3.6.0/src/mutmut/type_checking.py +112 -0
  21. mutmut-3.6.0/src/mutmut/utils/__init__.py +0 -0
  22. mutmut-3.6.0/src/mutmut/utils/file_utils.py +14 -0
  23. mutmut-3.6.0/src/mutmut/utils/format_utils.py +55 -0
  24. mutmut-3.6.0/src/mutmut/utils/safe_setproctitle.py +24 -0
  25. mutmut-3.5.0/src/mutmut/__init__.py +0 -24
  26. mutmut-3.5.0/src/mutmut/__main__.py +0 -1716
  27. mutmut-3.5.0/src/mutmut/file_mutation.py +0 -402
  28. mutmut-3.5.0/src/mutmut/trampoline_templates.py +0 -56
  29. mutmut-3.5.0/src/mutmut/type_checking.py +0 -98
  30. {mutmut-3.5.0 → mutmut-3.6.0}/LICENSE +0 -0
  31. {mutmut-3.5.0 → mutmut-3.6.0}/src/mutmut/result_browser_layout.tcss +0 -0
@@ -1,12 +1,53 @@
1
1
  Changelog
2
2
  ---------
3
3
 
4
+ 3.6.0
5
+ ~~~~~
6
+
7
+ * Add `# pragma: no mutate block` and `# pragma: no mutate start/end` comments
8
+
9
+ * Add `do_not_mutate_patterns` to disable mutations with a regex
10
+
11
+ * Add `only_mutate` config to select which files get mutated
12
+
13
+ * Also mutate methods decorated with (only) `@staticmethod` or `@classmethod`
14
+
15
+ * Add `use_setproctitle` option to disable process renaming (automatically disabled on MacOS)
16
+
17
+ * Add (unstable) `timeout_multiplier` and `timeout_constant` options
18
+
19
+ * Rename `paths_to_mutate` to `source_paths`
20
+
21
+ * Deprecate `tests_dir` in favor of `pytest_add_cli_args_test_selection`
22
+
23
+ * Change `max_stack_depth` to only consider functions inside `source_paths`
24
+
25
+ * Disable mutation of enums and `@staticmethod`/`@classmethod` methods when `type_check_command` is set, as these mutations break type checking
26
+
27
+ * Fix mutation for enum class methods
28
+
29
+ * Fix mutate_only_covered_lines when project uses custom coverage.py config
30
+
31
+ * Fix execution when running mutmut via `python -m mutmut run`
32
+
33
+ * Fix mutation of `class _SomePrivateClass` class methods
34
+
35
+ * Fix mutation of default args
36
+
37
+ * Fix `mutmut browse` crash when no file is selected yet
38
+
39
+ * Fix timeout checker looking up the wrong mutant's expected test time, which could cause mutants to hang
40
+
41
+ * Compare stats paths after resolving symlinks
42
+
43
+ * Warn when mutmut cannot match mutants with the collected stats
44
+
4
45
  3.5.0
5
46
  ~~~~~
6
47
 
7
48
  * Filter mutants based on type annotations. This feature needs to be turned on with the `type_check_command` config.
8
49
 
9
- * Support running on Windows
50
+ * Show error message when running on Windows
10
51
 
11
52
  * Save mutation stats for use with CI/CD pipeline (#460)
12
53
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mutmut
3
- Version: 3.5.0
3
+ Version: 3.6.0
4
4
  Summary: mutation testing for Python 3
5
5
  Keywords: mutmut,mutant,mutation,test,testing
6
6
  Author: Anders Hovmöller
@@ -86,8 +86,7 @@ it will try to figure out where the code to mutate is.
86
86
 
87
87
 
88
88
  You can stop the mutation run at any time and mutmut will restart where you
89
- left off. It will continue where it left off, and re-test functions that were
90
- modified since last run.
89
+ left off.
91
90
 
92
91
  To work with the results, use `mutmut browse` where you can see the mutants,
93
92
  retest them when you've updated your tests.
@@ -122,7 +121,7 @@ In `setup.cfg` in the root of your project you can configure mutmut if you need
122
121
  .. code-block:: ini
123
122
 
124
123
  [mutmut]
125
- paths_to_mutate=src/
124
+ source_paths=src/
126
125
  pytest_add_cli_args_test_selection=tests/
127
126
 
128
127
  If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutmut` section:
@@ -130,7 +129,7 @@ If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutm
130
129
  .. code-block:: toml
131
130
 
132
131
  [tool.mutmut]
133
- paths_to_mutate = [ "src/" ]
132
+ source_paths = [ "src/" ]
134
133
  pytest_add_cli_args_test_selection= [ "tests/" ]
135
134
 
136
135
  See below for more options for configuring mutmut.
@@ -162,8 +161,8 @@ introduced bug in those base functions will lead to many tests that fail which
162
161
  are hard to understand how they relate to the function with the change.
163
162
 
164
163
  You can configure mutmut to only count a test as being relevant for a function
165
- if the stack depth from the test to the function is below some limit. In your
166
- `setup.cfg` add:
164
+ if the stack depth is below some limit. Only stack frames from code inside `source_paths`
165
+ is counted towards the limit, 3rd party libraries are ignored. In your `setup.cfg` add:
167
166
 
168
167
  .. code-block:: ini
169
168
 
@@ -177,10 +176,14 @@ caught.
177
176
  Exclude files from mutation
178
177
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~
179
178
 
179
+ By default mutmut mutates all python files in `source_paths`.
180
180
  You can exclude files from mutation in `setup.cfg`:
181
181
 
182
182
  .. code-block::
183
183
 
184
+ only_mutate=
185
+ src/api/*
186
+ src/services/*
184
187
  do_not_mutate=
185
188
  *__tests.py
186
189
 
@@ -221,7 +224,7 @@ To enable this filtering, configure the `type_check_command` to output json resu
221
224
  # for pyrefly
222
225
  type_check_command = ['pyrefly', 'check', '--output-format=json']
223
226
  # for mypy
224
- type_check_command = ['mypy', 'traces_parser', '--output', 'json']
227
+ type_check_command = ['mypy', 'your_source_dir', '--output', 'json', '--disable-error-code', 'unused-ignore']
225
228
 
226
229
  Currently, only `pyrefly` and `mypy` are supported.
227
230
  With `pyright` and `ty`, mutating a class method `Foo.bar()` can break the types of all methods of `Foo`,
@@ -242,22 +245,174 @@ to failing tests.
242
245
  debug=true
243
246
 
244
247
 
245
- Whitelisting
246
- ~~~~~~~~~~~~
248
+ Disable setproctitle (macOS)
249
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
247
250
 
248
- You can mark lines like this:
251
+ Mutmut uses ``setproctitle`` to show the current mutant name in the process
252
+ list, which is helpful for monitoring long runs. However, ``setproctitle``
253
+ uses CoreFoundation APIs on macOS that are not fork-safe, causing segfaults
254
+ in child processes.
249
255
 
250
- .. code-block:: python
256
+ By default, mutmut automatically disables ``setproctitle`` on macOS and
257
+ enables it on other platforms. If you need to override this (e.g. to enable it on
258
+ macOS at your own risk, or to disable it on other platforms), set ``use_setproctitle``:
259
+
260
+ .. code-block:: toml
261
+
262
+ # pyproject.toml
263
+ [tool.mutmut]
264
+ use_setproctitle = false
251
265
 
252
- some_code_here() # pragma: no mutate
253
266
 
254
- to stop mutation on those lines. Some cases we've found where you need to
255
- whitelist lines are:
267
+ Disabling mutation on specific code
268
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
256
269
 
257
- - The version string on your library. You really shouldn't have a test for this :P
270
+ If you do not want to mutate specific parts of your code, you can disable mutation
271
+ via the following options. Examples where this could be relevant:
272
+
273
+ - If you don't want to test exact log messages (e.g. `logging.info("Foo")` versus the mutated `logging.info("XXFooXX")`)
258
274
  - Optimizing break instead of continue. The code runs fine when mutating break
259
275
  to continue, but it's slower.
260
276
 
277
+ Skipping via regex
278
+ ^^^^^^^^^^^^^^^^^^
279
+
280
+ You can use a regex on the source code, to tell mutmut which expressions it should not mutate:
281
+
282
+ .. code-block:: toml
283
+
284
+ # pyproject.toml
285
+ [tool.mutmut]
286
+ do_not_mutate_patterns = [
287
+ # disable mutations of all logger.info/debug/... statements
288
+ 'logger\.\w+',
289
+ # disable mutating exceptions
290
+ 'raise \w+',
291
+ ]
292
+
293
+ Mutmut will match the regex on the source code
294
+ and skip mutating any expression on the matched lines.
295
+
296
+ Skipping via code comments
297
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
298
+
299
+ You can use following comments to disable mutation:
300
+
301
+ - `# pragma: no mutate`: disable mutation on this single line
302
+ - `# pragma: no mutate block`: disable mutation in the whole intendation block
303
+ - `# pragma: no mutate start/end`: disable mutation between the start and end comments
304
+
305
+ Skipping single lines
306
+ ^^^^^^^^^^^^^^^^^^^^^
307
+
308
+ You can mark lines like this to stop mutating them:
309
+
310
+ .. code-block:: python
311
+
312
+ some_code_here() # pragma: no mutate
313
+
314
+
315
+ Skipping Code Blocks
316
+ ^^^^^^^^^^^^^^^^^^^^
317
+
318
+ You can skip an entire indentation block from mutation using
319
+ ``# pragma: no mutate block``. This works on any compound statement --
320
+ functions, classes, ``if``/``elif``/``else``, loops, context managers, etc.
321
+
322
+ Both syntax styles are supported:
323
+
324
+ - ``# pragma: no mutate block``
325
+ - ``# pragma: no mutate: block``
326
+
327
+ **Skipping an entire function or class** -- place the pragma inline on the
328
+ definition line. The entire node (including all children) is skipped and no
329
+ trampoline is generated:
330
+
331
+ .. code-block:: python
332
+
333
+ def complex_algorithm(): # pragma: no mutate block
334
+ return some_complex_calculation()
335
+
336
+ class MySettings: # pragma: no mutate block
337
+ DEBUG = True
338
+ MAX_RETRIES = 3
339
+
340
+ **Skipping only the body of a function** -- place the pragma on its own line
341
+ inside the function. The function definition (including default arguments) is
342
+ still mutable, but the body is suppressed:
343
+
344
+ .. code-block:: python
345
+
346
+ def foo(val=1):
347
+ # pragma: no mutate block
348
+ x = 1
349
+ y = complex_calculation()
350
+ z = x + y
351
+
352
+ def bar():
353
+ # this function is still mutated normally
354
+ return 42
355
+
356
+ **Skipping an ``if`` branch without affecting ``elif``/``else``** -- place
357
+ the pragma inline on the ``if``. Only the ``if`` condition and its indented
358
+ body are suppressed; sibling branches (``elif``, ``else``) remain mutable
359
+ because they exit the original indentation scope:
360
+
361
+ .. code-block:: python
362
+
363
+ if error_condition: # pragma: no mutate block
364
+ log_error()
365
+ send_alert()
366
+ elif other_condition:
367
+ # still mutated -- this branch is outside the block scope
368
+ handle_other()
369
+ else:
370
+ # still mutated
371
+ handle_success()
372
+
373
+ The same principle applies to ``for``/``else``, ``while``/``else``,
374
+ ``try``/``except``/``finally``, and ``match``/``case``.
375
+
376
+ This is useful for:
377
+
378
+ - Functions or classes that should be excluded from mutation entirely
379
+ - Error-handling branches that are hard to unit test in isolation
380
+ - Logging or telemetry blocks that don't affect program correctness
381
+ - Generated or boilerplate code within an otherwise mutable function
382
+
383
+
384
+ Skipping Code Regions
385
+ ^^^^^^^^^^^^^^^^^^^^^
386
+
387
+ For suppressing mutations across a range of lines regardless of indentation,
388
+ use ``# pragma: no mutate start`` and ``# pragma: no mutate end``:
389
+
390
+ .. code-block:: python
391
+
392
+ a = mutate_this()
393
+
394
+ # pragma: no mutate start
395
+ b = skip_this()
396
+ c = skip_this_too()
397
+ # pragma: no mutate end
398
+
399
+ d = mutate_this_too()
400
+
401
+ Every line between the markers (inclusive) is suppressed. This works inside
402
+ functions, classes, or at module level, and ignores indentation entirely.
403
+
404
+ An unmatched ``# pragma: no mutate end`` without a preceding ``start`` raises
405
+ a ``PragmaParseError`` at parse time. An unclosed ``# pragma: no mutate start``
406
+ (no matching ``end`` before end-of-file) raises a ``PragmaParseError``.
407
+ Both errors include the filename and line number.
408
+
409
+ **Nesting restriction:** opening a new ``block`` or ``start`` context while
410
+ another context is already active is not allowed and raises a
411
+ ``PragmaParseError``. The error message includes both the offending line and
412
+ the line where the existing context was opened. Close the current context
413
+ first (dedent for ``block``, or ``# pragma: no mutate end`` for ``start``)
414
+ before opening a new one.
415
+
261
416
 
262
417
  Modifying pytest arguments
263
418
  ~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -279,6 +434,20 @@ You can add and override pytest arguments:
279
434
  also_copy = ["mutmut_pytest.ini"]
280
435
 
281
436
 
437
+ Unstable configs
438
+ ~~~~~~~~~~~~~~~~
439
+
440
+ Following configurations exist, but may be changed in any minor version.
441
+ If you use them, expect that a new version could change or break this feature.
442
+
443
+
444
+ .. code-block:: toml
445
+
446
+ # Configure how long mutmut waits before killing a slow mutation
447
+ # Currently calculated as (duration_of_original_tests + timeout_constant) * timeout_multiplier seconds
448
+ timeout_constant = 1.0
449
+ timeout_multiplier = 15.0
450
+
282
451
 
283
452
  Example mutations
284
453
  -----------------
@@ -53,8 +53,7 @@ it will try to figure out where the code to mutate is.
53
53
 
54
54
 
55
55
  You can stop the mutation run at any time and mutmut will restart where you
56
- left off. It will continue where it left off, and re-test functions that were
57
- modified since last run.
56
+ left off.
58
57
 
59
58
  To work with the results, use `mutmut browse` where you can see the mutants,
60
59
  retest them when you've updated your tests.
@@ -89,7 +88,7 @@ In `setup.cfg` in the root of your project you can configure mutmut if you need
89
88
  .. code-block:: ini
90
89
 
91
90
  [mutmut]
92
- paths_to_mutate=src/
91
+ source_paths=src/
93
92
  pytest_add_cli_args_test_selection=tests/
94
93
 
95
94
  If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutmut` section:
@@ -97,7 +96,7 @@ If you use `pyproject.toml`, you must specify the paths as array in a `tool.mutm
97
96
  .. code-block:: toml
98
97
 
99
98
  [tool.mutmut]
100
- paths_to_mutate = [ "src/" ]
99
+ source_paths = [ "src/" ]
101
100
  pytest_add_cli_args_test_selection= [ "tests/" ]
102
101
 
103
102
  See below for more options for configuring mutmut.
@@ -129,8 +128,8 @@ introduced bug in those base functions will lead to many tests that fail which
129
128
  are hard to understand how they relate to the function with the change.
130
129
 
131
130
  You can configure mutmut to only count a test as being relevant for a function
132
- if the stack depth from the test to the function is below some limit. In your
133
- `setup.cfg` add:
131
+ if the stack depth is below some limit. Only stack frames from code inside `source_paths`
132
+ is counted towards the limit, 3rd party libraries are ignored. In your `setup.cfg` add:
134
133
 
135
134
  .. code-block:: ini
136
135
 
@@ -144,10 +143,14 @@ caught.
144
143
  Exclude files from mutation
145
144
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~
146
145
 
146
+ By default mutmut mutates all python files in `source_paths`.
147
147
  You can exclude files from mutation in `setup.cfg`:
148
148
 
149
149
  .. code-block::
150
150
 
151
+ only_mutate=
152
+ src/api/*
153
+ src/services/*
151
154
  do_not_mutate=
152
155
  *__tests.py
153
156
 
@@ -188,7 +191,7 @@ To enable this filtering, configure the `type_check_command` to output json resu
188
191
  # for pyrefly
189
192
  type_check_command = ['pyrefly', 'check', '--output-format=json']
190
193
  # for mypy
191
- type_check_command = ['mypy', 'traces_parser', '--output', 'json']
194
+ type_check_command = ['mypy', 'your_source_dir', '--output', 'json', '--disable-error-code', 'unused-ignore']
192
195
 
193
196
  Currently, only `pyrefly` and `mypy` are supported.
194
197
  With `pyright` and `ty`, mutating a class method `Foo.bar()` can break the types of all methods of `Foo`,
@@ -209,22 +212,174 @@ to failing tests.
209
212
  debug=true
210
213
 
211
214
 
212
- Whitelisting
213
- ~~~~~~~~~~~~
215
+ Disable setproctitle (macOS)
216
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
214
217
 
215
- You can mark lines like this:
218
+ Mutmut uses ``setproctitle`` to show the current mutant name in the process
219
+ list, which is helpful for monitoring long runs. However, ``setproctitle``
220
+ uses CoreFoundation APIs on macOS that are not fork-safe, causing segfaults
221
+ in child processes.
216
222
 
217
- .. code-block:: python
223
+ By default, mutmut automatically disables ``setproctitle`` on macOS and
224
+ enables it on other platforms. If you need to override this (e.g. to enable it on
225
+ macOS at your own risk, or to disable it on other platforms), set ``use_setproctitle``:
226
+
227
+ .. code-block:: toml
228
+
229
+ # pyproject.toml
230
+ [tool.mutmut]
231
+ use_setproctitle = false
218
232
 
219
- some_code_here() # pragma: no mutate
220
233
 
221
- to stop mutation on those lines. Some cases we've found where you need to
222
- whitelist lines are:
234
+ Disabling mutation on specific code
235
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
223
236
 
224
- - The version string on your library. You really shouldn't have a test for this :P
237
+ If you do not want to mutate specific parts of your code, you can disable mutation
238
+ via the following options. Examples where this could be relevant:
239
+
240
+ - If you don't want to test exact log messages (e.g. `logging.info("Foo")` versus the mutated `logging.info("XXFooXX")`)
225
241
  - Optimizing break instead of continue. The code runs fine when mutating break
226
242
  to continue, but it's slower.
227
243
 
244
+ Skipping via regex
245
+ ^^^^^^^^^^^^^^^^^^
246
+
247
+ You can use a regex on the source code, to tell mutmut which expressions it should not mutate:
248
+
249
+ .. code-block:: toml
250
+
251
+ # pyproject.toml
252
+ [tool.mutmut]
253
+ do_not_mutate_patterns = [
254
+ # disable mutations of all logger.info/debug/... statements
255
+ 'logger\.\w+',
256
+ # disable mutating exceptions
257
+ 'raise \w+',
258
+ ]
259
+
260
+ Mutmut will match the regex on the source code
261
+ and skip mutating any expression on the matched lines.
262
+
263
+ Skipping via code comments
264
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^
265
+
266
+ You can use following comments to disable mutation:
267
+
268
+ - `# pragma: no mutate`: disable mutation on this single line
269
+ - `# pragma: no mutate block`: disable mutation in the whole intendation block
270
+ - `# pragma: no mutate start/end`: disable mutation between the start and end comments
271
+
272
+ Skipping single lines
273
+ ^^^^^^^^^^^^^^^^^^^^^
274
+
275
+ You can mark lines like this to stop mutating them:
276
+
277
+ .. code-block:: python
278
+
279
+ some_code_here() # pragma: no mutate
280
+
281
+
282
+ Skipping Code Blocks
283
+ ^^^^^^^^^^^^^^^^^^^^
284
+
285
+ You can skip an entire indentation block from mutation using
286
+ ``# pragma: no mutate block``. This works on any compound statement --
287
+ functions, classes, ``if``/``elif``/``else``, loops, context managers, etc.
288
+
289
+ Both syntax styles are supported:
290
+
291
+ - ``# pragma: no mutate block``
292
+ - ``# pragma: no mutate: block``
293
+
294
+ **Skipping an entire function or class** -- place the pragma inline on the
295
+ definition line. The entire node (including all children) is skipped and no
296
+ trampoline is generated:
297
+
298
+ .. code-block:: python
299
+
300
+ def complex_algorithm(): # pragma: no mutate block
301
+ return some_complex_calculation()
302
+
303
+ class MySettings: # pragma: no mutate block
304
+ DEBUG = True
305
+ MAX_RETRIES = 3
306
+
307
+ **Skipping only the body of a function** -- place the pragma on its own line
308
+ inside the function. The function definition (including default arguments) is
309
+ still mutable, but the body is suppressed:
310
+
311
+ .. code-block:: python
312
+
313
+ def foo(val=1):
314
+ # pragma: no mutate block
315
+ x = 1
316
+ y = complex_calculation()
317
+ z = x + y
318
+
319
+ def bar():
320
+ # this function is still mutated normally
321
+ return 42
322
+
323
+ **Skipping an ``if`` branch without affecting ``elif``/``else``** -- place
324
+ the pragma inline on the ``if``. Only the ``if`` condition and its indented
325
+ body are suppressed; sibling branches (``elif``, ``else``) remain mutable
326
+ because they exit the original indentation scope:
327
+
328
+ .. code-block:: python
329
+
330
+ if error_condition: # pragma: no mutate block
331
+ log_error()
332
+ send_alert()
333
+ elif other_condition:
334
+ # still mutated -- this branch is outside the block scope
335
+ handle_other()
336
+ else:
337
+ # still mutated
338
+ handle_success()
339
+
340
+ The same principle applies to ``for``/``else``, ``while``/``else``,
341
+ ``try``/``except``/``finally``, and ``match``/``case``.
342
+
343
+ This is useful for:
344
+
345
+ - Functions or classes that should be excluded from mutation entirely
346
+ - Error-handling branches that are hard to unit test in isolation
347
+ - Logging or telemetry blocks that don't affect program correctness
348
+ - Generated or boilerplate code within an otherwise mutable function
349
+
350
+
351
+ Skipping Code Regions
352
+ ^^^^^^^^^^^^^^^^^^^^^
353
+
354
+ For suppressing mutations across a range of lines regardless of indentation,
355
+ use ``# pragma: no mutate start`` and ``# pragma: no mutate end``:
356
+
357
+ .. code-block:: python
358
+
359
+ a = mutate_this()
360
+
361
+ # pragma: no mutate start
362
+ b = skip_this()
363
+ c = skip_this_too()
364
+ # pragma: no mutate end
365
+
366
+ d = mutate_this_too()
367
+
368
+ Every line between the markers (inclusive) is suppressed. This works inside
369
+ functions, classes, or at module level, and ignores indentation entirely.
370
+
371
+ An unmatched ``# pragma: no mutate end`` without a preceding ``start`` raises
372
+ a ``PragmaParseError`` at parse time. An unclosed ``# pragma: no mutate start``
373
+ (no matching ``end`` before end-of-file) raises a ``PragmaParseError``.
374
+ Both errors include the filename and line number.
375
+
376
+ **Nesting restriction:** opening a new ``block`` or ``start`` context while
377
+ another context is already active is not allowed and raises a
378
+ ``PragmaParseError``. The error message includes both the offending line and
379
+ the line where the existing context was opened. Close the current context
380
+ first (dedent for ``block``, or ``# pragma: no mutate end`` for ``start``)
381
+ before opening a new one.
382
+
228
383
 
229
384
  Modifying pytest arguments
230
385
  ~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -246,6 +401,20 @@ You can add and override pytest arguments:
246
401
  also_copy = ["mutmut_pytest.ini"]
247
402
 
248
403
 
404
+ Unstable configs
405
+ ~~~~~~~~~~~~~~~~
406
+
407
+ Following configurations exist, but may be changed in any minor version.
408
+ If you use them, expect that a new version could change or break this feature.
409
+
410
+
411
+ .. code-block:: toml
412
+
413
+ # Configure how long mutmut waits before killing a slow mutation
414
+ # Currently calculated as (duration_of_original_tests + timeout_constant) * timeout_multiplier seconds
415
+ timeout_constant = 1.0
416
+ timeout_multiplier = 15.0
417
+
249
418
 
250
419
  Example mutations
251
420
  -----------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mutmut"
3
- version = "3.5.0"
3
+ version = "3.6.0"
4
4
  description = "mutation testing for Python 3"
5
5
  keywords = ["mutmut", "mutant", "mutation", "test", "testing"]
6
6
  authors = [
@@ -54,9 +54,11 @@ source-include = ["HISTORY.rst"]
54
54
  [dependency-groups]
55
55
  dev = [
56
56
  "inline-snapshot>=0.32.0",
57
- "pyrefly>=0.53.0",
57
+ "mypy>=1.20.1",
58
+ "pre-commit>=4.5.1",
59
+ "pyrefly>=0.59.1",
58
60
  "pytest-asyncio>=1.0.0",
59
- "ruff>=0.15.1",
61
+ "ruff>=0.15.7",
60
62
  ]
61
63
 
62
64
  [tool.pytest.ini_options]
@@ -67,3 +69,34 @@ asyncio_default_fixture_loop_scope = "function"
67
69
 
68
70
  [tool.inline-snapshot]
69
71
  format-command="ruff format --stdin-filename {filename}"
72
+
73
+ [tool.ruff]
74
+ line-length = 120
75
+ target-version = "py310"
76
+ extend-exclude = [
77
+ "docs/_themes/",
78
+ "docs/conf.py",
79
+ "e2e_projects/",
80
+ "tests/data/test_generation/",
81
+ ]
82
+
83
+ [tool.ruff.lint]
84
+ select = ["E", "F", "W", "B", "C4", "UP", "I"]
85
+ ignore = [
86
+ "E501", # line too long - handled by formatter
87
+ "B007", # unused loop control variable
88
+ "B018", # useless expression
89
+ "B024", # abstract base class without abstract methods
90
+ "B027", # empty method in abstract base class without abstract decorator
91
+ "B028", # no explicit stacklevel in warnings.warn
92
+ "B904", # raise without from in except
93
+ "C405", # unnecessary list literal
94
+ "C408", # unnecessary dict() call
95
+ "C416", # unnecessary list comprehension
96
+ "F402", # import shadowed by loop variable
97
+ "F841", # local variable assigned but never used
98
+ "UP007", # use X | Y for type annotations
99
+ ]
100
+
101
+ [tool.ruff.lint.isort]
102
+ force-single-line = true
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.metadata
4
+ import warnings
5
+ from collections import defaultdict
6
+
7
+ from mutmut.configuration import Config
8
+
9
+ __version__ = importlib.metadata.version("mutmut")
10
+
11
+
12
+ stats_time: float | None = None
13
+ duration_by_test: dict[str, float] = defaultdict(float)
14
+ tests_by_mangled_function_name: dict[str, set[str]] = defaultdict(set)
15
+
16
+ _stats: set[str] = set()
17
+ _covered_lines: dict[str, set[int]] | None = None
18
+
19
+
20
+ def __getattr__(name: str) -> object:
21
+ match name:
22
+ case "config":
23
+ warnings.warn(
24
+ "mutmut.config is deprecated as of 3.4.1, use mutmut.configuration.Config.get() instead",
25
+ FutureWarning,
26
+ stacklevel=2,
27
+ )
28
+ return Config.get()
29
+ case _:
30
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
31
+
32
+
33
+ def _reset_globals() -> None:
34
+ global duration_by_test, stats_time, _stats, tests_by_mangled_function_name
35
+ global _covered_lines
36
+
37
+ duration_by_test.clear()
38
+ stats_time = None
39
+ Config.reset()
40
+ _stats = set()
41
+ tests_by_mangled_function_name = defaultdict(set)
42
+ _covered_lines = None