Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-mutmut for openSUSE:Factory 
checked in at 2026-10-29 22:22:31
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-mutmut (Old)
 and      /work/SRC/openSUSE:Factory/.python-mutmut.new.1255 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-mutmut"

Thu Oct 29 22:22:31 2026 rev:13 rq:1393707 version:3.8.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-mutmut/python-mutmut.changes      
2026-09-14 16:22:37.429176351 +0200
+++ /work/SRC/openSUSE:Factory/.python-mutmut.new.1255/python-mutmut.changes    
2026-10-29 22:22:35.552364358 +0100
@@ -1,0 +2,11 @@
+Thu Oct 29 10:14:45 UTC 2026 - Martin Pluskal <[email protected]>
+
+- Update to 3.8.1:
+  * Fix crash when mypy reports no errors, and stats
+    collection crash with pytest 9.2
+  * Fix enums with custom __init__ failing to import,
+    and float literals too large for a float
+  * No equivalent mutants inside string escape sequences;
+    honour "pragma: no mutate" in multi-line statements
+
+-------------------------------------------------------------------

Old:
----
  3.8.0.tar.gz

New:
----
  3.8.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-mutmut.spec ++++++
--- /var/tmp/diff_new_pack.kSXFnW/_old  2026-10-29 22:22:36.281394989 +0100
+++ /var/tmp/diff_new_pack.kSXFnW/_new  2026-10-29 22:22:36.283395073 +0100
@@ -23,7 +23,7 @@
 %endif
 %{?sle15_python_module_pythons}
 Name:           python-mutmut
-Version:        3.8.0
+Version:        3.8.1
 Release:        0
 Summary:        Python mutation testing
 License:        BSD-3-Clause
@@ -104,7 +104,7 @@
 %pre
 %python_libalternatives_reset_alternative mutmut
 
-%files %{python_files}
+%files %python_files
 %doc README.rst
 %license LICENSE
 %python_alternative %{_bindir}/mutmut

++++++ 3.8.0.tar.gz -> 3.8.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/HISTORY.rst new/mutmut-3.8.1/HISTORY.rst
--- old/mutmut-3.8.0/HISTORY.rst        2026-09-12 18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/HISTORY.rst        2026-10-28 08:46:47.000000000 +0100
@@ -1,6 +1,21 @@
 Changelog
 ---------
 
+3.8.1
+~~~~~
+
+* Fix stats collection crashing with pytest 9.2 (`#615`, `#616`)
+
+* Fix ``# pragma: no mutate`` being ignored inside multi-line statements 
(`#614`)
+
+* Don't produce equivalent case mutants inside string escape sequences like `` 
`` and ``\N{...}`` (`#609`, `#611`)
+
+* Fix enums with a custom ``__init__`` failing to import under mutmut (`#600`, 
`#603`)
+
+* Fix crash when mutating float literals too large for a float, like ``1e999`` 
(`#604`, `#605`)
+
+* Fix crash when mypy reports no errors (`#598`, `#599`)
+
 3.8.0
 ~~~~~~~~~~
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/README.rst new/mutmut-3.8.1/README.rst
--- old/mutmut-3.8.0/README.rst 2026-09-12 18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/README.rst 2026-10-28 08:46:47.000000000 +0100
@@ -373,7 +373,7 @@
 You can use following comments to disable mutation:
 
 - `# pragma: no mutate`: disable mutation on this single line
-- `# pragma: no mutate block`: disable mutation in the whole intendation block
+- `# pragma: no mutate block`: disable mutation in the whole indentation block
 - `# pragma: no mutate start/end`: disable mutation between the start and end 
comments
 
 Skipping single lines
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/pyproject.toml 
new/mutmut-3.8.1/pyproject.toml
--- old/mutmut-3.8.0/pyproject.toml     2026-09-12 18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/pyproject.toml     2026-10-28 08:46:47.000000000 +0100
@@ -1,6 +1,6 @@
 [project]
 name = "mutmut"
-version = "3.8.0"
+version = "3.8.1"
 description = "mutation testing for Python 3"
 keywords = ["mutmut", "mutant", "mutation", "test", "testing"]
 authors = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/src/mutmut/mutation/file_mutation.py 
