Skip to content

Library API

The public package is typed and exposes the following names. Prefer the CLI workflow unless the target cannot be wrapped. The package's __all__ declaration is the machine-readable source of truth for the supported public API.

Lifecycle and reporting

activate_frozen(integration, bootstrap_path) -> None

Installs the process-wide monitor and records activation inside a supported frozen or embedded startup boundary. integration accepts "pyinstaller", "nuitka", "cx-freeze", or "embedded". Prefer the generated and fail-open startup files described in the frozen application guide over calling this function directly. Direct calls propagate invalid integration and installation errors to the application.

install(*, report_at_exit=True, report_destination=None, report_text=None, report_json=None, report_color=None, monitor_path_hooks=None, monitor_importer_cache=None, monitor_sys_path=None, deep=None, deep_path_hooks=None, deep_path_entry_finders=None, deep_loaders=None, deep_import_outcomes=None, deep_import_calls=None, speculative_replay=None) -> Monitor

Installs the process-wide monitor and returns it. Repeated calls return and enable the same monitor. Only activity after installation can be observed. When report_at_exit is true, a report is registered using Python's atexit mechanism. report_destination, report_text, and report_json each accept one path or a list of paths. report_destination infers JSON from .json, text from .txt or .text, and text for the - standard-error sentinel; other extensions require the format-specific arguments. When no API destination is supplied, METAPATHOLOGY_REPORT is consulted as an os.pathsep-separated destination list before defaulting to one text report on standard error. All targets share one captured report artifact. report_color accepts "auto", "always", or "never" for automatic text reports. API values override METAPATHOLOGY_COLOR; the default "auto" colors TTY destinations unless NO_COLOR is nonempty or TERM=dumb. JSON never contains ANSI escapes.

monitor_path_hooks controls path-hook observation and defaults to true. A later true value enables it if initially disabled; false does not disable an active mechanism. Use uninstall() for cleanup. monitor_importer_cache has the same enable-later semantics and controls passive sys.path_importer_cache snapshots and diffs. monitor_sys_path opts into reversible list-mutation attribution and import-boundary reassignment detection for sys.path; it defaults to false unless deep=True and can also be set with METAPATHOLOGY_MONITOR_SYS_PATH.

deep=True enables all five delegated deep mechanisms and sys.path monitoring. Each deep_* argument can override the umbrella independently for delegated path hooks, mutable path-entry finders, mutable loaders, exact import outcomes, or builtins.__import__ calls. deep_import_calls wraps __import__ to observe every import statement, including sys.modules cache hits that no other mechanism sees; the swap is chain-safe against other tools that also wrap __import__, and importlib.import_module() bypasses it. Path-hook wrapping changes callable identity; deep mechanisms put monitor code inline with imports and should be reserved for controlled diagnostic runs.

speculative_replay is independent of deep. When enabled (or via METAPATHOLOGY_SPECULATIVE_REPLAY), report generation replays a finder that a sys.path_importer_cache change displaced for a path entry against a module that later failed to resolve on that path, reporting whether the retained finder returns a spec now. It performs at most one foreign find_spec() per selected candidate, caps the report at 16 probes, never mutates import state, declines reload-target lookups, and never claims the original import would have succeeded. Because it recomputes each report, repeated reports repeat those foreign calls.

Capture booleans resolve consistently: an explicit API value wins, then its METAPATHOLOGY_* environment value, then the documented default. Accepted environment booleans are 1/0, true/false, yes/no, and on/off (case-insensitive). report_at_exit remains API-only because it controls callback ownership rather than captured evidence. Early bootstrap activation remains environment-only because it must run before this API is importable.

Automatic reports always include the current process ID in their filename. For process 1234, report.json becomes report.1234.json. When the configured path contains {pid}, that marker is replaced instead, so report-{pid}.json becomes report-1234.json. Explicit write_report() paths are used unchanged. Write failures are recorded and suppressed by automatic reporting so they do not replace the target's exit status.

monitoring(*, monitor_path_hooks=None, monitor_importer_cache=None, monitor_sys_path=None, deep=None, deep_path_hooks=None, deep_path_entry_finders=None, deep_loaders=None, deep_import_outcomes=None, deep_import_calls=None, speculative_replay=None) -> ContextManager[Monitor]

Defines a bounded monitoring region and yields the process-wide monitor. The monitor is installed on entry and, if the region began from an inactive state, uninstalled after the last nested or overlapping region exits. Cleanup also runs when the block raises. If a monitor was installed explicitly before the first region, monitoring() emits a RuntimeWarning and leaves it active afterward; this makes the likely lifecycle mismatch visible without tearing down instrumentation the region does not own.

Nested regions share the monitor. A nested region may enable another mechanism, but mechanisms are not selectively disabled when that inner region exits; they remain enabled until the shared installation ends. This matches the monotonic enable-later behavior of install().

The context manager does not register an atexit report because its cleanup boundary occurs before process exit. Recorded evidence remains available after the block, so call write_report() or render_report() afterward. Avoid mixing manual uninstall() calls into active regions because manual lifecycle control cannot participate in region ownership.

uninstall() -> None

Disables monitoring, restores plain sys.meta_path, sys.path_hooks, and any instrumented sys.path lists, removes shadows from instrumented finders, and unregisters the exit callback. Repeated calls are safe. The CPython audit callback cannot be removed and remains as an inactive no-op.

get_monitor() -> Monitor | None

Returns the process-wide monitor, or None if install() has never been called.

write_report(destination=None, *, format="text", color="auto") -> None

