Development¶
The repository uses uv and keeps runtime code stdlib-only.
Set up and check a change¶
uv sync --all-groups
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run basedpyright
uv run pyrefly check
Run repository hooks before committing:
Import monitoring changes often require a fresh interpreter because audit
hooks cannot be removed and import state is global. Prefer subprocess tests
for behavior that calls install().
Core invariants¶
Changes must preserve these properties:
- Never claim or load a module; record and delegate only.
- Keep hooks and finder wrappers re-entrancy guarded.
- Import everything needed by a hot path before that path runs.
- Never stringify foreign objects while recording an import event.
- Hold the shared state lock only for plain-data reads and writes, never while running code that could import.
- Make all reversible instrumentation pristine again in
uninstall(). - Keep reporting safe under concurrent imports and malformed module state.
The complete project rules live in the repository's
AGENTS.md.
Tests¶
Use fully annotated tests and real finders, modules, files, and subprocesses where practical. A bug fix starts with a reproducer. Generated mutation sequences should assert invariants after each operation and remain reproducible from the failing Hypothesis example or seed.
Use snapshot or golden tests only when the complete reviewed text is itself a compatibility contract. Otherwise prefer semantic assertions.
Performance benchmarks¶
Run the startup, import-throughput, mutation, report-rendering, and memory benchmark from the repository root:
The script uses fresh target-interpreter processes for every sample and writes
raw JSON, a Markdown summary, plus import and sys.meta_path mutation graphs beneath
.cache/metapathology-benchmarks/. Use --quick for a smoke run, or customize
the workload and interpreter, for example:
Fresh-process timings also cover bare interpreter startup, package import,
deferred monitor-API import, direct script execution, and the monitored CLI
wrapper. Timing and memory are collected in separate trials so tracemalloc does not
distort the speed measurements. Workers use python -S and disable bytecode
writes to exclude interpreter-specific .pth finders and cache-warming order
effects. They use __import__() rather than importlib.import_module() so the
workload crosses the builtin audit boundary. The native scenario therefore
measures the controlled standard-finder path with one retained audit-start
record per synthetic import;
attributed installs the same delegating instance finder in both control and
monitored processes so every synthetic import exercises both an audit-start
and a retained finder-call record. Pass --include-deep to add deep, which
enables every opt-in deep diagnostic around the controlled standard-finder
path. Each monitored sample separately
renders a JSON report after the workload, measuring report-time analysis
without folding it into throughput. Mutation samples perform repeated
pop/append pairs and therefore include the monitor's intentional
stack-capture cost. Trials are
shuffled to balance system warm-up; --seed controls and records that order.
Use --deep-only for the standalone deep workflow without repeating the
default scenarios.
Finder wrappers capture the finder's id and display name once rather than
allocating them for every recorded probe. Search-path snapshots remain exact,
but identity-equal immutable tuples share storage through a fixed eight-entry
least-recently used cache. Identity comparison avoids invoking foreign equality
code in the import hot path; bounded eviction prevents the cache itself from
becoming another unbounded producer. Cache access uses the existing record
lock, and uninstall() clears it without affecting retained event snapshots.
Documentation¶
Update the closest discovery surface in the same change as public behavior:
- CLI behavior belongs in help and Using metapathology.
- Report vocabulary belongs in Reading the report.
- Public Python behavior belongs in Library API.
- Architectural invariants belong in
AGENTS.mdand, when useful to users, How it works.
Build the documentation locally with the repository's Zensical configuration:
CI performs a strict build for every pull request and every push to main,
deploys successful main builds to GitHub Pages, and repeats the build every
six hours to catch toolchain or hosting regressions. An agent-authored
freshness job that edits documentation and opens pull requests is deliberately
not preconfigured: it needs an explicitly chosen model provider, repository
write policy, and secret before it can be secure and functional.
Development dependencies need a recorded justification. Runtime dependencies
are not permitted. Temporary compatibility code must include a TODO with a
specific removal trigger such as a supported Python version, dependency
version, or date.
Real freezer smoke tests are opt-in because they download large build tools, invoke native compilers, and take substantially longer than the stdlib-only suite. Run the currently reviewed toolchain with:
METAPATHOLOGY_TEST_FREEZERS=1 uv run \
--with pyinstaller==6.21.0 \
--with cx-freeze==8.6.4 \
--with nuitka==4.1.3 \
pytest -m freezer tests/test_freezer_integrations.py
These packages are isolated additions for the command and are not project or runtime dependencies. The fixtures build PyInstaller directory/one-file, Nuitka standalone/one-file, and cx_Freeze directory executables, then assert semantically on text reports written inside those interpreters. Freezer builds remain platform-specific; passing locally does not claim another platform.
When releasing, update both project.version in pyproject.toml and
metapathology.__version__. The package test requires them to match. Keeping
the small duplication avoids importing importlib.metadata during every
package import and CLI invocation.
The private _InstrumentedMetaPath.__iadd__ and __imul__ annotations
deliberately return the concrete class while Python 3.10 remains supported.
This avoids adding typing_extensions solely for two private annotations.
When the minimum supported Python becomes 3.11, restore the more precise
standard-library typing.Self annotation.
Runtime modules assign TYPE_CHECKING = False directly instead of importing
it from typing. Basedpyright and Pyrefly both recognize the conventional name
as true during analysis. Keep type-only annotations quoted and type-only
imports inside those blocks so runtime startup does not import typing.
The development commands above are documented by pytest, Ruff, basedpyright, Pyrefly, and prek.
Commit subjects should be concise, imperative, and scoped. Explain the user-visible problem or invariant, why the approach fits, and meaningful tradeoffs or follow-up triggers in the body rather than narrating the diff.