Repository: boxed/mutmut

Issue:
Mutmut tries to mutate metaclasses
It looks like mutmut breaks when encountering metaclasses:

### foobar/__init__.py
```python
class MetaFoobar(type):
    def __new__(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, bases, dct)

class Foobar(metaclass=MetaFoobar):
    pass
```

### tests/test_foobar.py
```python
from foobar import Foobar

def test_foobar():
    assert Foobar().hello() == "Hello, World!"
```

### console
```console
$ pytest --quiet tests
.
1 passed in 0.01s
$ mutmut run
⠋ Generating mutantsfoobar/__init__.py
     also copying tests
     also copying test
     also copying setup.cfg
     also copying pyproject.toml

    done in 1ms
⠙ Listing all tests python -m pytest  -vv -x -q --collect-only --rootdir=.
=============================================== test session starts ================================================
platform linux -- Python 3.13.1, pytest-8.3.4, pluggy-1.5.0 -- /home/eloi/dev/test/.venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/eloi/dev/test/mutants
collected 0 items / 1 error

====================================================== ERRORS ======================================================
______________________________________ ERROR collecting tests/test_foobar.py _______________________________________
/usr/lib/python3.13/importlib/__init__.py:88: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1387: in _gcd_import
    ???
<frozen importlib._bootstrap>:1360: in _find_and_load
    ???
<frozen importlib._bootstrap>:1331: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:935: in _load_unlocked
    ???
../.venv/lib/python3.13/site-packages/_pytest/assertion/rewrite.py:184: in exec_module
    exec(co, module.__dict__)
../tests/test_foobar.py:1: in <module>
    from foobar import Foobar
foobar/__init__.py:115: in <module>
    class Foobar(metaclass=MetaFoobar):
foobar/__init__.py:107: in __new__
    result = _mutmut_trampoline(object.__getattribute__(self, "xǁMetaFoobarǁ__new____mutmut_orig"), object.__getattribute__(self, "xǁMetaFoobarǁ__new____mutmut_mutants"), *args, **kwargs)
foobar/__init__.py:17: in _mutmut_trampoline
    result = orig(*args, **kwargs)
E   TypeError: MetaFoobar.xǁMetaFoobarǁ__new____mutmut_orig() missing 1 required positional argument: 'dct'
============================================= short test summary info ==============================================
ERROR tests/test_foobar.py - TypeError: MetaFoobar.xǁMetaFoobarǁ__new____mutmut_orig() missing 1 required positional argument: 'dct'
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
======================================= no tests collected, 1 error in 0.13s =======================================
    exit code 2
Failed to collect list of tests
```

### mutants/foobar/__init__.py
<details>
<summary>mutants/foobar/__init__.py</summary>

```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


class MetaFoobar(type):
    def xǁMetaFoobarǁ__new____mutmut_orig(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_1(cls, name, bases, dct):
        dct['XXhelloXX'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_2(cls, name, bases, dct):
        dct[None] = lambda self: "Hello, World!"
        return super().__new__(cls, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_3(cls, name, bases, dct):
        dct['hello'] = lambda self: "XXHello, World!XX"
        return super().__new__(cls, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_4(cls, name, bases, dct):
        dct['hello'] = lambda self: None
        return super().__new__(cls, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_5(cls, name, bases, dct):
        dct['hello'] = None
        return super().__new__(cls, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_6(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(None, name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_7(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, None, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_8(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, None, dct)
    def xǁMetaFoobarǁ__new____mutmut_9(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, bases, None)
    def xǁMetaFoobarǁ__new____mutmut_10(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__( name, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_11(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, bases, dct)
    def xǁMetaFoobarǁ__new____mutmut_12(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, dct)
    def xǁMetaFoobarǁ__new____mutmut_13(cls, name, bases, dct):
        dct['hello'] = lambda self: "Hello, World!"
        return super().__new__(cls, name, bases,)

    xǁMetaFoobarǁ__new____mutmut_mutants = {
    'xǁMetaFoobarǁ__new____mutmut_1': xǁMetaFoobarǁ__new____mutmut_1,
        'xǁMetaFoobarǁ__new____mutmut_2': xǁMetaFoobarǁ__new____mutmut_2,
        'xǁMetaFoobarǁ__new____mutmut_3': xǁMetaFoobarǁ__new____mutmut_3,
        'xǁMetaFoobarǁ__new____mutmut_4': xǁMetaFoobarǁ__new____mutmut_4,
        'xǁMetaFoobarǁ__new____mutmut_5': xǁMetaFoobarǁ__new____mutmut_5,
        'xǁMetaFoobarǁ__new____mutmut_6': xǁMetaFoobarǁ__new____mutmut_6,
        'xǁMetaFoobarǁ__new____mutmut_7': xǁMetaFoobarǁ__new____mutmut_7,
        'xǁMetaFoobarǁ__new____mutmut_8': xǁMetaFoobarǁ__new____mutmut_8,
        'xǁMetaFoobarǁ__new____mutmut_9': xǁMetaFoobarǁ__new____mutmut_9,
        'xǁMetaFoobarǁ__new____mutmut_10': xǁMetaFoobarǁ__new____mutmut_10,
        'xǁMetaFoobarǁ__new____mutmut_11': xǁMetaFoobarǁ__new____mutmut_11,
        'xǁMetaFoobarǁ__new____mutmut_12': xǁMetaFoobarǁ__new____mutmut_12,
        'xǁMetaFoobarǁ__new____mutmut_13': xǁMetaFoobarǁ__new____mutmut_13
    }

    def __new__(self, *args, **kwargs):
        result = _mutmut_trampoline(object.__getattribute__(self, "xǁMetaFoobarǁ__new____mutmut_orig"), object.__getattribute__(self, "xǁMetaFoobarǁ__new____mutmut_mutants"), *args, **kwargs)
        return result

    __new__.__signature__ = _mutmut_signature(xǁMetaFoobarǁ__new____mutmut_orig)
    xǁMetaFoobarǁ__new____mutmut_orig.__name__ = 'xǁMetaFoobarǁ__new__'



class Foobar(metaclass=MetaFoobar):
    pass
```
</details>

I am not sure what mutmut should do here.
Anyway, mutating the signature of the `__new__` method is a guaranteed fail, so maybe mutmut could avoid this?

Discussion:
@boxed: Seems reasonable. Do you want to give it a shot to build a PR?

Solution 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..0e1fe11 100644
--- a/mutmut/__main__.py
+++ b/mutmut/__main__.py
@@ -68,7 +68,7 @@ import mutmut
 # TODO: hash of function. If hash changes, retest all mutants as mutant IDs are not stable
 
 
-NEVER_MUTATE_FUNCTION_NAMES = {'__getattribute__', '__setattr__'}
+NEVER_MUTATE_FUNCTION_NAMES = {'__getattribute__', '__setattr__', '__new__'}
 NEVER_MUTATE_FUNCTION_CALLS = {'isinstance', 'len'}
 CLASS_NAME_SEPARATOR = 'ǁ'
 
@@ -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 patch:
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
 }