Filesystem & OS
Mounting host directories into the sandbox, and handling the OS calls sandboxed code makes. See filesystem access for how these fit together.
A mount point mapping a virtual path to a host directory.
The directory is opened here, and every feed this mount is passed to serves
that same directory — so build one and reuse it. 'overlay' writes live in
each feed’s own table and are discarded when the feed ends.
Warning: mode='read-write' writes files from untrusted code to your
real filesystem.
Those files are untrusted input; do not execute them. Importing counts as
executing, and the import can be indirect: with a directory on sys.path
mounted, sandboxed code can write json.py, or any module not yet
imported, and the next import runs it. That includes imports made by
pydantic_monty itself. sys.path[0] is the script’s directory, or the
cwd for python -m, python -c and the REPL.
Tools also read files without an explicit import: conftest.py,
sitecustomize.py, .git/hooks/*, Makefile, .env, __pycache__.
The 'overlay' default keeps writes in memory, so nothing reaches the host
filesystem. Use 'read-write' only with a directory that contains no code
or config and is not on sys.path or any other execution path.
from pathlib import Path
from pydantic_monty import Monty, MountDir
with Monty() as pool, MountDir(host_path=Path('host-dir'), virtual_path='/data') as mount:
with pool.checkout() as session:
contents = session.feed_run("open('/data/notes.txt').read()", mount=mount)
The directory is held open from construction until the mount is closed, not
for the duration of a feed — so reusing one across feeds is free, and works
the same inside a with block or outside it. with (or close()) is what
hands the directory back; feeds passed a closed mount raise ValueError.
Without it the directory stays open until the object is collected: a file
descriptor on Unix, but on Windows that blocks renaming or deleting it.
def __new__(
cls,
*,
host_path: str | Path,
virtual_path: str,
mode: Literal['read-only', 'read-write', 'overlay'] = 'overlay',
write_bytes_limit: int | None = None,
memory_usage_limit: int = 100000000,
) -> MountDir
Configure a mount point; validation happens here, not at feed time.
All arguments are keyword-only: mount tools disagree on host-first
(docker -v) vs virtual-first (nginx alias) ordering, so requiring
names removes the ambiguity.
host_path : str | Path
Real host directory to expose. Opened at construction;
raises if it doesn’t exist, isn’t a directory, or cannot be
opened — on macOS/BSD a search-only (0o111) directory is not
mountable, though Linux accepts one. Sandbox code can never see
this path or reach outside it. The mount tracks the directory
itself rather than its name, so renaming it on the host does
not detach the mount; on Windows the open handle prevents the
host renaming or deleting it at all while the mount lives.
Symlinks inside it are followed only if their targets are
relative — an absolute target raises PermissionError in the
sandbox even when it points back into the same mount.
virtual_path : str
Absolute POSIX-style path prefix inside the sandbox
(e.g. '/data'), regardless of host OS. Raises TypeError
if not absolute.
mode : Literal[‘read-only’, ‘read-write’, ‘overlay’] Default: 'overlay'
'read-only' — reads only, writes raise PermissionError;
'read-write' — writes through to the host directory, where
the files persist after the feed (see the warning above);
'overlay' (default) — reads fall through to the host, writes
are captured in memory per feed and discarded when it ends.
Cap on cumulative bytes written through the
mount within one feed; exceeding it raises OSError in the
sandbox. None (default) means unlimited.
memory_usage_limit : int Default: 100000000
Per-mount budget in bytes (default 100 MB,
matches DEFAULT_MEMORY_USAGE_LIMIT in rust) shared by retained
overlay data and transient filesystem results; an operation that
would exceed it raises MemoryError in the sandbox.
def close() -> None
Release the open host directory. Idempotent.
Feeds passed this mount afterwards raise ValueError; the attributes
above keep answering. Only Windows needs this — it refuses to rename or
delete a directory while a handle to it is open — but MountDir also
works as a context manager, which closes on exit.
Bases: AbstractOS
In-memory virtual filesystem for sandboxed Monty execution.
OSAccess provides a complete virtual filesystem that Monty code can interact
with via pathlib.Path methods. Files exist only in memory (when using
MemoryFile) and cannot access the real filesystem. Environment access is
isolated to the provided environ mapping. date.today() and
datetime.now() default to the host clock; override those methods in a
subclass if you need a deterministic or virtual clock.
List of AbstractFile objects registered with this filesystem.
Type: list[AbstractFile]
Dictionary of environment variables accessible via os.getenv().
def __init__(
files: Sequence[AbstractFile] | None = None,
environ: dict[str, str] | None = None,
*,
root_dir: str | PurePosixPath = '/',
)
Create a virtual filesystem with the given files.
files : Sequence[AbstractFile] | None Default: None
Files to register in the virtual filesystem. Use MemoryFile
for sandboxed in-memory files, or CallbackFile for custom logic
(with security caveats - see its docstring).
Environment variables accessible to Monty code via os.getenv(). Isolated from the real environment.
root_dir : str | PurePosixPath Default: '/'
Base directory for normalizing relative file paths. Relative paths in files will be prefixed with this. Default is ’/’.
AssertionError— If root_dir is not an absolute path.ValueError— If a file path conflicts with another file (e.g., trying to create a file inside another file’s path).
def path_open(path: PurePosixPath, mode: str) -> MontyFileHandle
Perform the open(path, mode) open-time effect against the in-memory tree.
'r'/'rb': verify the file exists and is not a directory; raiseFileNotFoundError/IsADirectoryErrorif not.'w'/'wb': truncate (or create empty) via_write_file.'a'/'ab': create the file if missing; leave existing content untouched. RaisesIsADirectoryErrorif the path is a directory.
The returned MontyFileHandle carries the canonicalized mode
('rt' → 'r', 'r+b' → 'rb+') and becomes the first argument
of any subsequent read/write/append OS calls Monty issues for this
file.
The handle is constructed before any side effect so that a
malformed mode raises ValueError without touching the
filesystem. Direct callers (not routed through Monty, which
pre-validates) could otherwise pass e.g. 'wxyz' and silently
trigger the truncate/create branch before the eventual mode-parse
failure.
Bases: ABC
Abstract base class for implementing virtual filesystems and host OS access.
Subclass this and implement the abstract methods to provide a custom
filesystem and selected host-backed operations that Monty code can interact
with via pathlib.Path, os, date.today(), and datetime.now().
Pass an instance as the os parameter to Monty.run().
def __call__(
function_name: OsFunction,
args: tuple[Any, ...],
kwargs: dict[str, Any] | None = None,
) -> Any
Adapter used by Monty’s os= callback surface.
Monty calls __call__ directly, so this method stays as the public
callable entrypoint. Override dispatch() when you want to customize
routing or return NOT_HANDLED.
Any — The OS operation result, or NOT_HANDLED to let Monty apply its
Any — standard unhandled-operation behavior.
def dispatch(
function_name: OsFunction,
args: tuple[Any, ...],
kwargs: dict[str, Any] | None = None,
) -> Any
Dispatch an OS operation to the appropriate method.
This handles Monty’s built-in pathlib.Path, os, and host clock
operations. Subclasses can override it for custom behavior or return
NOT_HANDLED to delegate back to Monty’s default fallback errors.
Any — The result of the OS operation.
function_name : OsFunction
The OS operation being called (e.g., ‘Path.exists’).
The arguments passed to the method.
The keyword arguments passed to the method.
@abstractmethod
def path_exists(path: PurePosixPath) -> bool
Check if a path exists.
bool — True if the path exists, False otherwise.
The path to check.
@abstractmethod
def path_is_file(path: PurePosixPath) -> bool
Check if a path is a regular file.
bool — True if the path is a regular file, False otherwise.
The path to check.
@abstractmethod
def path_is_dir(path: PurePosixPath) -> bool
Check if a path is a directory.
bool — True if the path is a directory, False otherwise.
The path to check.
@abstractmethod
def path_is_symlink(path: PurePosixPath) -> bool
Check if a path is a symbolic link.
bool — True if the path is a symbolic link, False otherwise.
The path to check.
def path_open(path: PurePosixPath, mode: str) -> MontyFileHandle
Perform the open-time effect for open(path, mode).
Monty issues this OS call from the open() builtin. The handler is
responsible for the side-effect that matches mode and for returning
a MontyFileHandle Monty can wrap into an _io.* file object:
'r'/'r+'(text/binary): verify the file exists and is not a directory; raiseFileNotFoundError/IsADirectoryErrorif not.'w'/'w+': truncate the file (creating it if missing).'a'/'a+': create the file if missing, leaving existing content untouched.
The returned MontyFileHandle becomes the first argument of any
subsequent Path.read_text / Path.write_text / Path.append_text
(and bytes variants) OS calls; dispatch() normalizes it back to the
underlying PurePosixPath so the existing path_* handlers continue
to work without modification.
@abstractmethod
def path_read_text(path: PurePosixPath | MontyFileHandle) -> str
Read the contents of a file as text.
str — The file contents as a string.
path : PurePosixPath | MontyFileHandle
The path to the file, either as a PurePosixPath (from
pathlib.Path methods) or a MontyFileHandle (after the
file was opened via open()). Use path_from_arg() to
collapse both shapes if you don’t need the extra handle
metadata.
FileNotFoundError— If the file does not exist.IsADirectoryError— If the path is a directory.
@abstractmethod
def path_read_bytes(path: PurePosixPath | MontyFileHandle) -> bytes
Read the contents of a file as bytes.
bytes — The file contents as bytes.
path : PurePosixPath | MontyFileHandle
The path to the file, either as a PurePosixPath (from
pathlib.Path methods) or a MontyFileHandle (after the
file was opened via open()). Use path_from_arg() to
collapse both shapes if you don’t need the extra handle
metadata.
FileNotFoundError— If the file does not exist.IsADirectoryError— If the path is a directory.
@abstractmethod
def path_write_text(path: PurePosixPath | MontyFileHandle, data: str) -> int
Write text data to a file.
int — The number of characters written.
path : PurePosixPath | MontyFileHandle
The path to the file, either as a PurePosixPath (from
pathlib.Path methods) or a MontyFileHandle (after the
file was opened via open()). Use path_from_arg() to
collapse both shapes if you don’t need the extra handle
metadata.
data : str
The text content to write.
FileNotFoundError— If the parent directory does not exist.IsADirectoryError— If the path is a directory.
@abstractmethod
def path_write_bytes(path: PurePosixPath | MontyFileHandle, data: bytes) -> int
Write binary data to a file.
int — The number of bytes written.
path : PurePosixPath | MontyFileHandle
The path to the file, either as a PurePosixPath (from
pathlib.Path methods) or a MontyFileHandle (after the
file was opened via open()). Use path_from_arg() to
collapse both shapes if you don’t need the extra handle
metadata.
data : bytes
The binary content to write.
FileNotFoundError— If the parent directory does not exist.IsADirectoryError— If the path is a directory.
def path_append_text(path: PurePosixPath | MontyFileHandle, data: str) -> int
Append text data to a file and return the number of characters written.
Accepts either a PurePosixPath or a MontyFileHandle (see
path_read_text for details).
def path_append_bytes(path: PurePosixPath | MontyFileHandle, data: bytes) -> int
Append binary data to a file and return the number of bytes written.
Accepts either a PurePosixPath or a MontyFileHandle (see
path_read_text for details).
@abstractmethod
def path_mkdir(path: PurePosixPath, parents: bool, exist_ok: bool) -> None
Create a directory.
The path of the directory to create.
parents : bool
If True, create parent directories as needed.
exist_ok : bool
If True, don’t raise an error if the directory exists.
FileNotFoundError— If parents is False and parent directory doesn’t exist.FileExistsError— If exist_ok is False and the directory already exists.
@abstractmethod
def path_unlink(path: PurePosixPath) -> None
Remove a file.
The path to the file to remove.
FileNotFoundError— If the file does not exist.IsADirectoryError— If the path is a directory.
@abstractmethod
def path_rmdir(path: PurePosixPath) -> None
Remove an empty directory.
The path to the directory to remove.
FileNotFoundError— If the directory does not exist.NotADirectoryError— If the path is not a directory.OSError— If the directory is not empty.
@abstractmethod
def path_iterdir(path: PurePosixPath) -> list[PurePosixPath]
List the contents of a directory.
list[PurePosixPath] — A list of full paths (as PurePosixPath) for entries in the directory.
The path to the directory.
FileNotFoundError— If the directory does not exist.NotADirectoryError— If the path is not a directory.
@abstractmethod
def path_stat(path: PurePosixPath) -> StatResult
Get file status information.
Use file_stat(), dir_stat(), or symlink_stat() helpers to create the return value.
StatResult — A StatResult with file metadata.
The path to stat.
FileNotFoundError— If the path does not exist.
@abstractmethod
def path_rename(path: PurePosixPath, target: PurePosixPath) -> None
Rename a file or directory.
The current path.
The new path.
FileNotFoundError— If the source path does not exist.FileExistsError— If the target already exists (platform-dependent).
@abstractmethod
def path_resolve(path: PurePosixPath) -> str
Resolve a path to an absolute path, resolving any symlinks.
str — The resolved absolute path with symlinks resolved.
The path to resolve.
@abstractmethod
def path_absolute(path: PurePosixPath) -> str
Convert a path to an absolute path without resolving symlinks.
str — The absolute path.
The path to convert.
@abstractmethod
def getenv(key: str, default: str | None = None) -> str | None
Get an environment variable value.
str | None — The value of the environment variable, or default if not set.
key : str
The name of the environment variable.
The value to return if the environment variable is not set.
@abstractmethod
def get_environ() -> dict[str, str]
Get the entire environment as a dictionary.
dict[str, str] — A dictionary containing all environment variables.
def date_today() -> datetime.date
Return today’s date for Monty’s date.today() host callback.
Override this when the sandbox should observe a virtual or fixed clock. The default implementation proxies to the host Python process.
def datetime_now(tz: datetime.tzinfo | None = None) -> datetime.datetime
Return the current datetime for Monty’s datetime.now(tz=...) callback.
Override this when the sandbox should observe a virtual or fixed clock.
The default implementation proxies to the host Python process and passes
any provided timezone through to datetime.datetime.now().
Bases: Protocol
Protocol defining the interface for files used with OSAccess.
This protocol allows custom file implementations to be used with OSAccess. The built-in implementations are:
MemoryFile: Stores content in memory (recommended for sandboxed execution)CallbackFile: Delegates to custom callbacks (use with caution - see its docstring)
The virtual path of the file within the OSAccess filesystem.
Type: PurePosixPath
The filename (basename) extracted from path.
Type: str
Unix-style permission bits (e.g., 0o644).
Type: int
Whether the file has been marked as deleted.
Type: bool
def read_content() -> str | bytes
Read and return the file’s content.
def write_content(content: str | bytes) -> None
Write content to the file.
def delete() -> None
Mark the file as deleted.
An in-memory virtual file for use with OSAccess.
This is the recommended file type for sandboxed Monty execution. Content is stored entirely in Python memory with no access to the real filesystem.
When Monty code reads from this file, it receives the stored content. When Monty code writes to this file, the content attribute is updated.
Example::
from pydantic_monty import Monty, OSAccess, MemoryFile
fs = OSAccess( [ MemoryFile(‘/config.json’, ’{“debug”: true}’), MemoryFile(‘/data.bin’, b’\x00\x01\x02’), ] )
result = Monty(''' from pathlib import Path Path(‘/config.json’).read_text() ''').run(os=fs)
The virtual path of the file within the OSAccess filesystem.
Type: PurePosixPath
The filename (basename) extracted from path.
Type: str
The file content (str for text, bytes for binary).
Unix-style permission bits (default: 0o644).
Type: int
Whether the file has been marked as deleted.
Type: bool
def __init__(
path: str | PurePosixPath,
content: str | bytes,
*,
permissions: int = 420,
) -> None
Create an in-memory virtual file.
path : str | PurePosixPath
The virtual path for this file in the OSAccess filesystem.
The initial file content (str for text, bytes for binary).
permissions : int Default: 420
Unix-style permission bits (default: 0o644).
def read_content() -> str | bytes
Return the stored content.
def write_content(content: str | bytes) -> None
Update the stored content.
def delete() -> None
Mark the file as deleted.
A virtual file backed by custom read/write callbacks.
This class allows you to create files whose content is dynamically generated or persisted through custom logic. When Monty code reads or writes to this file, the provided callbacks are invoked.
The virtual path of the file within the OSAccess filesystem.
Type: PurePosixPath
The filename (basename) extracted from path.
Type: str
Callback invoked when the file is read. Receives the path and must return str or bytes.
Type: Callable[[PurePosixPath], str | bytes]
Callback invoked when the file is written. Receives the path and content (str or bytes).
Type: Callable[[PurePosixPath, str | bytes], None]
Unix-style permission bits (default: 0o644).
Type: int
Whether the file has been marked as deleted.
Type: bool
def __init__(
path: str | PurePosixPath,
read: Callable[[PurePosixPath], str | bytes],
write: Callable[[PurePosixPath, str | bytes], None],
*,
permissions: int = 420,
) -> None
Create a callback-backed virtual file.
path : str | PurePosixPath
The virtual path for this file in the OSAccess filesystem.
Callback to generate content when the file is read.
Callback to handle content when the file is written.
permissions : int Default: 420
Unix-style permission bits (default: 0o644).
def read_content() -> str | bytes
Read content by invoking the read callback.
def write_content(content: str | bytes) -> None
Write content by invoking the write callback.
def delete() -> None
Mark the file as deleted.
Bases: NamedTuple
Equivalent to os.stat_result.
protection bits
Type: int
inode
Type: int
device
Type: int
number of hard links
Type: int
user ID of owner
Type: int
group ID of owner
Type: int
total size, in bytes
Type: int
time of last access
Type: float
time of last modification
Type: float
time of last change
Type: float
@classmethod
def file_stat(cls, size: int, mode: int = 420, mtime: float | None = None) -> Self
Creates a stat_result namedtuple for a regular file.
Use this when responding to Path.stat() OS calls.
size : int
File size in bytes
mode : int Default: 420
File permissions as octal (e.g., 0o644) or full mode with file type
Modification time as Unix timestamp, defaults to Now.
@classmethod
def dir_stat(cls, mode: int = 493, mtime: float | None = None) -> Self
Creates a stat_result namedtuple for a directory.
Use this when responding to Path.stat() OS calls on directories.
Self — A namedtuple with stat_result fields
mode : int Default: 493
Directory permissions as octal (e.g., 0o755) or full mode with file type
Modification time as Unix timestamp, defaults to Now.
Host-side handle to a file opened inside a Monty sandbox.
Plain data holder — Monty never gives the host a live OS file descriptor.
Exposed to callbacks (e.g. as the first argument of an open result or
a read/write request) so they can route on path and branch on
mode/binary/readable/writable without re-parsing the mode string.
Construct one from a Python open OS handler to return a handle back to
Monty: MontyFileHandle('/data/foo.txt', 'r'). The mode is canonicalized
at construction ('rt' → 'r', 'r+b' → 'rb+').
Virtual sandbox path of the open file (always POSIX-style, never a host path).
Type: str
Canonical Python open() mode string for this file (e.g. 'r', 'rb+', 'w').
Type: str
Current position for sized/line/seek operations.
Char index in text mode, byte index in binary mode. 0 for a freshly
opened file.
Type: int
True if the mode opens the file in binary form ('rb', 'wb', …).
Type: bool
True if the mode permits read() ('r', 'r+', 'w+', 'a+', and binary variants).
Type: bool
True if the mode permits write() ('w', 'a', 'r+', 'w+', 'a+', and binary variants).
Type: bool
def __new__(cls, path: str, mode: str, *, position: int = 0) -> MontyFileHandle
Construct a MontyFileHandle to return from an open OS callback.
path : str
Virtual sandbox path of the opened file (POSIX-style).
mode : str
Python open() mode string. Parsed and canonicalized at
construction, so 'rt' becomes 'r' and 'r+b' becomes
'rb+'. Raises ValueError for malformed or unsupported
modes (e.g. 'x').
position : int Default: 0
Initial position for sized/line/seek operations (char
index in text mode, byte index in binary mode). Almost always
0 for a freshly opened file.
OS-call handler shared by feed_run / feed_start.
Type: TypeAlias Default: Callable[[OsFunction, tuple[Any, ...], dict[str, Any]], Any] | AbstractOS
Default: Literal['Path.exists', 'Path.is_file', 'Path.is_dir', 'Path.is_symlink', 'open', 'Path.read_text', 'Path.read_bytes', 'Path.write_text', 'Path.write_bytes', 'Path.append_text', 'Path.append_bytes', 'Path.mkdir', 'Path.unlink', 'Path.rmdir', 'Path.iterdir', 'Path.stat', 'Path.rename', 'Path.resolve', 'Path.absolute', 'os.getenv', 'os.environ', 'date.today', 'datetime.now']
Default: object()