new/mutmut-3.8.1/src/mutmut/mutation/file_mutation.py
--- old/mutmut-3.8.0/src/mutmut/mutation/file_mutation.py       2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/src/mutmut/mutation/file_mutation.py       2026-10-28 
08:46:47.000000000 +0100
@@ -35,6 +35,8 @@
 from mutmut.utils.format_utils import is_mutated_method_name
 
 NEVER_MUTATE_FUNCTION_NAMES = {"__getattribute__", "__setattr__", "__new__"}
+NEVER_MUTATE_ENUM_FUNCTION_NAMES = {"__init__", "_generate_next_value_"}
+ENUM_BASE_NAMES = {"Enum", "IntEnum", "StrEnum", "Flag", "IntFlag"}
 NEVER_MUTATE_FUNCTION_CALLS = {"len", "isinstance"}
 
 
@@ -217,6 +219,7 @@
         )
         self.ignored_classes: set[str] = set()
         self.ignored_functions: set[str] = set()
+        self._enum_creation_hooks: set[cst.FunctionDef] = set()
 
     def on_visit(self, node: cst.CSTNode) -> bool:
         if self._skip_node_and_children(node):
@@ -300,6 +303,15 @@
             if isinstance(node, cst.BaseExpression):
                 return True
 
+        if isinstance(node, cst.ClassDef) and _is_enum_class(node):
+            self._enum_creation_hooks.update(
+                child
+                for child in node.body.body
+                if isinstance(child, cst.FunctionDef) and child.name.value in 
NEVER_MUTATE_ENUM_FUNCTION_NAMES
+            )
+        if node in self._enum_creation_hooks:
+            return True
+
         if (
             isinstance(node, cst.Call)
             and isinstance(node.func, cst.Name)
@@ -344,6 +356,11 @@
         return False
 
 
+def _is_enum_class(node: cst.ClassDef) -> bool:
+    bases = (base.value.attr if isinstance(base.value, cst.Attribute) else 
base.value for base in node.bases)
+    return any(isinstance(base, cst.Name) and base.value in ENUM_BASE_NAMES 
for base in bases)
+
+
 def _walk(node: cst.CSTNode) -> Iterator[cst.CSTNode]:
     """Yield `node` and every node below it."""
     stack = [node]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/src/mutmut/mutation/mutators.py 
new/mutmut-3.8.1/src/mutmut/mutation/mutators.py
--- old/mutmut-3.8.0/src/mutmut/mutation/mutators.py    2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/src/mutmut/mutation/mutators.py    2026-10-28 
08:46:47.000000000 +0100
@@ -1,5 +1,7 @@
 """This module contains the mutations for indidvidual nodes, e.g. replacing a 
!= b with a == b."""
 
+import cmath
+import math
 import re
 from collections.abc import Callable
 from collections.abc import Iterable
@@ -17,15 +19,33 @@
     ]
 ]
 
-# pattern to match (nearly) all chars in a string that are not part of an 
escape sequence
-NON_ESCAPE_SEQUENCE = re.compile(r"((?<!\\)[^\\]+)")
+# Pattern matching either: (1) escape sequences with case-sensitive parts 
(preserved as group None),
+# or (2) runs of non-backslash characters (captured as group 1 for case 
transformation).
+# For non-raw strings, preserve \xHH, \uHHHH, \UHHHHHHHH, \N{NAME} to avoid 
equivalent mutants.
+NON_ESCAPE_SEQUENCE = re.compile(
+    r"\\x[0-9a-fA-F]{2}"  # \xHH
+    r"|\\u[0-9a-fA-F]{4}"  # \uHHHH
+    r"|\\U[0-9a-fA-F]{8}"  # \UHHHHHHHH
+    r"|\\N\{[^}]+\}"  # \N{NAME}
+    r"|(\\.)"  # other escapes like \\, \n, \t (group 1, but treat as escape)
+    r"|((?<!\\)[^\\]+)"  # OR non-backslash run (group 2, to be transformed)
+)
 
 
 def operator_number(node: cst.BaseNumber) -> Iterable[cst.BaseNumber]:
