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.
Files changed (39) hide show
  1. {mutmut-3.8.0 → mutmut-3.8.1}/HISTORY.rst +15 -0
  2. {mutmut-3.8.0 → mutmut-3.8.1}/PKG-INFO +2 -2
  3. {mutmut-3.8.0 → mutmut-3.8.1}/README.rst +1 -1
  4. {mutmut-3.8.0 → mutmut-3.8.1}/pyproject.toml +1 -1
  5. {mutmut-3.8.0 → mutmut-3.8.1}/pyproject.toml.orig +1 -1
  6. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/file_mutation.py +17 -0
  7. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/mutators.py +45 -7
  8. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/pragma_handling.py +9 -0
  9. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/runners/harness.py +1 -1
  10. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/type_checking.py +2 -1
  11. {mutmut-3.8.0 → mutmut-3.8.1}/LICENSE +0 -0
  12. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/__init__.py +0 -0
  13. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/__main__.py +0 -0
  14. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/code_coverage.py +0 -0
  15. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/configuration.py +0 -0
  16. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/core.py +0 -0
  17. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/models/__init__.py +0 -0
  18. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/models/results.py +0 -0
  19. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/__init__.py +0 -0
  20. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/data.py +0 -0
  21. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/diff_apply.py +0 -0
  22. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/trampoline.py +0 -0
  23. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/mutation/trampoline_templates.py +0 -0
  24. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/py.typed +0 -0
  25. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/runners/__init__.py +0 -0
  26. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/state.py +0 -0
  27. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/stats.py +0 -0
  28. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/__init__.py +0 -0
  29. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/browse.py +0 -0
  30. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/result_browser_layout.tcss +0 -0
  31. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/ui/terminal.py +0 -0
  32. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/__init__.py +0 -0
  33. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/file_utils.py +0 -0
  34. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/format_utils.py +0 -0
  35. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/logging_utils.py +0 -0
  36. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/utils/safe_setproctitle.py +0 -0
  37. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/workers/__init__.py +0 -0
  38. {mutmut-3.8.0 → mutmut-3.8.1}/src/mutmut/workers/isolation.py +0 -0
  39. {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.0
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 intendation block
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 intendation block
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mutmut"
3
- version = "3.8.0"
3
+ version = "3.8.1"
4
4
  description = "mutation testing for Python 3"
5
5
  keywords = [
6
6
  "mutmut",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mutmut"
3
- version = "3.8.0"
3
+ version = "3.8.1"
4
4
  description = "mutation testing for Python 3"
5
5
  keywords = ["mutmut", "mutant", "mutation", "test", "testing"]
6
6
  authors = [
@@ -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
- # pattern to match (nearly) all chars in a string that are not part of an escape sequence
21
- NON_ESCAPE_SEQUENCE = re.compile(r"((?<!\\)[^\\]+)")
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
- yield node.with_changes(value=repr(node.evaluated_value + 1))
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
- yield node.with_changes(value=repr(node.evaluated_value + 1j))
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._nodeid, prefix="mutants/"))
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
- report = [json.loads(line) for line in completed_process.stdout.splitlines()]
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