Skip to content

Preserve function signature#469

Merged
Otto-AA merged 3 commits intomainfrom
keep-signature
Feb 15, 2026
Merged

Preserve function signature#469
Otto-AA merged 3 commits intomainfrom
keep-signature

Conversation

@Otto-AA
Copy link
Collaborator

@Otto-AA Otto-AA commented Feb 14, 2026

Closes #454 and #465 .

@boxed In case you want to review, this is a slightly bigger change of trampoline internals. This PR adds some complexity at the trampoline wrapper (handling various possible parameter formats, async declarations and async generators), but I think it's worth it so we can preserve the original function signature. Also, don't need to manually copy the signature with trampoline.__signature__ = inspect.signature(original_method) anymore.

With this change, asyncio.iscoroutinefunction(async_func) also returns true after we mutated the function. This is done by using the same signature as it was originally, and adding some logic to pass our arguments to the trampoline.

For instance:

async def foo(a: str, b, *args, **kwargs) -> dict[str, int]:
    # ...

Will be mutated to:

async def foo(a: str, b, *args, **kwargs) -> dict[str, int]:
    args = [a, b, *args]
    kwargs = {**kwargs}
    return await _mutmut_trampoline(x_foo__mutmut_orig, x_foo__mutmut_mutants, args, kwargs, None)

In the commit 944d43f, I've also added inline-snapshot (+ruff formatting for the snapshots) to make it easier to work with regression snapshots. e.g. the tests/test_mutation regression.py are useful to have and with inline-snapshot it's easier to update them whenever some implementation detail changes.

The e2e tests and a sample repository I tried were not affected by this change, so I don't think it breaks existing functionality.

@Otto-AA Otto-AA requested a review from boxed February 14, 2026 19:42
@boxed
Copy link
Owner

boxed commented Feb 14, 2026

Nice!

@Otto-AA Otto-AA merged commit f631c40 into main Feb 15, 2026
10 checks passed
@Peter-Lavigne
Copy link

I think this PR introduced a regression for default-value mutations. With the old *args, **kwargs trampoline, a mutation to a default parameter was observable because the mutant function resolved its own default. With the new signature-preserving trampoline, the wrapper resolves the default before dispatch, making the mutation unreachable.

For example, given this source:

def check(fix: bool = False) -> None:
    ...

The generated trampoline is:

def check(fix: bool = False) -> None:
    args = [fix]
    kwargs = {}
    return _mutmut_trampoline(x_check__mutmut_orig, x_check__mutmut_mutants, args, kwargs, None)

I'm new to mutation testing and to this repo, so let me know if I'm missing something.

@Otto-AA
Copy link
Collaborator Author

Otto-AA commented Mar 2, 2026

Thanks for pointing this out. I currently don't have the time to verify this, but it does look like a regression.

I'm not sure yet how to fix it (removing the default value would break calls that do not pass the "fix" value; keeping the default would make it hard to know if a value has been passed or not; maybe we could set it to some custom unique value instead?)

@Otto-AA
Copy link
Collaborator Author

Otto-AA commented Mar 2, 2026

I've opened #477 to track this default arg issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Async functions get synchronous trampolines causing "coroutine was never awaited" errors

3 participants