Repository: boxed/mutmut

Issue:
Incompatibility with Pydantic model which contains a method
For the following minimal example:
```python
from pydantic import BaseModel

class Model(BaseModel):
    max_retries: int = 2

    def get_max_tries(self) -> int:
        return self.max_retries + 1
```

running `mutmut run` produces error:
```
> mutmut run
⠦ Generating mutants
    done in 1ms
⠇ Running stats     
=========================================================================================================================================================================================================== ERRORS ============================================================================================================================================================================================================
_____________________________________________________________________________________________________________________________________________________________________________________________ ERROR collecting tests/test_main.py _____________________________________________________________________________________________________________________________________________________________________________________________
tests/test_main.py:1: in <module>
    from src.main import Model
src/main.py:49: in <module>
    class Model(BaseModel):
../.venv/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py:113: in __new__
    private_attributes = inspect_namespace(
../.venv/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py:506: in inspect_namespace
    raise PydanticUserError(
E   pydantic.errors.PydanticUserError: A non-annotated attribute was detected: `xǁModelǁget_max_tries__mutmut_mutants = {'xǁModelǁget_max_tries__mutmut_1': <function Model.xǁModelǁget_max_tries__mutmut_1 at 0x1062d4c20>, 'xǁModelǁget_max_tries__mutmut_2': <function Model.xǁModelǁget_max_tries__mutmut_2 at 0x1062d4cc0>}`. All model fields require a type annotation; if `xǁModelǁget_max_tries__mutmut_mutants` is not meant to be a field, you may be able to resolve this error by annotating it as a `ClassVar` or updating `model_config['ignored_types']`.
E   
E   For further information visit https://errors.pydantic.dev/2.10/u/model-field-missing-annotation
=================================================================================================================================================================================================== short test summary info ===================================================================================================================================================================================================
ERROR tests/test_main.py - pydantic.errors.PydanticUserError: A non-annotated attribute was detected: `xǁModelǁget_max_tries__mutmut_mutants = {'xǁModelǁget_max_tries__mutmut_1': <function Model.xǁModelǁget_max_tries__mutmut_1 at 0x1062d4c20>, 'xǁModelǁget_max_tries__mutmut_2': <function Model.xǁModelǁget_max_tries__mutmut_2 at 0x1062d4cc0>}`. All model fields require a type annotation; if `xǁModelǁget_max_tr...
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1 error in 0.13s
failed to collect stats. runner returned 2
```

The content of `mutants/src/main.py`:
```python
from inspect import signature as _mutmut_signature

def _mutmut_trampoline(orig, mutants, *args, **kwargs):
    import os
    mutant_under_test = os.environ['MUTANT_UNDER_TEST']
    if mutant_under_test == 'fail':
        from mutmut.__main__ import MutmutProgrammaticFailException
        raise MutmutProgrammaticFailException('Failed programmatically')      
    elif mutant_under_test == 'stats':
        from mutmut.__main__ import record_trampoline_hit
        record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
        result = orig(*args, **kwargs)
        return result  # for the yield case
    prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
    if not mutant_under_test.startswith(prefix):
        result = orig(*args, **kwargs)
        return result  # for the yield case
    mutant_name = mutant_under_test.rpartition('.')[-1]
    result = mutants[mutant_name](*args, **kwargs)
    return result


from inspect import signature as _mutmut_signature

def _mutmut_yield_from_trampoline(orig, mutants, *args, **kwargs):
    import os
    mutant_under_test = os.environ['MUTANT_UNDER_TEST']
    if mutant_under_test == 'fail':
        from mutmut.__main__ import MutmutProgrammaticFailException
        raise MutmutProgrammaticFailException('Failed programmatically')      
    elif mutant_under_test == 'stats':
        from mutmut.__main__ import record_trampoline_hit
        record_trampoline_hit(orig.__module__ + '.' + orig.__name__)
        result = yield from orig(*args, **kwargs)
        return result  # for the yield case
    prefix = orig.__module__ + '.' + orig.__name__ + '__mutmut_'
    if not mutant_under_test.startswith(prefix):
        result = yield from orig(*args, **kwargs)
        return result  # for the yield case
    mutant_name = mutant_under_test.rpartition('.')[-1]
    result = yield from mutants[mutant_name](*args, **kwargs)
    return result


