Miroslav Simko <ms@iolabs.ch> 2026-09-02T09:09:03+02:00
Commit #7 ยท 6 snippets
src/pipeline/job_config.py | 11 +++++++---- test/test_job_config.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-)
| 63 | def load_job_config(path: Path) -> JobConfig: | 63 | def load_job_config(path: Path) -> JobConfig: |
| 64 | """Parse ``path`` into a :class:`JobConfig`. | 64 | """Parse ``path`` into a :class:`JobConfig`. |
| 65 | 65 | ||
| 66 | Relative ``data_dir`` entries are resolved against the config file's | 66 | Relative ``data_dir`` entries are resolved against the config file's |
| 67 | directory. Missing required keys or unknown top-level keys raise | 67 | directory. Malformed JSON, missing required keys and unknown top-level |
| 68 | :class:`JobConfigError` (a :class:`ValueError`). | 68 | keys all raise :class:`JobConfigError` (a :class:`ValueError`). |
| 69 | """ | 69 | """ |
| 70 | path = Path(path) | 70 | path = Path(path) |
| 71 | raw = json.loads(path.read_text(encoding="utf-8")) | 71 | try: |
| 72 | raw = json.loads(path.read_text(encoding="utf-8")) | ||
| 73 | except json.JSONDecodeError as exc: | ||
| 74 | raise JobConfigError(f"Job config {path} is not valid JSON: {exc}") from exc | ||
| 72 | 75 | ||
| 73 | if not isinstance(raw, dict): | 76 | if not isinstance(raw, dict): |
| 74 | raise JobConfigError(f"Job config {path} must be a JSON object") | 77 | raise JobConfigError(f"Job config {path} must be a JSON object") |
| 75 | 78 |
| 42 | to_segment: int | None = None | 42 | to_segment: int | None = None |
| 43 | device: str | None = None | 43 | device: str | None = None |
| 44 | random_seed: int | None = None | 44 | random_seed: int | None = None |
| 45 | spine_las_files: list[str] | None = None | 45 | spine_las_files: list[str] | None = None |
| 46 | set: dict[str, Any] = {} | 46 | set: dict[str, Any] = pydantic.Field(default_factory=dict) |
| 47 | 47 | ||
| 48 | @pydantic.field_validator("set", mode="before") | 48 | @pydantic.field_validator("set", mode="before") |
| 49 | @classmethod | 49 | @classmethod |
| 50 | def _empty_set_mapping(cls, value: Any) -> Any: | 50 | def _empty_set_mapping(cls, value: Any) -> Any: |
| 1 | import argparse | 1 | import argparse |
| 2 | import json | 2 | import json |
| 3 | from pathlib import Path | 3 | from pathlib import Path |
| 4 | 4 | ||
| 5 | import pydantic | ||
| 5 | import pytest | 6 | import pytest |
| 6 | 7 | ||
| 7 | from src.pipeline import job_config | 8 | from src.pipeline import job_config |
| 8 | 9 |
| 256 | 257 | ||
| 257 | assert isinstance(cfg, job_config.JobConfig) | 258 | assert isinstance(cfg, job_config.JobConfig) |
| 258 | assert cfg.job_id | 259 | assert cfg.job_id |
| 259 | assert cfg.data_dir.is_absolute() | 260 | assert cfg.data_dir.is_absolute() |
| 261 | |||
| 262 | |||
| 263 | def test_load_job_config_rejects_malformed_json(tmp_path: Path) -> None: | ||
| 264 | cfg_path = tmp_path / "job.json" | ||
| 265 | cfg_path.write_text("{not json", encoding="utf-8") | ||
| 266 | |||
| 267 | with pytest.raises(job_config.JobConfigError, match="not valid JSON"): | ||
| 268 | job_config.load_job_config(cfg_path) | ||
| 269 | |||
| 270 | |||
| 271 | def test_job_config_is_frozen() -> None: | ||
| 272 | cfg = job_config.JobConfig(job_id="j", data_dir=Path("/x")) | ||
| 273 | |||
| 274 | with pytest.raises(pydantic.ValidationError): | ||
| 275 | cfg.job_id = "other" | ||
| 276 | |||
| 277 | |||
| 278 | def test_job_config_defaults_are_independent_per_instance() -> None: | ||
| 279 | first = job_config.JobConfig(job_id="a", data_dir=Path("/x")) | ||
| 280 | second = job_config.JobConfig(job_id="b", data_dir=Path("/y")) | ||
| 281 | |||
| 282 | first.set["k"] = 1 | ||
| 283 | |||
| 284 | assert second.set == {} | ||
| 285 | |||
| 286 | |||
| 287 | def test_job_config_coerces_scalar_types() -> None: | ||
| 288 | cfg = job_config.JobConfig(job_id="j", data_dir="/x", from_segment="3") | ||
| 289 | |||
| 290 | assert isinstance(cfg.data_dir, Path) | ||
| 291 | assert cfg.from_segment == 3 |
| 1 | import argparse | 1 | import argparse |
| 2 | import json | 2 | import json |
| 3 | from pathlib import Path | 3 | from pathlib import Path |
| 4 | 4 | ||
| 5 | import pydantic | ||
| 5 | import pytest | 6 | import pytest |
| 6 | 7 | ||
| 7 | from src.pipeline import job_config | 8 | from src.pipeline import job_config |
| 8 | 9 |
| 256 | 257 | ||
| 257 | assert isinstance(cfg, job_config.JobConfig) | 258 | assert isinstance(cfg, job_config.JobConfig) |
| 258 | assert cfg.job_id | 259 | assert cfg.job_id |
| 259 | assert cfg.data_dir.is_absolute() | 260 | assert cfg.data_dir.is_absolute() |
| 261 | |||
| 262 | |||
| 263 | def test_load_job_config_rejects_malformed_json(tmp_path: Path) -> None: | ||
| 264 | cfg_path = tmp_path / "job.json" | ||
| 265 | cfg_path.write_text("{not json", encoding="utf-8") | ||
| 266 | |||
| 267 | with pytest.raises(job_config.JobConfigError, match="not valid JSON"): | ||
| 268 | job_config.load_job_config(cfg_path) | ||
| 269 | |||
| 270 | |||
| 271 | def test_job_config_is_frozen() -> None: | ||
| 272 | cfg = job_config.JobConfig(job_id="j", data_dir=Path("/x")) | ||
| 273 | |||
| 274 | with pytest.raises(pydantic.ValidationError): | ||
| 275 | cfg.job_id = "other" | ||
| 276 | |||
| 277 | |||
| 278 | def test_job_config_defaults_are_independent_per_instance() -> None: | ||
| 279 | first = job_config.JobConfig(job_id="a", data_dir=Path("/x")) | ||
| 280 | second = job_config.JobConfig(job_id="b", data_dir=Path("/y")) | ||
| 281 | |||
| 282 | first.set["k"] = 1 | ||
| 283 | |||
| 284 | assert second.set == {} | ||
| 285 | |||
| 286 | |||
| 287 | def test_job_config_coerces_scalar_types() -> None: | ||
| 288 | cfg = job_config.JobConfig(job_id="j", data_dir="/x", from_segment="3") | ||
| 289 | |||
| 290 | assert isinstance(cfg.data_dir, Path) | ||
| 291 | assert cfg.from_segment == 3 |
default_factoryfor the set-valued field (shared mutable default), malformed JSON raisesJobConfigError, config tests added.