Skip to content

fix: support non-editable installs (site-packages import precedence)#487

Open
pythoniste wants to merge 2 commits intoboxed:mainfrom
inspyration:fix/353-pydantic-classvar
Open

fix: support non-editable installs (site-packages import precedence)#487
pythoniste wants to merge 2 commits intoboxed:mainfrom
inspyration:fix/353-pydantic-classvar

Conversation

@pythoniste
Copy link
Copy Markdown

When the package-under-test is installed in site-packages as a
non-editable wheel rather than via pip install -e ., mutmut's
trampoline-injected code in the mutants/ directory was never loaded.

Python imported the original (non-mutated) modules from site-packages
because they were already cached in sys.modules. The stats phase found
zero test coverage and all mutants were marked "not checked".

Add _patch_imported_packages() to setup_source_paths(): after adding
mutants/ to sys.path, patch path on every already-imported package
so submodule lookups prefer the mutants tree. Flush leaf modules whose
mutated .py exists on disk so they get re-imported with trampoline code.

Add _copy_parent_init_files() to copy_src_dir(): when paths_to_mutate
targets individual files, parent init.py files were missing from
the mutants tree, preventing Python from recognising it as a package.

Tested on a Pydantic-heavy project (8,996 mutants): stats phase now
finds 477 covered functions and achieves a 70% kill rate, where
previously it found zero coverage.

  When the package-under-test is installed in site-packages as a
  non-editable wheel rather than via `pip install -e .`, mutmut's
  trampoline-injected code in the mutants/ directory was never loaded.

  Python imported the original (non-mutated) modules from site-packages
  because they were already cached in sys.modules. The stats phase found
  zero test coverage and all mutants were marked "not checked".

  Add _patch_imported_packages() to setup_source_paths(): after adding
  mutants/ to sys.path, patch __path__ on every already-imported package
  so submodule lookups prefer the mutants tree. Flush leaf modules whose
  mutated .py exists on disk so they get re-imported with trampoline code.

  Add _copy_parent_init_files() to copy_src_dir(): when paths_to_mutate
  targets individual files, parent __init__.py files were missing from
  the mutants tree, preventing Python from recognising it as a package.

  Tested on a Pydantic-heavy project (8,996 mutants): stats phase now
  finds 477 covered functions and achieves a 70% kill rate, where
  previously it found zero coverage.
@pythoniste
Copy link
Copy Markdown
Author

By the way, the branch name is misleading. At first, I wanted to fix Pydantic and then, I fixed something else.

Copy link
Copy Markdown
Collaborator

@Otto-AA Otto-AA left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for the PR.

I don't have a lot of time for reviewing currently, so I probably won't get to test and read through this PR in the next weeks.

I don't fully understand the editable vs non-editable problem yet. Why does the editable install work fine, but the non-editable version gets imported as non-mutated version and needs to get flushed out of the modules? We do sys.path.insert(0, abs_path) to preferably import mutated code, is the non-editable version already installed at this point?

And would installing the project as editable be possible for you? I would tend to detect if the project is installed as non-editable and raise an Exception asking the user to install as editable instead.

Comment thread src/mutmut/__main__.py
Without them Python's import system cannot recognise ``pkg`` as a package
and falls back to the copy installed in site-packages, skipping the
trampoline-injected mutant code entirely.
"""
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

paths_to_mutate should not cover individual files (#414 )

Comment thread src/mutmut/__main__.py
"""Remove cached test modules from sys.modules so they get re-imported.

Called in the forked child process when testing a mutant that targets
import-time code (e.g. __init_subclass__). Without this, the child
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused, mutmut does not support import-time code mutations. Is this a feature PR to add this?

e.g. in following file:

MY_CONST = 1234

def foo():
  return 1234

Only foo will be mutated, MY_CONST won't be mutated.

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.

2 participants