+    # A float literal too large for a float, like 1e999 (a common way to spell
+    # infinity), evaluates to inf. inf + 1 is still inf, which has no literal
+    # form, so there is no number mutant to make.
     if isinstance(node, cst.Integer | cst.Float):
-        yield node.with_changes(value=repr(node.evaluated_value + 1))
+        value = node.evaluated_value + 1
+        if isinstance(value, float) and math.isinf(value):
+            return
+        yield node.with_changes(value=repr(value))
     elif isinstance(node, cst.Imaginary):
-        yield node.with_changes(value=repr(node.evaluated_value + 1j))
+        complex_value = node.evaluated_value + 1j
+        if cmath.isinf(complex_value):
+            return
+        yield node.with_changes(value=repr(complex_value))
     else:
         print("Unexpected number type", node)
 
@@ -42,13 +62,31 @@
             # that mutation is meaningless for
             return
 
+        # For raw strings (r"..." or R"..."), escape sequences are literal, so 
case changes are NOT equivalent
+        is_raw = "r" in prefix.lower()
+
         supported_str_mutations: list[Callable[[str], str]] = [
             lambda x: "XX" + x + "XX",
-            # do not modify escape sequences, as this could break python syntax
-            lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: 
match.group(1).lower(), x),
-            lambda x: NON_ESCAPE_SEQUENCE.sub(lambda match: 
match.group(1).upper(), x),
         ]
 
+        if is_raw:
+            # Raw strings: use original regex (all non-backslash chars)
+            old_pattern = re.compile(r"((?<!\\)[^\\]+)")
+            supported_str_mutations.extend(
+                [
+                    lambda x: old_pattern.sub(lambda m: m.group(1).lower(), x),
+                    lambda x: old_pattern.sub(lambda m: m.group(1).upper(), x),
+                ]
+            )
+        else:
+            # Regular strings: preserve escape sequences with case-sensitive 
hex/names
+            supported_str_mutations.extend(
+                [
+                    lambda x: NON_ESCAPE_SEQUENCE.sub(lambda m: 
m.group(2).lower() if m.group(2) else m.group(0), x),
+                    lambda x: NON_ESCAPE_SEQUENCE.sub(lambda m: 
m.group(2).upper() if m.group(2) else m.group(0), x),
+                ]
+            )
+
         for mut_func in supported_str_mutations:
             new_value = f"{prefix}{value[0]}{mut_func(value[1:-1])}{value[-1]}"
             if new_value == value:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/src/mutmut/mutation/pragma_handling.py 
new/mutmut-3.8.1/src/mutmut/mutation/pragma_handling.py
--- old/mutmut-3.8.0/src/mutmut/mutation/pragma_handling.py     2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/src/mutmut/mutation/pragma_handling.py     2026-10-28 
08:46:47.000000000 +0100
@@ -217,6 +217,15 @@
             self.no_mutate_lines.add(line)
         return True
 
+    def visit_ParenthesizedWhitespace(self, node: cst.ParenthesizedWhitespace) 
-> bool | None:
+        # A comment ending a line inside brackets (a call, a literal, a 
parenthesized
+        # condition) belongs to this whitespace node, not to the statement's 
trailing
+        # whitespace, so visit_SimpleStatementLine never sees it.
+        if _parse_pragma_token(node.first_line.comment) == "bare":
+            line = self.get_metadata(PositionProvider, node).start.line
+            self.no_mutate_lines.add(line)
+        return True
+
     def _visit_compound_header(self, node: cst.CSTNode) -> None:
         body = getattr(node, "body", None)
         if isinstance(body, cst.IndentedBlock):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/src/mutmut/runners/harness.py 
new/mutmut-3.8.1/src/mutmut/runners/harness.py
--- old/mutmut-3.8.0/src/mutmut/runners/harness.py      2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/src/mutmut/runners/harness.py      2026-10-28 
08:46:47.000000000 +0100
@@ -154,7 +154,7 @@
             def pytest_runtest_teardown(self, item: Any, nextitem: Any) -> 
None:
                 unused(nextitem)
                 for function in state()._stats:
