Summary
mutmut auto-copies pyproject.toml (plus tests/, setup.cfg, test*.py) into the mutants/ tree, but not the adjacent lockfile. When a project's type_check_command or test command uses a resolver wrapper (uv run …, poetry run …), that subprocess runs in the mutants/ cwd, finds the copied pyproject.toml with no lock beside it, and re-resolves dependencies from the version constraints. The resulting mutants/.venv can differ from the project's locked/installed environment, silently breaking type-checking or tests inside mutmut.
mutmut already copies the dependency spec (pyproject.toml); copying the matching resolution (the lockfile) is the natural completeness fix. Adding also_copy = ["uv.lock"] works around it today, confirming the missing lockfile is the cause.
Scope: affects projects that resolve deps via uv/poetry and wrap their type-check/test command in uv run/poetry run. Projects using a bare command inherit the active environment and are unaffected.
Why wrap the command in uv run at all?
For a project packaged as an installable distribution (e.g. [tool.uv] package = true) whose code imports itself by package name (from mypkg import …), the type checker must resolve mypkg to the mutant copy under mutants/src, not the original source. uv run executed in the mutants/ cwd reinstalls the project editable against the mutant tree, giving the checker one consistent copy.
A bare pyrefly check instead inherits the parent environment, where mypkg is editable-installed pointing at the original src/. The checker then sees two copies of every class — the originals (via the installed package) and the mutants (the files under check) — and reports spurious "X is not assignable to Y" errors across the two trees, on non-mutant lines, which aborts mutmut. So uv run/poetry run is the correct command for installed-package projects; it is not gratuitous. (Projects that merely put src/ on the path have no editable install to conflict, so the bare command shown in the docs works for them.)
Root cause
mutmut/configuration.py (~line 125) hardcodes the copy list with pyproject.toml but no lockfile:
also_copy=[Path(y) for y in s("also_copy", [])]
+ [
Path("tests/"),
Path("test/"),
Path("setup.cfg"),
Path("pyproject.toml"),
]
+ list(Path(".").glob("test*.py")),
Suggested fix
Add lockfiles to the same hardcoded list — missing ones are already skipped by the existence check in copy_also_copy_files, exactly like setup.cfg:
also_copy=[Path(y) for y in s("also_copy", [])]
+ [
Path("tests/"),
Path("test/"),
Path("setup.cfg"),
Path("pyproject.toml"),
Path("uv.lock"),
Path("poetry.lock"),
Path("Pipfile.lock"),
Path("pdm.lock"),
]
+ list(Path(".").glob("test*.py")),
Copying only when present (the existing copy_also_copy_files guard) means no behavior change for projects without a lockfile.
Environment
Summary
mutmut auto-copies
pyproject.toml(plustests/,setup.cfg,test*.py) into themutants/tree, but not the adjacent lockfile. When a project'stype_check_commandor test command uses a resolver wrapper (uv run …,poetry run …), that subprocess runs in themutants/cwd, finds the copiedpyproject.tomlwith no lock beside it, and re-resolves dependencies from the version constraints. The resultingmutants/.venvcan differ from the project's locked/installed environment, silently breaking type-checking or tests inside mutmut.mutmut already copies the dependency spec (
pyproject.toml); copying the matching resolution (the lockfile) is the natural completeness fix. Addingalso_copy = ["uv.lock"]works around it today, confirming the missing lockfile is the cause.Scope: affects projects that resolve deps via uv/poetry and wrap their type-check/test command in
uv run/poetry run. Projects using a bare command inherit the active environment and are unaffected.Why wrap the command in
uv runat all?For a project packaged as an installable distribution (e.g.
[tool.uv] package = true) whose code imports itself by package name (from mypkg import …), the type checker must resolvemypkgto the mutant copy undermutants/src, not the original source.uv runexecuted in themutants/cwd reinstalls the project editable against the mutant tree, giving the checker one consistent copy.A bare
pyrefly checkinstead inherits the parent environment, wheremypkgis editable-installed pointing at the originalsrc/. The checker then sees two copies of every class — the originals (via the installed package) and the mutants (the files under check) — and reports spurious "X is not assignable to Y" errors across the two trees, on non-mutant lines, which aborts mutmut. Souv run/poetry runis the correct command for installed-package projects; it is not gratuitous. (Projects that merely putsrc/on the path have no editable install to conflict, so the bare command shown in the docs works for them.)Root cause
mutmut/configuration.py(~line 125) hardcodes the copy list withpyproject.tomlbut no lockfile:Suggested fix
Add lockfiles to the same hardcoded list — missing ones are already skipped by the existence check in
copy_also_copy_files, exactly likesetup.cfg:Copying only when present (the existing
copy_also_copy_filesguard) means no behavior change for projects without a lockfile.Environment