"""Composition read boundary, independent of optional audition/reference tools.""" from __future__ import annotations import contextlib import contextvars import os import pathlib import sys ACTIVE = contextvars.ContextVar('composition_reads', default=None) def _read(event, args): scope = ACTIVE.get() if scope is None or event != 'open': return name, mode, flags = args if not isinstance(name, (str, bytes, os.PathLike)): return # r+ and w+ can read; they must obey the same boundary as a plain read. if flags & os.O_WRONLY: return path = pathlib.Path(os.fsdecode(name)).resolve() data, code, cache, reads = scope python_runtime = any(path.is_relative_to(pathlib.Path(root).resolve()) for root in (sys.base_prefix, sys.prefix)) allowed = path.is_relative_to(data) or path.is_relative_to(code) or python_runtime if cache and path.is_relative_to(cache): allowed = True if not allowed: raise ValueError('Forbidden composition input: ' + str(path)) reads.add(str(path)) sys.addaudithook(_read) @contextlib.contextmanager def composition(data, role, cache=None): reads = set() code = pathlib.Path(__file__).resolve().parent token = ACTIVE.set((pathlib.Path(data).resolve(), code, pathlib.Path(cache).resolve() / role if cache else None, reads)) try: yield reads finally: ACTIVE.reset(token)