Back to report index

Lanefinder (job config + wrappers) c096c20: AI3D-379 Review fixes: default_factory for set, JobConfigError on malformed JSON, config tests

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(-)
Importance #1: src/pipeline/job_config.py @@ -63,13 +63,16 @@
63def load_job_config(path: Path) -> JobConfig:63def load_job_config(path: Path) -> JobConfig:
64 """Parse ``path`` into a :class:`JobConfig`.64 """Parse ``path`` into a :class:`JobConfig`.
6565
66 Relative ``data_dir`` entries are resolved against the config file's66 Relative ``data_dir`` entries are resolved against the config file's
67 directory. Missing required keys or unknown top-level keys raise67 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
7275
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")
7578
Importance #2: src/pipeline/job_config.py @@ -42,9 +42,9 @@
42 to_segment: int | None = None42 to_segment: int | None = None
43 device: str | None = None43 device: str | None = None
44 random_seed: int | None = None44 random_seed: int | None = None
45 spine_las_files: list[str] | None = None45 spine_las_files: list[str] | None = None
46 set: dict[str, Any] = {}46 set: dict[str, Any] = pydantic.Field(default_factory=dict)
4747
48 @pydantic.field_validator("set", mode="before")48 @pydantic.field_validator("set", mode="before")
49 @classmethod49 @classmethod
50 def _empty_set_mapping(cls, value: Any) -> Any:50 def _empty_set_mapping(cls, value: Any) -> Any:
Importance #3: test/test_job_config.py @@ -1,8 +1,9 @@
1import argparse1import argparse
2import json2import json
3from pathlib import Path3from pathlib import Path
44
5import pydantic
5import pytest6import pytest
67
7from src.pipeline import job_config8from src.pipeline import job_config
89
Importance #4: test/test_job_config.py @@ -256,4 +257,35 @@
256257
257 assert isinstance(cfg, job_config.JobConfig)258 assert isinstance(cfg, job_config.JobConfig)
258 assert cfg.job_id259 assert cfg.job_id
259 assert cfg.data_dir.is_absolute()260 assert cfg.data_dir.is_absolute()
261
262
263def 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
271def 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
278def 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
287def 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
Importance #5: test/test_job_config.py @@ -1,8 +1,9 @@
1import argparse1import argparse
2import json2import json
3from pathlib import Path3from pathlib import Path
44
5import pydantic
5import pytest6import pytest
67
7from src.pipeline import job_config8from src.pipeline import job_config
89
Importance #6: test/test_job_config.py @@ -256,4 +257,35 @@
256257
257 assert isinstance(cfg, job_config.JobConfig)258 assert isinstance(cfg, job_config.JobConfig)
258 assert cfg.job_id259 assert cfg.job_id
259 assert cfg.data_dir.is_absolute()260 assert cfg.data_dir.is_absolute()
261
262
263def 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
271def 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
278def 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
287def 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