from pydantic import BaseModel


class Model(BaseModel):
    max_retries: int = 2

    def xǁModelǁget_max_tries__mutmut_orig(self) -> int:
        return self.max_retries + 1

    def xǁModelǁget_max_tries__mutmut_1(self) -> int:
        return self.max_retries - 1

    def xǁModelǁget_max_tries__mutmut_2(self) -> int:
        return self.max_retries + 2

    xǁModelǁget_max_tries__mutmut_mutants = {
    'xǁModelǁget_max_tries__mutmut_1': xǁModelǁget_max_tries__mutmut_1, 
        'xǁModelǁget_max_tries__mutmut_2': xǁModelǁget_max_tries__mutmut_2
    }

    def get_max_tries(self, *args, **kwargs):
        result = _mutmut_trampoline(object.__getattribute__(self, "xǁModelǁget_max_tries__mutmut_orig"), object.__getattribute__(self, "xǁModelǁget_max_tries__mutmut_mutants"), *args, **kwargs)
        return result 

    get_max_tries.__signature__ = _mutmut_signature(xǁModelǁget_max_tries__mutmut_orig)
    xǁModelǁget_max_tries__mutmut_orig.__name__ = 'xǁModelǁget_max_tries'
```

is this something that is going to be fixed?

Discussion:

Fix (patch):
diff --git a/HISTORY.rst b/HISTORY.rst
index d273cb0..62715e4 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -1,6 +1,11 @@
 Changelog
 ---------
 
+Unreleased
+~~~~~~~~~~
+
+* Annotate mutant dicts (and fixes compatibility with Pydantic)
+
 3.2.3
 ~~~~~
 
diff --git a/mutmut/__main__.py b/mutmut/__main__.py
index a3e37eb..589f0c4 100644
--- a/mutmut/__main__.py
+++ b/mutmut/__main__.py
@@ -188,6 +188,13 @@ class BadTestExecutionCommandsException(Exception):
 # language=python
 trampoline_impl = """
 from inspect import signature as _mutmut_signature
+from typing import Annotated
+from typing import Callable
+from typing import ClassVar
+
+
+MutantDict = Annotated[dict[str, Callable], "Mutant"]
+
 
 def _mutmut_trampoline(orig, mutants, *args, **kwargs):
     import os
@@ -318,7 +325,7 @@ def build_trampoline(*, orig_name, mutants, class_name, is_generator):
 
     mangled_name = mangle_function_name(name=orig_name, class_name=class_name)
 
-    mutants_dict = f'{mangled_name}__mutmut_mutants = {{\n' + ', \n    '.join(f'{repr(m)}: {m}' for m in mutants) + '\n}'
+    mutants_dict = f'{mangled_name}__mutmut_mutants : ClassVar[MutantDict] = {{\n' + ', \n    '.join(f'{repr(m)}: {m}' for m in mutants) + '\n}'
     access_prefix = ''
     access_suffix = ''
     if class_name is not None:


Test changes:
diff --git a/tests/test_mutmut3.py b/tests/test_mutmut3.py
index 3c8ee4b..18ecda6 100644
--- a/tests/test_mutmut3.py
+++ b/tests/test_mutmut3.py
@@ -28,7 +28,7 @@ def x_foo__mutmut_1(a, b, c):
 def x_foo__mutmut_2(a, b, c):
     return a + b / c
 
-x_foo__mutmut_mutants = {
+x_foo__mutmut_mutants : ClassVar[MutantDict] = {
 'x_foo__mutmut_1': x_foo__mutmut_1, 
     'x_foo__mutmut_2': x_foo__mutmut_2
 }
@@ -63,7 +63,7 @@ def x_foo__mutmut_orig(a: List[int]) -> int:
 def x_foo__mutmut_1(a: List[int]) -> int:
     return 2
 
-x_foo__mutmut_mutants = {
+x_foo__mutmut_mutants : ClassVar[MutantDict] = {
 'x_foo__mutmut_1': x_foo__mutmut_1
 }