Diagnose which finders handle imports and how sys.meta_path changes
Project description
metapathology
Diagnose Python import hooks without changing import outcomes.
[!IMPORTANT] This project is mostly AI generated (for now). I do understand what it does and how it works, but I only skimmed the code. I can attest the explanations in README is accurate though.
metapathology is a stdlib-only diagnostic tool for CPython imports. It
reports:
- which finder located each imported module;
- where code changed
sys.meta_path, with a stack trace; and - which modules were found without going through the usual
sys.pathandsys.path_hookssearch.
This is useful when two packages customize imports and one prevents the other
from seeing a module. The original example was
beartype#556: an editable
install finder located a module before beartype.claw's path hook could see
it.
Full guides are published at glinte.github.io/metapathology. They cover usage, import-system concepts, report interpretation, the library API, limitations, and development.
Usage
Primary: run your program under observation, no code changes needed —
$ python -m metapathology myscript.py --my-args
$ python -m metapathology -m pytest tests/
A report is printed at exit. An installed metapathology command provides a
shorter equivalent:
$ metapathology myscript.py --my-args
Prefer python -m metapathology: it guarantees the hooks land in the same
interpreter and venv as the code under investigation.
Library API, for when a wrapper
isn't possible (notebooks, embedded interpreters, "I can only touch
conftest.py"):
import metapathology
monitor = metapathology.install() # as early as possible
install() is idempotent and returns the process-wide Monitor. By default,
it prints a report to standard error when Python exits. To control when or
where the report is written, disable that callback and report explicitly:
import sys
import metapathology
monitor = metapathology.install(report_at_exit=False)
try:
import package_under_investigation
finally:
metapathology.report(sys.stdout)
metapathology.uninstall()
uninstall() is idempotent. It restores a plain sys.meta_path, removes the
wrappers from finders, and unregisters the exit report. The CPython audit hook
cannot be removed, so it remains installed as an inactive no-op. Recorded
events remain available after uninstalling.
For integration with another diagnostic or test harness:
metapathology.render_report()returns the same report as a string;metapathology.report(file=None)writes it tofile, or to standard error when omitted;metapathology.get_monitor()returns the process-wide monitor, orNonebefore the first call toinstall(); andmonitor.events()returns a capture-order snapshot of the structuredFindSpecCall,MetaPathMutation,MetaPathReassignment, andInternalErrorrecords. Mutating the returned list does not alter the monitor.
Calling report() or render_report() before install() raises
RuntimeError. There are no runtime dependencies. See the complete
usage guide for CLI behavior,
lifecycle details, and integration examples.
Reading the report
The mutation and reassignment sections show how finder precedence changed.
Finder attribution groups recorded find_spec() calls by finder and lists the
modules each finder claimed. The suspicious-findings section uses these
labels:
[bypass]means a custom finder claimed a source module, but a freshPathFinderlookup would choose a different loader or origin. Tools attached throughsys.path_hooksdid not see the import that actually happened.[unfindable]means a custom finder claimed a source module that a freshPathFinderlookup cannot find at all. This is a stronger form of bypass.[no-spec]means a newsys.modulesentry has neither a__spec__nor a recorded finder claim. It was probably created manually or loaded through anexec_module()-style path that is invisible to meta-path finders.
These are diagnostic leads, not necessarily defects. Custom finders may bypass
the standard path machinery intentionally, and the report replays the current
PathFinder state rather than the exact state at import time. The
report guide explains every
section and finding category.
Resource use
The monitor retains every recorded finder call, mutation, reassignment, and
internal error so the final report is exhaustive. Its memory use therefore
grows with import activity for as long as monitoring remains enabled; there is
currently no event limit or silent dropping policy. For a long-running or
import-heavy process, call report() and uninstall() once the behavior of
interest has been captured. Stack traces are stored for sys.meta_path
changes, which makes mutation records more expensive than finder-call records.
See limitations and resource behavior
and the reproducible speed and memory benchmarks
before monitoring a long-running process.
How Python finds an imported module
The detailed import walkthrough connects these Python mechanisms to what metapathology can record.
Python first checks the requested module's fully qualified name in
sys.modules. If an entry is already there, Python returns that same module
object without calling any finder or executing the module again. During a
first import, Python adds the new module to this cache before executing its
code so recursive imports do not load a second copy. If the name is absent,
Python reads sys.meta_path,
a list of objects called finders. It asks each finder in order whether it can
locate the module by calling the finder's find_spec() method.
A finder returns None when it cannot locate the module, and Python moves on
to the next finder. A finder that can locate the module returns a module
spec: a small object describing the module and how to load it. Python stops
asking other finders once it receives a spec. The Python documentation calls
this process the meta path.
One of Python's standard finders is PathFinder. It searches sys.path for a
top-level module or a package's __path__ for a submodule. For each path item,
it uses a cached path-entry finder or calls the factories in sys.path_hooks
to create one. Path hooks therefore do not receive every import themselves.
A custom meta-path finder placed before PathFinder can return a spec first,
preventing PathFinder and the path-entry finders created by those hooks from
seeing the module. See
the path-based finder
for the full protocol.
How it works
metapathology observes that process in three ways:
- It registers a
sys.addaudithook()callback for CPython'simportaudit event. This event says that an uncached import is starting; it does not say which finder will succeed. It also lets the monitor recover if less-common code assigns an entirely new list tosys.meta_path. - It temporarily replaces
sys.meta_pathwith a compatiblelistsubclass. This records the usual changes as they happen: additions, removals, replacements, clearing, and reordering, with a stack trace showing where each change came from. Newly added finders are prepared for call recording. - It wraps each finder's existing
find_spec()method. The wrapper records whether the finder returnedNoneor a module spec, then returns the same result. It does not supply a spec of its own or load a module.
The standard BuiltinImporter, FrozenImporter, and PathFinder entries are
classes shared by CPython, so metapathology deliberately leaves them unwrapped.
At exit, the report compares the recorded result with what
PathFinder.find_spec() finds. If PathFinder cannot find the module or would
use a different kind of loader, the report notes that the normal
sys.path_hooks route was skipped.
Caveats
- CPython only (relies on the
importaudit event and import-system internals). - Monitoring begins when
metapathologyis installed. Finders added earlier by.pthfiles are shown in the initialsys.meta_pathlist, but there can be no stack trace for when they were added because that happened during Python startup. - The temporary changes to finders and
sys.meta_pathare reversed byuninstall(). Python does not provide a way to remove an audit hook, so the installed callback remains as an inactive no-op after uninstalling. - This tool changes
sys.meta_pathwhile it is running. Use it for debugging, not as part of an application's normal runtime.
See the complete limitations guide for timing, visibility, replay, cleanup, and memory boundaries.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file metapathology-0.2.1.tar.gz.
File metadata
- Download URL: metapathology-0.2.1.tar.gz
- Upload date:
- Size: 19.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21fa3fe9cd9c05d5e5b58dca0220773a21ceae89a7e92a1b29671ecbec0860f6
|
|
| MD5 |
01455f7e7cda8e2826433f777bff869f
|
|
| BLAKE2b-256 |
217eed04b2cc1d98a926ebe8f184dddfe2276d989649d404fffee903d8bd09f9
|
File details
Details for the file metapathology-0.2.1-py3-none-any.whl.
File metadata
- Download URL: metapathology-0.2.1-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
806e549112370bce6ac51bc1b4d2dc073edbc60b13d6a2de822847b01975b45f
|
|
| MD5 |
612b3bc009eff94fe425d61d17f88655
|
|
| BLAKE2b-256 |
97b06f56523f95ee303102c84ba098ae2b0ab9e778b6e879fe14b59d6d55e6ce
|