-                    
state().tests_by_mangled_function_name[function].add(strip_prefix(item._nodeid, 
prefix="mutants/"))
+                    
state().tests_by_mangled_function_name[function].add(strip_prefix(item.nodeid, 
prefix="mutants/"))
                 state()._stats.clear()
 
             # noinspection PyMethodMayBeStatic
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/src/mutmut/type_checking.py 
new/mutmut-3.8.1/src/mutmut/type_checking.py
--- old/mutmut-3.8.0/src/mutmut/type_checking.py        2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/src/mutmut/type_checking.py        2026-10-28 
08:46:47.000000000 +0100
@@ -21,7 +21,8 @@
 
     try:
         if "mypy" in type_check_command:
-            report = [json.loads(line) for line in 
completed_process.stdout.splitlines()]
+            # mypy prints a lone newline (not an empty string) when it found 
no errors
+            report = [json.loads(line) for line in 
completed_process.stdout.splitlines() if line.strip()]
         else:
             report = json.loads(completed_process.stdout)
     except json.JSONDecodeError:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/tests/e2e/test_e2e_my_lib.py 
new/mutmut-3.8.1/tests/e2e/test_e2e_my_lib.py
--- old/mutmut-3.8.0/tests/e2e/test_e2e_my_lib.py       2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/tests/e2e/test_e2e_my_lib.py       2026-10-28 
08:46:47.000000000 +0100
@@ -85,7 +85,6 @@
                 "my_lib.x_escape_sequences__mutmut_2": 0,
                 "my_lib.x_escape_sequences__mutmut_3": 1,
                 "my_lib.x_escape_sequences__mutmut_4": 0,
-                "my_lib.x_escape_sequences__mutmut_5": 0,
                 "my_lib.x_create_a_segfault_when_mutated__mutmut_1": -11,
                 "my_lib.x_create_a_segfault_when_mutated__mutmut_2": 0,
                 "my_lib.x_create_a_segfault_when_mutated__mutmut_3": 0,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/tests/mutation/test_mutation.py 
new/mutmut-3.8.1/tests/mutation/test_mutation.py
--- old/mutmut-3.8.0/tests/mutation/test_mutation.py    2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/tests/mutation/test_mutation.py    2026-10-28 
08:46:47.000000000 +0100
@@ -234,11 +234,19 @@
             r'a: str = "Fo\N{ghost} \U11223344"',
             [
                 r'a: str = "XXFo\N{ghost} \U11223344XX"',
-                r'a: str = "FO\N{GHOST} \U11223344"',
+                r'a: str = "FO\N{ghost} \U11223344"',
                 r'a: str = "fo\N{ghost} \U11223344"',
                 "a: str = None",
             ],
         ),
