Skip to content

Mutate decorated functions: keep decorators on the trampoline only (#387) - #623

Open
Culaii wants to merge 1 commit into
boxed:mainfrom
Culaii:feat/decorators-on-trampoline-only
Open

Culaii wants to merge 1 commit into
boxed:mainfrom
Culaii:feat/decorators-on-trampoline-only

Conversation

@Culaii

@Culaii Culaii commented Nov 2, 2026 •

Copy link
Copy Markdown

Closes #387. Supersedes #620 (@override only), which this covers.

Problem

A decorated function is never mutated, except one with a lone @staticmethod / @classmethod. @property, @contextmanager, @override, @abstractmethod, registration decorators (@app.get(...), a plugin registry) all take the function out of mutation testing silently: the score stays at 100% while those bodies are never measured. On the codebase I work on, 318 of 2,814 functions on the mutation surface were skipped this way, 73 of them with real branches.

Change

The approach proposed in @Otto-AA in #387 (7 Mar. 2026): the decorators stay on the trampoline only, and the copies (the original and every mutant) keep only the decorators that decide how they bind -- staticmethod / classmethod, which the dict dispatch needs to call them alike. Against the three reasons in the source comment:

  1. A decorator with side effects (@app.post("/foo"), a registry) runs once, on the trampoline, exactly as without mutmut.
  2. Decorator nodes are still skipped, so their arguments are never mutated.
  3. @property sees the trampoline, a plain function, never a copy.

is_classmethod is detected among all decorators (it was only checked for a lone one), so @deco @classmethod still binds to the subclass.

Still skipped:

  • decorators after which another function of the same name follows in the scope -- @overload, @x.setter / .getter / .deleter, @f.register -- since their copies would collide in the mangled names;
  • caching decorators (cache, lru_cache, cached_property). Measured on e2e_projects/my_lib: with the cache on the trampoline, all 9 mutants of cached_fibonacci survive although test_my_lib kills most of them -- the parent process fills the cache during the stats run, and every forked mutant answers from it. A per-copy cache (decorating the copies instead of the trampoline) would fix that, but changes caching semantics; I left it out to keep this reviewable and am happy to follow up if you want it.

Tests

  • test_decorated_bodies_are_mutated_but_never_their_decorators replaces test_do_not_mutate_top_level_decorators: decorator lines intact, bodies mutated.
  • Namesake and caching decorators are not mutated; the copies keep only the binding decorators.
  • Runtime (exec) tests: a @property stays a property and its mutants run; @contextmanager; a registering decorator runs once and registers the trampoline, which reaches the active mutant; @deco @classmethod binds to the subclass; an @override method.
  • e2e snapshot: the only new entries are Point.skip_multi_decorator's 8 mutants, all killed (the fixture's skip_ name no longer describes it -- happy to rename it here if you prefer).

Suite: 473 passed / 4 skipped on main, 490 / 4 with the change; pre-commit run --all-files passes.

I also ran a backport of the patch on 3.8.0 against ~10 modules of a production codebase: about 72 previously hidden mutants were measured and killed, nothing broke, and the 4 survivors were all genuine (3 equivalent mutants from a redundant condition, 1 missing test).

… keep only binding decorators

Follows the direction proposed in boxed#387 (Otto-AA, 7 Mar. 2026). The copies (original and mutants)
carry only staticmethod/classmethod; every other decorator wraps the trampoline alone, so it runs
once, never wraps a copy, and its arguments are still not mutated. Still skipped: decorators that
define several functions under one name (overload, x.setter/getter/deleter, f.register), and caching
decorators (cache, lru_cache, cached_property), whose cache on the trampoline would answer every
mutant from the parent process (false survivors, measured on the e2e cached_fibonacci).
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.

mutmut 3.3.0 does not mutate decorated functions/methods

1 participant