Files
Mason Daugherty c83608fe5f chore: linting, formatting, bumps (#658)
* Bump to latest actions, `mint`, Python, lock
* Run linting/formatting/tests
2025-09-25 23:11:45 -04:00

186 lines
5.9 KiB
Python

"""Test utilities for the documentation build pipeline.
This module provides test utilities including context managers for setting up
temporary file systems with source and build directories.
"""
import shutil
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
from typing import NotRequired
from typing_extensions import TypedDict
class File(TypedDict):
"""Represents a file in the test file system."""
path: str
"""Relative path of the file within the source directory."""
bytes: NotRequired[bytes]
"""File as bytes, if applicable."""
content: NotRequired[str]
"""File content as string, if applicable. Assumes utf-8 encoding."""
class FileSystem:
"""Represents a test file system with source and build directories.
This class provides methods for interacting with a test file system,
including listing files, checking existence, and reading file contents.
Attributes:
temp_dir: Path to the temporary directory.
src_dir: Path to the source directory (temp_dir/src).
build_dir: Path to the build directory (temp_dir/build).
"""
def __init__(self, temp_dir: Path, src_dir: Path, build_dir: Path) -> None:
"""Initialize the file system with directory paths.
Args:
temp_dir: Path to the temporary directory.
src_dir: Path to the source directory.
build_dir: Path to the build directory.
"""
self.temp_dir = temp_dir
self.src_dir = src_dir
self.build_dir = build_dir
def list_build_files(self) -> list[Path]:
"""List all files in the build directory.
Returns:
List of Path objects for files in the build directory,
relative to the build directory.
"""
if not self.build_dir.exists():
return []
files = [
file_path.relative_to(self.build_dir)
for file_path in self.build_dir.rglob("*")
if file_path.is_file()
]
return sorted(files)
def build_file_exists(self, path: str) -> bool:
"""Check if a file exists in the build directory.
Args:
path: Relative path to the file within the build directory.
Returns:
True if the file exists, False otherwise.
"""
return (self.build_dir / path).exists()
@contextmanager
def file_system(files: list[File]) -> Iterator[FileSystem]:
"""Create a temporary test file system with the given files.
This context manager creates a temporary directory structure with src/ and build/
subdirectories. The src/ directory is populated with the provided test files.
The temporary directory is automatically cleaned up when exiting the context.
Args:
files: List of File objects to create in the source directory.
Yields:
A FileSystem instance with initialized directories.
Example:
```python
with test_file_system([
{"path": "index.md", "content": "# Hello"},
{"path": "image.png", "bytes": b"PNG_DATA"}
]) as fs:
# Use fs to interact with the test file system
assert fs.build_file_exists("index.md")
```
"""
temp_dir = Path(tempfile.mkdtemp())
src_dir = temp_dir / "src"
build_dir = temp_dir / "build"
try:
# Create src and build directories
src_dir.mkdir()
build_dir.mkdir()
# Create test files in src directory
for file in files:
file_path = src_dir / file["path"]
# Create parent directories if needed
file_path.parent.mkdir(parents=True, exist_ok=True)
# Write file content
if file.get("bytes") is not None:
file_path.write_bytes(file["bytes"])
elif file.get("content") is not None:
file_path.write_text(file["content"], encoding="utf-8")
else:
msg = "File must have either 'bytes' or 'content' defined"
raise ValueError(msg)
# Yield the file system
yield FileSystem(temp_dir, src_dir, build_dir)
finally:
# Clean up the temporary directory
if temp_dir.exists():
shutil.rmtree(temp_dir)
@contextmanager
def temp_directory(files: list[File]) -> Iterator[Path]:
"""Create a temporary directory with the given files.
This context manager creates a temporary directory and populates it with
the provided test files. The temporary directory is automatically cleaned
up when exiting the context.
Args:
files: List of File objects to create in the directory.
Yields:
Path to the temporary directory.
Example:
```python
with temp_directory([
{"path": "index.md", "content": "# Hello"},
{"path": "subdir/image.png", "bytes": b"PNG_DATA"}
]) as temp_dir:
# Use temp_dir to interact with the test directory
assert (temp_dir / "index.md").exists()
```
"""
temp_dir = Path(tempfile.mkdtemp()).resolve()
try:
# Create test files in temp directory
for file in files:
file_path = temp_dir / file["path"]
# Create parent directories if needed
file_path.parent.mkdir(parents=True, exist_ok=True)
# Write file content
if file.get("bytes") is not None:
file_path.write_bytes(file["bytes"])
elif file.get("content") is not None:
file_path.write_text(file["content"], encoding="utf-8")
else:
msg = "File must have either 'bytes' or 'content' defined"
raise ValueError(msg)
# Yield the temp directory path
yield temp_dir
finally:
# Clean up the temporary directory
if temp_dir.exists():
shutil.rmtree(temp_dir)