+        # Escape sequences with hex digits should not produce equivalent 
case-flip mutants
+        (r'"\u00a0"', [r'"XX\u00a0XX"']),
+        (r'"\U0001f600"', [r'"XX\U0001f600XX"']),
+        (r'"\x7f"', [r'"XX\x7fXX"']),
+        (r'"\N{GREEK CAPITAL LETTER DELTA}"', [r'"XX\N{GREEK CAPITAL LETTER 
DELTA}XX"']),
+        (r'"Hello\u00a0World"', [r'"XXHello\u00a0WorldXX"', 
r'"hello\u00a0world"', r'"HELLO\u00a0WORLD"']),
+        # Raw strings: escapes are literal, so case changes are NOT equivalent
+        (r'r"\u00a0"', [r'r"XX\u00a0XX"', r'r"\u00A0"']),
         ("lambda: 0", ["lambda: 1", "lambda: None"]),
         ("1 in (1, 2)", ["2 in (1, 2)", "1 not in (1, 2)", "1 in (2, 2)", "1 
in (1, 3)"]),
         ("1+1", ["2+1", "1 - 1", "1+2"]),
@@ -299,6 +307,11 @@
         ("1.0j", "2j"),
         ("0o1", "2"),
         ("1.0e10", "10000000001.0"),
+        # too large for a float, so they evaluate to inf, which has no literal 
form
+        ("1e999", []),
+        ("1e999j", []),
+        ("a = 1e999", "a = None"),
+        ("1.5e400 + 2", ["1.5e400 - 2", "1.5e400 + 3"]),
         ("a = {x for x in y}", "a = None"),
         ("x+=1", ["x = 1", "x -= 1", "x+=2"]),
         ("x-=1", ["x = 1", "x += 1", "x-=2"]),
@@ -603,6 +616,37 @@
     assert "return val + 1" in mutated_code
 
 
+def test_pragma_no_mutate_inside_multiline_call():
+    """A pragma on one line of a multi-line call suppresses that line only;
+    the other arguments are still mutated."""
+    source = """
+def foo(a, b):
+    return max(
+        a + 1,  # pragma: no mutate
+        b - 1,
+    )
+""".strip()
+    mutated_code = mutated_module(source)
+    assert "a - 1" not in mutated_code
+    assert "a + 2" not in mutated_code
+    assert "b + 1" in mutated_code
+    assert "b - 2" in mutated_code
+
+
+def test_pragma_no_mutate_inside_multiline_dict():
+    source = """
+def foo(a):
+    return {
+        "x": a * 2,  # pragma: no mutate
+        "y": a * 3,
+    }
+""".strip()
+    mutated_code = mutated_module(source)
+    assert "a / 2" not in mutated_code
+    assert "a * 3,  # pragma" not in mutated_code
+    assert "a / 3" in mutated_code
+
+
 def test_pragma_no_mutate_on_match_case_guard():
     """A pragma on a `case ... if guard:` line suppresses mutation of the
     guard expression, e.g. `n > 0` becoming `n >= 0`."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/tests/mutation/test_mutation_runtime.py 
new/mutmut-3.8.1/tests/mutation/test_mutation_runtime.py
--- old/mutmut-3.8.0/tests/mutation/test_mutation_runtime.py    2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/tests/mutation/test_mutation_runtime.py    2026-10-28 
08:46:47.000000000 +0100
@@ -38,6 +38,39 @@
     monkeypatch.setenv("MUTANT_UNDER_TEST", mutant_name)
 
 
+def test_enum_creation_hooks_runtime_execution(monkeypatch):
+    """Test that an enum whose __init__ and _generate_next_value_ run during 
class creation can be executed."""
+    source = """
+from enum import Enum, auto
+
+class Color(Enum):
+    def _generate_next_value_(name, start, count, last_values):
+        return count + 10
+
+    RED = auto()
+    GREEN = auto()
+
+    def __init__(self, value):
+        self.label = f"c{value}"
+
+    def describe(self):
+        return self.name.lower()
+""".strip()
+
+    mutated_file = mutate_file_contents("test.py", source)
+    mutated_code, mutant_names = mutated_file.code, mutated_file.mutant_names
+    assert len(mutant_names) > 0, "Should have at least one mutant"
+
+    monkeypatch.setenv("MUTANT_UNDER_TEST", "none")
+    namespace = {"__name__": "test_module"}
+    exec(mutated_code, namespace)
+    Color = namespace["Color"]
+
+    assert Color.GREEN.value == 11
+    assert Color.GREEN.label == "c11"
+    assert Color.GREEN.describe() == "green"
+
+
 def test_type_annotation_runtime_execution(monkeypatch):
     """Test that forward and quoted annotations can be executed
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/tests/mutation/test_pragma_handling.py 
new/mutmut-3.8.1/tests/mutation/test_pragma_handling.py
--- old/mutmut-3.8.0/tests/mutation/test_pragma_handling.py     2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/tests/mutation/test_pragma_handling.py     2026-10-28 
08:46:47.000000000 +0100
@@ -113,6 +113,56 @@
         assert ignored_code.ignore_node_lines == set()
 
 
+class TestMultiLineStatementPragma:
+    """A pragma at the end of a line inside brackets suppresses that line."""
+
+    def test_call_argument(self):
+        source = """
+def foo(a, b):
+    return max(
+        a + 1,  # pragma: no mutate
+        b - 1,
+    )
+"""
+        ignored_code = _parse_ignored_code("test.py", source)
+        assert ignored_code.no_mutate_lines == {4}
+        assert ignored_code.ignore_node_lines == set()
+
+    def test_dict_entry(self):
+        source = """
+def foo(a):
+    return {
+        "x": a * 2,  # pragma: no mutate
+        "y": a * 3,
+    }
+"""
+        ignored_code = _parse_ignored_code("test.py", source)
+        assert ignored_code.no_mutate_lines == {4}
+
+    def test_parenthesized_condition(self):
+        source = """
+def foo(a, b):
+    if (
+        a > 1  # pragma: no mutate
+        and b > 1
+    ):
+        return 1
+"""
+        ignored_code = _parse_ignored_code("test.py", source)
+        assert ignored_code.no_mutate_lines == {4}
+
+    def test_other_comment_inside_brackets_ignored(self):
+        source = """
+def foo(a, b):
+    return max(
+        a + 1,  # just a comment
+        b - 1,
+    )
+"""
+        ignored_code = _parse_ignored_code("test.py", source)
+        assert ignored_code.no_mutate_lines == set()
+
+
 class TestMatchCasePragma:
     """Tests for pragmas on `match` and `case` headers."""
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/tests/test_type_checking.py 
new/mutmut-3.8.1/tests/test_type_checking.py
--- old/mutmut-3.8.0/tests/test_type_checking.py        2026-09-12 
18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/tests/test_type_checking.py        2026-10-28 
08:46:47.000000000 +0100
@@ -1,3 +1,5 @@
+import json
+import subprocess
 from pathlib import Path
 
 import pytest
@@ -7,6 +9,54 @@
 from mutmut.type_checking import TypeCheckingError
 from mutmut.type_checking import parse_mypy_report
 from mutmut.type_checking import parse_pyrefly_report
+from mutmut.type_checking import run_type_checker
+
+MYPY_COMMAND = ["mypy", "src", "--output", "json"]
+
+
+def _fake_completed_process(monkeypatch, stdout: str, returncode: int = 0, 
stderr: str = "") -> None:
+    def fake_run(command, **kwargs):
+        return subprocess.CompletedProcess(command, returncode, stdout=stdout, 
stderr=stderr)
+
+    monkeypatch.setattr("mutmut.type_checking.subprocess.run", fake_run)
+
+
+def test_run_type_checker_mypy_without_errors(monkeypatch):
+    # mypy prints a lone newline (and exits with 0) when it found no type 
errors at all
+    _fake_completed_process(monkeypatch, stdout="\n", returncode=0)
+
+    assert run_type_checker(MYPY_COMMAND) == []
+
+
+def test_run_type_checker_mypy_with_errors(monkeypatch):
+    error = {
+        "file": "src/mod.py",
+        "line": 2,
+        "column": 11,
+        "message": 'Incompatible return value type (got "int", expected 
"str")',
+        "hint": None,
+        "code": "return-value",
+        "severity": "error",
+    }
+    # mypy prints one JSON object per line and exits with 1 when it found type 
errors
+    _fake_completed_process(monkeypatch, 
stdout=f"{json.dumps(error)}\n{json.dumps(error)}\n", returncode=1)
+
+    errors = run_type_checker(MYPY_COMMAND)
+
+    assert [(e.file_path.name, e.line_number) for e in errors] == [("mod.py", 
2), ("mod.py", 2)]
+
+
+def test_run_type_checker_mypy_still_raises_on_non_json_output(monkeypatch):
+    # a fatal mypy failure (exit code 2) must stay loud and not be mistaken 
for "no errors"
+    _fake_completed_process(
+        monkeypatch,
+        stdout="Found 1 error in 1 file (errors prevented further checking)\n",
+        returncode=2,
+        stderr="mypy: error: Cannot read file 'src': No such file or 
directory",
+    )
+
+    with pytest.raises(Exception, match="type check command did not return 
JSON"):
+        run_type_checker(MYPY_COMMAND)
 
 
 def test_mypy_parsing():
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/mutmut-3.8.0/uv.lock new/mutmut-3.8.1/uv.lock
--- old/mutmut-3.8.0/uv.lock    2026-09-12 18:53:22.000000000 +0200
+++ new/mutmut-3.8.1/uv.lock    2026-10-28 08:46:47.000000000 +0100
@@ -486,7 +486,7 @@
 
 [[package]]
 name = "mutmut"
-version = "3.8.0"
+version = "3.8.1"
 source = { editable = "." }
 dependencies = [
     { name = "click" },

Reply via email to