mutmut 3.8.0__tar.gz → 3.8.1__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.
- {mutmut-3.8.0 → mutmut-3.8.1}/HISTORY.rst +15 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/PKG-INFO +2 -2
- {mutmut-3.8.0 → mutmut-3.8.1}/README.rst +1 -1
- {mutmut-3.8.0 → mutmut-3.8.1}/pyproject.toml +1 -1
- {mutmut-3.8.0 → mutmut-3.8.1}/pyproject.toml.orig +1 -1
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/file_mutation.py +17 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/mutators.py +45 -7
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/pragma_handling.py +9 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/runners/harness.py +1 -1
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/type_checking.py +2 -1
- {mutmut-3.8.0 → mutmut-3.8.1}/LICENSE +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/__main__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/code_coverage.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/configuration.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/core.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/models/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/models/results.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/data.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/diff_apply.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/trampoline.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/trampoline_templates.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/py.typed +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/runners/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/state.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/stats.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/browse.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/result_browser_layout.tcss +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/terminal.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/file_utils.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/format_utils.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/logging_utils.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/safe_setproctitle.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/workers/__init__.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/workers/isolation.py +0 -0
- {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/workers/timeout.py +0 -0
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
Changelog
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
|
+
3.8.1
|
|
5
|
+
~~~~~
|
|
6
|
+
|
|
7
|
+
* Fix stats collection crashing with pytest 9.2 (`#615`, `#616`)
|
|
8
|
+
|
|
9
|
+
* Fix ``# pragma: no mutate`` being ignored inside multi-line statements (`#614`)
|
|
10
|
+
|
|
11
|
+
* Don't produce equivalent case mutants inside string escape sequences like `` `` and ``\N{...}`` (`#609`, `#611`)
|
|
12
|
+
|
|
13
|
+
* Fix enums with a custom ``__init__`` failing to import under mutmut (`#600`, `#603`)
|
|
14
|
+
|
|
15
|
+
* Fix crash when mutating float literals too large for a float, like ``1e999`` (`#604`, `#605`)
|
|
16
|
+
|
|
17
|
+
* Fix crash when mypy reports no errors (`#598`, `#599`)
|
|
18
|
+
|
|
4
19
|
3.8.0
|
|
5
20
|
~~~~~~~~~~
|
|
6
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mutmut
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.1
|
|
4
4
|
Summary: mutation testing for Python 3
|
|
5
5
|
Keywords: mutmut,mutant,mutation,test,testing
|
|
6
6
|
Author: Anders Hovmöller
|
|
@@ -407,7 +407,7 @@ Skipping via code comments
|
|
|
407
407
|
You can use following comments to disable mutation:
|
|
408
408
|
|
|
409
409
|
- `# pragma: no mutate`: disable mutation on this single line
|
|
410
|
-
- `# pragma: no mutate block`: disable mutation in the whole
|
|
410
|
+
- `# pragma: no mutate block`: disable mutation in the whole indentation block
|
|
411
411
|
- `# pragma: no mutate start/end`: disable mutation between the start and end comments
|
|
412
412
|
|
|
413
413
|
Skipping single lines
|
|
@@ -373,7 +373,7 @@ Skipping via code comments
|
|
|
373
373
|
You can use following comments to disable mutation:
|
|
374
374
|
|
|
375
375
|
- `# pragma: no mutate`: disable mutation on this single line
|
|
376
|
-
- `# pragma: no mutate block`: disable mutation in the whole
|
|
376
|
+
- `# pragma: no mutate block`: disable mutation in the whole indentation block
|
|
377
377
|
- `# pragma: no mutate start/end`: disable mutation between the start and end comments
|
|
378
378
|
|
|
379
379
|
Skipping single lines
|
|
@@ -35,6 +35,8 @@ from mutmut.utils.format_utils import get_mutant_name
|
|
|
35
35
|
from mutmut.utils.format_utils import is_mutated_method_name
|
|
36
36
|
|
|
37
37
|
NEVER_MUTATE_FUNCTION_NAMES = {"__getattribute__", "__setattr__", "__new__"}
|
|
38
|
+
NEVER_MUTATE_ENUM_FUNCTION_NAMES = {"__init__", "_generate_next_value_"}
|
|
39
|
+
ENUM_BASE_NAMES = {"Enum", "IntEnum", "StrEnum", "Flag", "IntFlag"}
|
|
38
40
|
NEVER_MUTATE_FUNCTION_CALLS = {"len", "isinstance"}
|
|
39
41
|
|
|
40
42
|
|
|
@@ -217,6 +219,7 @@ class MutationVisitor(cst.CSTVisitor):
|
|
|
217
219
|
)
|
|
218
220
|
self.ignored_classes: set[str] = set()
|
|
219
221
|
self.ignored_functions: set[str] = set()
|
|
222
|
+
self._enum_creation_hooks: set[cst.FunctionDef] = set()
|
|
220
223
|
|
|
221
224
|
def on_visit(self, node: cst.CSTNode) -> bool:
|
|
222
225
|
if self._skip_node_and_children(node):
|
|
@@ -300,6 +303,15 @@ class MutationVisitor(cst.CSTVisitor):
|
|
|
300
303
|
if isinstance(node, cst.BaseExpression):
|
|
301
304
|
return True
|
|
302
305
|
|
|
306
|
+
if isinstance(node, cst.ClassDef) and _is_enum_class(node):
|
|
307
|
+
self._enum_creation_hooks.update(
|
|
308
|
+
child
|
|
309
|
+
for child in node.body.body
|
|
310
|
+
if isinstance(child, cst.FunctionDef) and child.name.value in NEVER_MUTATE_ENUM_FUNCTION_NAMES
|
|
311
|
+
)
|
|
312
|
+
if node in self._enum_creation_hooks:
|
|
313
|
+
return True
|
|
314
|
+
|
|
303
315
|
if (
|
|
304
316
|
isinstance(node, cst.Call)
|
|
305
317
|
and isinstance(node.func, cst.Name)
|
|
@@ -344,6 +356,11 @@ class MutationVisitor(cst.CSTVisitor):
|
|
|
344
356
|
return False
|
|
345
357
|
|
|
346
358
|
|
|
359
|
+
def _is_enum_class(node: cst.ClassDef) -> bool:
|
|
360
|
+
bases = (base.value.attr if isinstance(base.value, cst.Attribute) else base.value for base in node.bases)
|
|
361
|
+
return any(isinstance(base, cst.Name) and base.value in ENUM_BASE_NAMES for base in bases)
|
|
362
|
+
|
|
363
|
+
|
|
347
364
|
def _walk(node: cst.CSTNode) -> Iterator[cst.CSTNode]:
|
|
348
365
|
"""Yield `node` and every node below it."""
|
|
349
366
|
stack = [node]
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""This module contains the mutations for indidvidual nodes, e.g. replacing a != b with a == b."""
|
|
2
2
|
|
|
3
|
+
import cmath
|
|
4
|
+
import math
|
|
3
5
|
import re
|
|
4
6
|
from collections.abc import Callable
|
|
5
7
|
from collections.abc import Iterable
|
|
@@ -17,15 +19,33 @@ OPERATORS_TYPE = Sequence[
|
|
|
17
19
|
]
|
|
18
20
|
]
|
|
19
21
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
+
# Pattern matching either: (1) escape sequences with case-sensitive parts (preserved as group None),
|
|
23
|
+
# or (2) runs of non-backslash characters (captured as group 1 for case transformation).
|
|
24
|
+
# For non-raw strings, preserve \xHH, \uHHHH, \UHHHHHHHH, \N{NAME} to avoid equivalent mutants.
|
|
25
|
+
NON_ESCAPE_SEQUENCE = re.compile(
|
|
26
|
+
r"\\x[0-9a-fA-F]{2}" # \xHH
|
|
27
|
+
r"|\\u[0-9a-fA-F]{4}" # \uHHHH
|
|
28
|
+
r"|\\U[0-9a-fA-F]{8}" # \UHHHHHHHH
|
|
29
|
+
r"|\\N\{[^}]+\}" # \N{NAME}
|
|
30
|
+
r"|(\\.)" # other escapes like \\, \n, \t (group 1, but treat as escape)
|
|
31
|
+
r"|((?<!\\)[^\\]+)" # OR non-backslash run (group 2, to be transformed)
|
|
32
|
+
)
|
|
22
33
|
|
|
23
34
|
|
|
24
35
|
def operator_number(node: cst.BaseNumber) -> Iterable[cst.BaseNumber]:
|
|
36
|
+
# A float literal too large for a float, like 1e999 (a common way to spell
|
|
37
|
+
# infinity), evaluates to inf. inf + 1 is still inf, which has no literal
|
|
38
|
+
# form, so there is no number mutant to make.
|
|
25
39
|
if isinstance(node, cst.Integer | cst.Float):
|
|
26
|
-
|
|
40
|
+
value = node.evaluated_value + 1
|
|
41
|
+
if isinstance(value, float) and math.isinf(value):
|
|
42
|
+
return
|
|
43
|
+
yield node.with_changes(value=repr(value))
|
|
27
44
|
elif isinstance(node, cst.Imaginary):
|
|
28
|
-
|
|
45
|
+
complex_value = node.evaluated_value + 1j
|
|
46
|
+
if cmath.isinf(complex_value):
|
|
47
|
+
return
|
|
48
|
+
yield node.with_changes(value=repr(complex_value))
|
|
29
49
|
else:
|
|
30
50
|
print("Unexpected number type", node)
|
|
31
51
|
|
|
@@ -42,13 +62,31 @@ def operator_string(node: cst.BaseString) -> Iterable[cst.BaseString]:
|
|
|
42
62
|
# that mutation is meaningless for
|
|
43
63
|
return
|
|
44
64
|
|
|
65
|
+
# For raw strings (r"..." or R"..."), escape sequences are literal, so case changes are NOT equivalent
|
|
66
|
+
is_raw = "r" in prefix.lower()
|
|
67
|
+
|
|
45
68
|
supported_str_mutations: list[Callable[[str], str]] = [
|
|
46
69
|
lambda x: "XX" + x + "XX",
|
|
47
|
-
# do not modify escape sequences, as this could break python syntax
|
|
48
|
-
lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).lower(), x),
|
|
49
|
-
lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: match.group(1).upper(), x),
|
|
50
70
|
]
|
|
51
71
|
|
|
72
|
+
if is_raw:
|
|
73
|
+
# Raw strings: use original regex (all non-backslash chars)
|
|
74
|
+
old_pattern = re.compile(r"((?<!\\)[^\\]+)")
|
|
75
|
+
supported_str_mutations.extend(
|
|
76
|
+
[
|
|
77
|
+
lambda x: old_pattern.sub(lambda m: m.group(1).lower(), x),
|
|
78
|
+
lambda x: old_pattern.sub(lambda m: m.group(1).upper(), x),
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
# Regular strings: preserve escape sequences with case-sensitive hex/names
|
|
83
|
+
supported_str_mutations.extend(
|
|
84
|
+
[
|
|
85
|
+
lambda x: NON_ESCAPE_SEQUENCE.sub(lambda m: m.group(2).lower() if m.group(2) else m.group(0), x),
|
|
86
|
+
lambda x: NON_ESCAPE_SEQUENCE.sub(lambda m: m.group(2).upper() if m.group(2) else m.group(0), x),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
|
|
52
90
|
for mut_func in supported_str_mutations:
|
|
53
91
|
new_value = f"{prefix}{value[0]}{mut_func(value[1:-1])}{value[-1]}"
|
|
54
92
|
if new_value == value:
|
|
@@ -217,6 +217,15 @@ class PragmaVisitor(cst.CSTVisitor):
|
|
|
217
217
|
self.no_mutate_lines.add(line)
|
|
218
218
|
return True
|
|
219
219
|
|
|
220
|
+
def visit_ParenthesizedWhitespace(self, node: cst.ParenthesizedWhitespace) -> bool | None:
|
|
221
|
+
# A comment ending a line inside brackets (a call, a literal, a parenthesized
|
|
222
|
+
# condition) belongs to this whitespace node, not to the statement's trailing
|
|
223
|
+
# whitespace, so visit_SimpleStatementLine never sees it.
|
|
224
|
+
if _parse_pragma_token(node.first_line.comment) == "bare":
|
|
225
|
+
line = self.get_metadata(PositionProvider, node).start.line
|
|
226
|
+
self.no_mutate_lines.add(line)
|
|
227
|
+
return True
|
|
228
|
+
|
|
220
229
|
def _visit_compound_header(self, node: cst.CSTNode) -> None:
|
|
221
230
|
body = getattr(node, "body", None)
|
|
222
231
|
if isinstance(body, cst.IndentedBlock):
|
|
@@ -154,7 +154,7 @@ class PytestRunner(TestRunner):
|
|
|
154
154
|
def pytest_runtest_teardown(self, item: Any, nextitem: Any) -> None:
|
|
155
155
|
unused(nextitem)
|
|
156
156
|
for function in state()._stats:
|
|
157
|
-
state().tests_by_mangled_function_name[function].add(strip_prefix(item.
|
|
157
|
+
state().tests_by_mangled_function_name[function].add(strip_prefix(item.nodeid, prefix="mutants/"))
|
|
158
158
|
state()._stats.clear()
|
|
159
159
|
|
|
160
160
|
# noinspection PyMethodMayBeStatic
|
|
@@ -21,7 +21,8 @@ def run_type_checker(type_check_command: list[str]) -> list[TypeCheckingError]:
|
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
23
|
if "mypy" in type_check_command:
|
|
24
|
-
|
|
24
|
+
# mypy prints a lone newline (not an empty string) when it found no errors
|
|
25
|
+
report = [json.loads(line) for line in completed_process.stdout.splitlines() if line.strip()]
|
|
25
26
|
else:
|
|
26
27
|
report = json.loads(completed_process.stdout)
|
|
27
28
|
except json.JSONDecodeError:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|