Writes the current report to standard error, a supplied text stream, or a string/path-like file destination. Paths are exact for explicit calls and are written through a same-directory temporary file plus os.replace(). Explicit I/O failures are recorded as InternalError and re-raised. Raises RuntimeError before the first installation and ValueError for an unknown format or color mode. In "auto", file paths and non-TTY streams remain plain; "always" deliberately permits ANSI escapes in files.

render_report(*, format="text", color=False) -> str

Returns text or JSON, including its trailing newline. JSON uses the stable metapathology.report schema version 2.0. The bundled metapathology/report.schema.json file defines its language-neutral shape; metapathology.ReportJSON and metapathology.ReportStatus expose the Python typing contract without eagerly importing the reporting implementation. Returned text is plain by default because it has no destination to inspect; pass color=True for ANSI styling. The flag has no effect on JSON. Raises RuntimeError before the first installation and ValueError for an unknown format. Ordinary generation failures degrade to a valid failure report rather than propagating.

Monitor

Applications receive a Monitor from install(); directly constructing competing monitors is not supported because import state is process-global.

  • enabled: bool — whether observation is currently active.
  • initial_meta_path: tuple[str, ...] — finder display names at installation.
  • path_hooks_enabled: bool — whether path-hook observation is currently active.
  • initial_path_hooks: tuple[ObjectRef, ...] — identities and safe type/name metadata captured when path-hook monitoring was enabled.
  • importer_cache_enabled: bool — whether passive importer-cache observation is currently active.
  • sys_path_enabled: bool — whether opt-in sys.path mutation observation is currently active.
  • deep_diagnostics: tuple[str, ...] — explicitly enabled inline delegation mechanisms.
  • standard_finder_status: str — whether exact PathFinder result capture (deep_import_outcomes) is active, unavailable, unsupported, or inactive after cleanup.
  • initial_importer_cache: tuple[ImporterCacheEntry, ...] — string-keyed cache entries captured when importer-cache monitoring was enabled.
  • baseline_modules: frozenset[str]sys.modules names at installation.
  • events() -> list[MonitorEvent] — capture-order snapshot of all records.
  • skipped_finders() -> list[tuple[str, str]] — finder display name and the reason it could not be wrapped. The normal CPython class finders are included with an explicit "expected" reason; see Reading the report for why they are deliberately left unchanged.

Lifecycle belongs to the module-level install(), uninstall(), and monitoring() APIs. Monitor exposes captured evidence but does not remember automatic report destinations, formats, or atexit policy.

Event records

All records are immutable, slotted classes with a standard field-based repr. They deliberately use identity equality and do not define positional pattern matching. Their shared seq field provides a single chronological order across record types. MonitorEvent includes the event classes below.

DeepDiagnosticCall

Records one opt-in deep-diagnostic call with the mechanism, safe object identity/type, module or path, outcome, exception type, and thread. The unobserved_reentrant outcome means a nested call delegated normally while the per-thread guard suppressed exact nested instrumentation. Modern mutable loaders use loader_create_module and loader_exec_module; absent methods are not added, and legacy load_module is not wrapped. Loader records also carry the target's ModuleCacheState at entry and return or exception exit.

StandardFinderCall

Records a PathFinder result captured by the opt-in reversible profiler (deep_import_outcomes). It links the SpecSummary to the exact import attempt and thread without modifying the shared PathFinder class. No record is created when the profiler could not be activated; consult Monitor.standard_finder_status and the report's stated fallback.

ImportAuditStart

Records that CPython began uncached resolution for a module, together with the thread name, immediate sys.meta_path identity and finder type names, and constant-size identities/fingerprints from enabled path-hook and importer-cache monitoring. It does not record an import outcome; success and failure remain unknown without separate evidence.

ObjectRef

Carries an observed object's numeric identity, safe type name, and optional callable name. It never retains or stringifies the foreign object.

ModuleCacheState

Carries one target name's constant-size sys.modules state: unavailable, missing, explicit none, or object with a numeric identity and safe type name. Dictionary subclasses are read through the built-in dict implementation so overridden mapping methods do not run. Non-dictionary cache replacements are not probed.

FindSpecCall

Records the module name, finder type and identity, whether the finder found the module, loader type, origin, captured search path and whether it represented sys.path or a parent package path, a SpecSummary, the exception type if the finder raised, and the thread name. The record also carries module_state_before and module_state_after, captured around the wrapped call. A changed pair does not reconstruct nested import activity or temporary intermediate objects.

SpecSummary

Contains only plain captured spec semantics: safe spec/loader identity, origin, cached path, package and namespace status, copied search locations, and explicit completeness state. Exact list and tuple locations are captured during the finder call. Foreign location sequences are not iterated inside an import.

ImporterCacheEntry and ImporterCacheReplacement

An entry stores a path and either an ObjectRef or None; None explicitly represents a negative cache entry. A replacement stores the path and its before/after cached values.

ImporterCacheDiff

Records additions, removals, replacements, omitted non-string-key counts, the passive observation boundary, and thread name. It participates in the shared event sequence.

MetaPathMutation

Records the list operation, added and removed finder display names, resulting contents, thread name, and captured stack.

MetaPathReassignment

Records the import during which replacement was detected, old and new finder display names, triggering thread, and detection stack. The fields describe detection time rather than the unknowable assignment moment.

PathHooksMutation

Records the operation, added and removed ObjectRef values, resulting hook order, thread, and mutation stack. Monitoring never calls the hooks.

PathHooksReassignment

Records old and new hook references plus the import, thread, and stack at the next import audit event that detected direct replacement.

InternalError

Records the instrumentation location, exception type, and optional sanitized message. Monitor-generated records leave the message unset when foreign error stringification would be unsafe.

Record fields contain primitive snapshots rather than live foreign import objects. See How it works for why this is necessary inside the import machinery.