90
tools/docs_codegen/yaml_loader.py
Normal file
90
tools/docs_codegen/yaml_loader.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
from tools.docs_codegen.errors import make_docs_codegen_error
|
||||
from tools.docs_codegen.scanner import ModelCodeBlock
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LoadedYaml:
|
||||
"""One loaded YAML document referenced by a ``model-code`` block."""
|
||||
|
||||
yaml_path: Path
|
||||
yaml_root: Any
|
||||
|
||||
|
||||
class YamlLoader:
|
||||
"""Load and cache one repository-relative YAML file."""
|
||||
|
||||
def __init__(self, repo_root: str | Path | None = None) -> None:
|
||||
self.repo_root = Path(repo_root) if repo_root is not None else None
|
||||
self._yaml_cache: dict[Path, Any] = {}
|
||||
|
||||
# -- Public API ----------------------------------------------------------
|
||||
|
||||
def load(
|
||||
self,
|
||||
*,
|
||||
test_case_path: str,
|
||||
block: ModelCodeBlock | None = None,
|
||||
) -> LoadedYaml:
|
||||
"""Resolve, parse, and cache the YAML referenced by a model-code block."""
|
||||
yaml_path = self._resolve_test_case_path(test_case_path=test_case_path, block=block)
|
||||
yaml_root = self._load_yaml_root(yaml_path)
|
||||
return LoadedYaml(
|
||||
yaml_path=yaml_path,
|
||||
yaml_root=yaml_root,
|
||||
)
|
||||
|
||||
# -- Resolution & parsing ------------------------------------------------
|
||||
|
||||
def _resolve_test_case_path(self, *, test_case_path: str, block: ModelCodeBlock | None = None) -> Path:
|
||||
"""Resolve a repo-relative ``test_case_path`` to an absolute, contained, existing file."""
|
||||
candidate = Path(test_case_path)
|
||||
if candidate.is_absolute():
|
||||
raise make_docs_codegen_error(
|
||||
"test_case_path must be a repository-relative path",
|
||||
block=block,
|
||||
test_case_path=test_case_path,
|
||||
)
|
||||
|
||||
base = self._base.resolve()
|
||||
resolved = (base / candidate).resolve()
|
||||
if not resolved.is_relative_to(base):
|
||||
raise make_docs_codegen_error(
|
||||
"test_case_path must stay within the repository",
|
||||
block=block,
|
||||
test_case_path=test_case_path,
|
||||
)
|
||||
|
||||
if not resolved.exists():
|
||||
raise make_docs_codegen_error(
|
||||
"test_case_path file does not exist",
|
||||
block=block,
|
||||
test_case_path=test_case_path,
|
||||
)
|
||||
return resolved
|
||||
|
||||
def _load_yaml_root(self, yaml_path: Path) -> Any:
|
||||
"""Return the parsed YAML for ``yaml_path``, caching it on first load."""
|
||||
if yaml_path not in self._yaml_cache:
|
||||
self._yaml_cache[yaml_path] = self._parse_yaml_file(yaml_path)
|
||||
return self._yaml_cache[yaml_path]
|
||||
|
||||
@staticmethod
|
||||
def _parse_yaml_file(yaml_path: Path) -> Any:
|
||||
"""Read and parse one YAML file, treating an empty document as ``{}``."""
|
||||
with yaml_path.open(encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle) or {}
|
||||
|
||||
# -- Path helpers --------------------------------------------------------
|
||||
|
||||
@property
|
||||
def _base(self) -> Path:
|
||||
"""Directory that repo-relative paths resolve against for filesystem I/O."""
|
||||
return self.repo_root if self.repo_root is not None else Path.cwd()
|
||||
Reference in New Issue
Block a user