#!/usr/bin/env python3 """Child interpreter for ``import_io_detector.audit_reads``. Import ``MODULE_PATH`` (argv[1]) with an audit hook on CPython's ``open`` event and print ONE json object on stdout:: {"target": "", "events": [{"path": ..., "mode": ..., "flags": ...}], "import_error": null | "Type: message"} Exit is 0 whenever that json was printed -- an import that raised is *reported* in ``import_error``, not treated as a probe failure, because a mutant may raise after the read and the read is what is being graded. Why a child process: ``sys.addaudithook`` cannot be uninstalled, so an in-process hook would keep watching the harness for the rest of its life. What is filtered, and why (each rule is a claim, stated so it can be argued with): * the target module's own source and bytecode -- the import itself * ``.py``/``.pyc``/``.pyo``/``.pyd``/``.so`` open from a directory on ``sys.path`` or under the interpreter's prefix -- the import machinery loading code, not the module reading data * opens whose mode/flags are write-only -- this grades reads; ``sys.dont_write_bytecode`` is set so a ``__pycache__`` write would not even be attempted Everything else is reported: a data file, a config, a seed, whatever spelling of ``open`` produced it. """ from __future__ import annotations import importlib.util import json import os import sys sys.dont_write_bytecode = True TARGET = os.path.realpath(sys.argv[1]) CODE_EXT = (".py", ".pyc", ".pyo", ".pyd", ".so") _DIRS = sorted({ os.path.realpath(d) for d in [*sys.path, sys.base_prefix, sys.prefix] if d and os.path.isdir(d) }) def _is_code(path: str) -> bool: """True when this open is the import machinery loading code.""" real = os.path.realpath(path) if real in (TARGET, TARGET + "c"): return True if real.endswith(CODE_EXT): return any(real == d or real.startswith(d + os.sep) for d in _DIRS) return False def _is_write(mode, flags) -> bool: """True when the open cannot be a read (so it is out of this probe's scope).""" if isinstance(mode, str) and mode: return any(c in mode for c in "wax") try: value = int(flags) except (TypeError, ValueError): return False return bool(value & (os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_APPEND)) def main() -> int: events: list = [] def hook(event, args): if event != "open": return path = args[0] if args else None mode = args[1] if len(args) > 1 else None flags = args[2] if len(args) > 2 else None if path is None or _is_write(mode, flags): return try: spelling = os.fspath(path) except TypeError: spelling = str(path) if isinstance(spelling, bytes): spelling = spelling.decode("utf-8", "replace") if _is_code(spelling): return events.append({ "path": spelling, "mode": mode if isinstance(mode, str) else None, "flags": flags if isinstance(flags, int) else None, }) sys.addaudithook(hook) error = None try: spec = importlib.util.spec_from_file_location("_audited_import", TARGET) module = importlib.util.module_from_spec(spec) sys.modules["_audited_import"] = module spec.loader.exec_module(module) except BaseException as exc: # report, do not grade, the error error = "%s: %s" % (type(exc).__name__, exc) json.dump({"target": TARGET, "events": events, "import_error": error}, sys.stdout) sys.stdout.write("\n") return 0 if __name__ == "__main__": sys.exit(main())