Coverage for src / rhiza / models / _base.py: 100%
24 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-16 12:51 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-16 12:51 +0000
1"""Base abstractions for YAML-serializable Rhiza models.
3This module defines the :class:`YamlSerializable` :class:`~typing.Protocol` that all
4YAML-capable Rhiza model classes satisfy, plus the :func:`read_yaml` helper used
5by their ``from_yaml`` classmethods.
7Example usage::
9 from rhiza.models.template import RhizaTemplate
11 template = RhizaTemplate.from_yaml(Path(".rhiza/template.yml"))
12"""
14from pathlib import Path
15from typing import Any, Protocol, Self, runtime_checkable
17import yaml
20@runtime_checkable
21class YamlSerializable(Protocol):
22 """Structural protocol for Rhiza models with YAML round-trip support.
24 Any class that implements ``from_yaml`` and ``to_yaml`` with these
25 signatures automatically satisfies this protocol (structural typing).
27 Implementors
28 ------------
29 - :class:`rhiza.models.template.RhizaTemplate`
30 - :class:`rhiza.models.lock.TemplateLock`
31 - :class:`rhiza.models.bundle.RhizaBundles` (read-only: does not implement ``to_yaml``)
32 """
34 @classmethod
35 def from_yaml(cls, file_path: Path) -> Self:
36 """Load the model from a YAML file.
38 Args:
39 file_path: Path to the YAML file to load.
41 Returns:
42 A new instance populated from the file.
44 Raises:
45 FileNotFoundError: If *file_path* does not exist.
46 yaml.YAMLError: If the file contains invalid YAML.
47 ValueError: If the file content is not recognised.
48 """
49 return cls.from_config(read_yaml(file_path))
51 @classmethod
52 def from_config(cls, config: dict[str, Any]) -> Self:
53 """Create a model instance from a configuration dictionary.
55 Args:
56 config: Dictionary containing model configuration.
58 Returns:
59 A new instance populated from the dictionary.
60 """
61 ...
63 @property
64 def config(self) -> dict[str, Any]:
65 """Return the model's current state as a configuration dictionary."""
66 ...
68 def to_yaml(self, file_path: Path) -> None:
69 """Save the model to a YAML file.
71 Args:
72 file_path: Destination path. Parent directories are created
73 automatically if they do not exist.
74 """
75 file_path.parent.mkdir(parents=True, exist_ok=True)
76 with open(file_path, "w", encoding="utf-8") as f:
77 yaml.dump(self.config, f, default_flow_style=False, sort_keys=False)
80def read_yaml(path: Path) -> dict[str, Any]:
81 """Open *path*, parse YAML, and return the config dict.
83 Args:
84 path: Path to the YAML file to load.
86 Returns:
87 Parsed YAML content as a dictionary.
89 Raises:
90 FileNotFoundError: If *path* does not exist.
91 yaml.YAMLError: If the file contains invalid YAML.
92 ValueError: If the file is empty or null.
93 TypeError: If the file does not contain a YAML mapping.
94 """
95 with open(path, encoding="utf-8") as f:
96 data = yaml.safe_load(f)
97 if data is None:
98 raise ValueError(f"{path.name} is empty") # noqa: TRY003
99 if not isinstance(data, dict):
100 raise TypeError(f"{path.name} does not contain a YAML mapping") # noqa: TRY003
101 return data