Back to report index

tablecloth 637406a: AI3D-379 Pydantic config models via iolabs-common ConfigModel

Miroslav Simko <ms@iolabs.ch> 2026-09-02T08:32:36+02:00

Commit #65 · 23 snippets

 README.md                                    | 12 +++---
 pyproject.toml                               |  5 ++-
 src/iolabs_point_cloud_tablecloth/config.py  | 64 +++++++++++++---------------
 src/iolabs_point_cloud_tablecloth/summary.py |  3 +-
 tests/test_config.py                         | 17 +++++++-
 tests/test_ground_smrf.py                    |  2 +-
 6 files changed, 57 insertions(+), 46 deletions(-)
Importance #1: src/iolabs_point_cloud_tablecloth/config.py @@ -1,48 +1,45 @@
1"""Tablecloth configuration.1"""Tablecloth configuration.
22
3Package-owned JSON defaults plus a frozen dataclass schema. Runtime overrides3Package-owned JSON defaults plus a frozen pydantic schema. Runtime overrides
4come from repeatable ``--set KEY=VALUE`` flags (JSON-decoded), never repo-local4come from repeatable ``--set KEY=VALUE`` flags (JSON-decoded), never repo-local
5JSON. Config plumbing (packaged load, deep merge, unknown-key rejection) comes5JSON. Config plumbing (packaged load, deep merge, unknown-key rejection,
6from :mod:`iolabs.common.config_loader`; only the mechanism check stays local.6coercion) comes from :mod:`iolabs.common.config_loader`.
7
8To add a config key, add a field to :class:`TableclothConfig` and a matching
9entry in ``tablecloth.default.json`` nothing else. Packaged defaults must
10validate with zero overrides.
711
8Coercion is strict, by design: a value must be valid for the field's declared12Coercion is strict, by design: a value must be valid for the field's declared
9type or the load fails. ``--set cell_m=true`` (bool for a float field),13type or the load fails. ``--set cell_m=true`` (bool for a float field),
10``--set mechanism=5`` (non-string for a str field), ``--set14``--set mechanism=5`` (non-string for a ``Literal`` field), ``--set
11csf_iterations=3.7`` (non-integral for an int field) and ``--set15csf_iterations=3.7`` (non-integral for an int field) and ``--set
12overlay_enabled=flase`` (bool typo) are all rejected rather than silently16overlay_enabled=flase`` (bool typo) are all rejected rather than silently
13coerced.17coerced.
14"""18"""
1519
20from __future__ import annotations
21
16import logging22import logging
17from dataclasses import dataclass23from typing import Any, Literal
18from typing import Any
1924
20from iolabs.common.config_loader import (25from iolabs.common import config_loader
21 ConfigError,
22 dataclass_from_mapping,
23 deep_merge_dicts,
24 load_packaged_json,
25)
26from iolabs.common.config_loader import parse_set_overrides as common_parse_set_overrides
2726
28logger = logging.getLogger(__name__)27logger = logging.getLogger(__name__)
2928
30_ALLOWED_MECHANISMS = frozenset({"none", "smrf_numpy", "csf_cloth"})
31_DEFAULT_CONFIG_NAME = "tablecloth.default.json"29_DEFAULT_CONFIG_NAME = "tablecloth.default.json"
32_PACKAGE_NAME = "iolabs_point_cloud_tablecloth"30_PACKAGE_NAME = "iolabs_point_cloud_tablecloth"
3331
3432
35class TableclothConfigError(ConfigError):33class TableclothConfigError(config_loader.ConfigError):
36 """Raised when the tablecloth config contains unsupported keys or values."""34 """Raised when the tablecloth config contains unsupported keys or values."""
3735
3836
39@dataclass(frozen=True)37class TableclothConfig(config_loader.ConfigModel):
40class TableclothConfig:
41 """Filter and runtime thresholds for tablecloth ground classification."""38 """Filter and runtime thresholds for tablecloth ground classification."""
4239
43 # Mechanism: none | smrf_numpy | csf_cloth40 # Mechanism: none | smrf_numpy | csf_cloth
44 mechanism: str = "smrf_numpy"41 mechanism: Literal["none", "smrf_numpy", "csf_cloth"] = "smrf_numpy"
4542
46 # SMRF (lip-first defaults; see plan §3)43 # SMRF (lip-first defaults; see plan §3)
47 cell_m: float = 0.20 # Lip-safe fine grid; band 0.15–0.2544 cell_m: float = 0.20 # Lip-safe fine grid; band 0.15–0.25
48 slope_threshold: float = 0.15 # Progressive SMRF slope; curb vs wall45 slope_threshold: float = 0.15 # Progressive SMRF slope; curb vs wall
Importance #2: src/iolabs_point_cloud_tablecloth/config.py @@ -74,25 +71,17 @@
7471
75def load_default_config_dict() -> dict[str, Any]:72def load_default_config_dict() -> dict[str, Any]:
76 """Return the package-owned default config as a plain dict."""73 """Return the package-owned default config as a plain dict."""
77 package = __package__ or _PACKAGE_NAME74 package = __package__ or _PACKAGE_NAME
78 return load_packaged_json(package, _DEFAULT_CONFIG_NAME)75 return config_loader.load_packaged_json(package, _DEFAULT_CONFIG_NAME)
79
80
81def _validate_mechanism(mechanism: str) -> None:
82 if mechanism not in _ALLOWED_MECHANISMS:
83 raise TableclothConfigError(
84 f"Unknown mechanism '{mechanism}'. "
85 f"Expected one of: {', '.join(sorted(_ALLOWED_MECHANISMS))}."
86 )
8776
8877
89def config_from_dict(raw: dict[str, Any]) -> TableclothConfig:78def config_from_dict(raw: dict[str, Any]) -> TableclothConfig:
90 """Build a validated :class:`TableclothConfig` from a raw mapping.79 """Build a validated :class:`TableclothConfig` from a raw mapping.
9180
92 Unknown-key rejection and per-field value coercion come from81 Unknown-key rejection and per-field value coercion come from
93 :func:`iolabs.common.config_loader.dataclass_from_mapping`; only the82 :func:`iolabs.common.config_loader.validate_config`. ``mechanism`` is
94 mechanism check is tablecloth-specific.83 restricted to the declared ``Literal`` choices.
9584
96 Args:85 Args:
97 raw: Merged config mapping (packaged defaults plus ``--set``86 raw: Merged config mapping (packaged defaults plus ``--set``
98 overrides).87 overrides).
Importance #3: src/iolabs_point_cloud_tablecloth/config.py @@ -103,25 +92,30 @@
103 Raises:92 Raises:
104 TableclothConfigError: *raw* holds an unknown key, a value that is not93 TableclothConfigError: *raw* holds an unknown key, a value that is not
105 valid for its declared field type, or an unsupported mechanism.94 valid for its declared field type, or an unsupported mechanism.
106 """95 """
107 config = dataclass_from_mapping(96 return config_loader.validate_config(
108 TableclothConfig,97 TableclothConfig,
109 raw,98 raw,
110 context="tablecloth config",99 context="tablecloth config",
111 error_cls=TableclothConfigError,100 error_cls=TableclothConfigError,
112 )101 )
113 _validate_mechanism(config.mechanism)
114 return config
115102
116103
117def load_config(overrides: dict[str, Any] | None = None) -> TableclothConfig:104def load_config(overrides: dict[str, Any] | None = None) -> TableclothConfig:
118 """Load the default config and apply flat ``KEY=VALUE`` overrides.105 """Load the default config and apply flat ``KEY=VALUE`` overrides.
119106
120 Overrides come from the CLI ``--set`` flag (already parsed into a dict).107 Overrides come from the CLI ``--set`` flag (already parsed into a dict).
121 """108 """
122 merged = deep_merge_dicts(load_default_config_dict(), dict(overrides or {}))109 package = __package__ or _PACKAGE_NAME
123 config = config_from_dict(merged)110 config = config_loader.load_config(
111 TableclothConfig,
112 package=package,
113 filename=_DEFAULT_CONFIG_NAME,
114 overrides=overrides,
115 context="tablecloth config",
116 error_cls=TableclothConfigError,
117 )
124 if overrides:118 if overrides:
125 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))119 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))
126 return config120 return config
127121
Importance #4: src/iolabs_point_cloud_tablecloth/config.py @@ -140,5 +134,5 @@
140134
141 Raises:135 Raises:
142 TableclothConfigError: An override is missing its ``=``.136 TableclothConfigError: An override is missing its ``=``.
143 """137 """
144 return common_parse_set_overrides(raw_overrides, error_cls=TableclothConfigError)138 return config_loader.parse_set_overrides(raw_overrides, error_cls=TableclothConfigError)
Importance #5: src/iolabs_point_cloud_tablecloth/summary.py @@ -1,8 +1,7 @@
1"""Aggregate run summary → ``out/run_summary.json``."""1"""Aggregate run summary → ``out/run_summary.json``."""
22
3import logging3import logging
4from dataclasses import asdict
5from pathlib import Path4from pathlib import Path
6from typing import Any5from typing import Any
76
8from iolabs.common.run_stats import write_stats7from iolabs.common.run_stats import write_stats
Importance #6: src/iolabs_point_cloud_tablecloth/summary.py @@ -62,9 +61,9 @@
62 "runtime_seconds": round(float(runtime_seconds), 3),61 "runtime_seconds": round(float(runtime_seconds), 3),
63 "peak_rss_gb": round(float(peak_rss_gb), 3),62 "peak_rss_gb": round(float(peak_rss_gb), 3),
64 "memory_budget_gb": float(config.memory_budget_gb),63 "memory_budget_gb": float(config.memory_budget_gb),
65 "mechanism": config.mechanism,64 "mechanism": config.mechanism,
66 "config": asdict(config),65 "config": config.model_dump(),
67 }66 }
6867
6968
70def write_run_summary(out_dir: Path, entries: list[dict[str, Any]]) -> Path:69def write_run_summary(out_dir: Path, entries: list[dict[str, Any]]) -> Path:
Importance #7: tests/test_config.py @@ -1,15 +1,30 @@
1import pytest1import pytest
2from iolabs.common import config_loader
23
3from iolabs_point_cloud_tablecloth.config import (4from iolabs_point_cloud_tablecloth.config import (
5 TableclothConfig,
4 TableclothConfigError,6 TableclothConfigError,
5 config_from_dict,7 config_from_dict,
6 load_config,8 load_config,
7 load_default_config_dict,9 load_default_config_dict,
8 parse_set_overrides,10 parse_set_overrides,
9)11)
1012
1113
14def test_packaged_defaults_validate_as_config_model() -> None:
15 """Zero-override load must yield a ConfigModel whose dump matches the JSON."""
16 config = load_config()
17 assert isinstance(config, config_loader.ConfigModel)
18 assert issubclass(TableclothConfigError, config_loader.ConfigError)
19 assert load_default_config_dict() == config.model_dump()
20
21
22def test_model_defaults_match_packaged_json() -> None:
23 """Field defaults must stay identical to the packaged default JSON."""
24 assert TableclothConfig().model_dump() == load_default_config_dict()
25
26
12def test_parse_set_overrides_json_decodes_values() -> None:27def test_parse_set_overrides_json_decodes_values() -> None:
13 parsed = parse_set_overrides(["cell_m=0.15", "overlay_enabled=true", "mechanism=none"])28 parsed = parse_set_overrides(["cell_m=0.15", "overlay_enabled=true", "mechanism=none"])
14 assert parsed == {"cell_m": 0.15, "overlay_enabled": True, "mechanism": "none"}29 assert parsed == {"cell_m": 0.15, "overlay_enabled": True, "mechanism": "none"}
1530
Importance #8: tests/test_config.py @@ -121,6 +136,6 @@
121 load_config({"cell_m": True})136 load_config({"cell_m": True})
122137
123138
124def test_non_string_mechanism_rejected() -> None:139def test_non_string_mechanism_rejected() -> None:
125 with pytest.raises(TableclothConfigError, match="Invalid str"):140 with pytest.raises(TableclothConfigError, match="Expected one of"):
126 load_config({"mechanism": 5})141 load_config({"mechanism": 5})
Importance #9: tests/test_ground_smrf.py @@ -70,9 +70,9 @@
7070
71def test_unknown_mechanism_raises() -> None:71def test_unknown_mechanism_raises() -> None:
72 cloud = flat_plane(n=100)72 cloud = flat_plane(n=100)
73 # Bypass config validation to hit the dispatcher.73 # Bypass config validation to hit the dispatcher.
74 bad = TableclothConfig(mechanism="not_a_mechanism") # type: ignore[arg-type]74 bad = TableclothConfig.model_construct(mechanism="not_a_mechanism")
75 with pytest.raises(TableclothConfigError):75 with pytest.raises(TableclothConfigError):
76 classify_ground(cloud.points, bad)76 classify_ground(cloud.points, bad)
7777
7878
Importance #10: pyproject.toml @@ -1,15 +1,16 @@
1[project]1[project]
2name = "iolabs-point-cloud-tablecloth"2name = "iolabs-point-cloud-tablecloth"
3version = "0.2.0"3version = "0.2.1"
4description = "Ground/hard-surface separation for MLS point clouds before lanefinder rasterization"4description = "Ground/hard-surface separation for MLS point clouds before lanefinder rasterization"
5requires-python = ">=3.11"5requires-python = ">=3.11"
6dependencies = [6dependencies = [
7 "numpy>=1.26",7 "numpy>=1.26",
8 "scipy>=1.13",8 "scipy>=1.13",
9 "pillow>=10.0",9 "pillow>=10.0",
10 "matplotlib>=3.8",10 "matplotlib>=3.8",
11 "iolabs-common>=0.7.0",11 "pydantic>=2.7",
12 "iolabs-common>=0.8.0",
12 "iolabs-geometry-geometry>=0.11.0",13 "iolabs-geometry-geometry>=0.11.0",
13]14]
1415
15[project.optional-dependencies]16[project.optional-dependencies]
Importance #11: README.md @@ -35,9 +35,9 @@
35## Stages35## Stages
3636
37| Stage | What it does |37| Stage | What it does |
38|---|---|38|---|---|
39| **A** | Package scaffold, frozen `TableclothConfig`, default JSON, config tests |39| **A** | Package scaffold, frozen `TableclothConfig` model, default JSON, config tests |
40| **B** | Streaming npz IO, SMRF core + CSF adapter, synthetic ground-filter tests |40| **B** | Streaming npz IO, SMRF core + CSF adapter, synthetic ground-filter tests |
41| **C** | Filter CLI, overlays, `run_summary.json`, full README (this document) |41| **C** | Filter CLI, overlays, `run_summary.json`, full README (this document) |
4242
43See [docs/plans/v1-implementation-plan.md](docs/plans/v1-implementation-plan.md)43See [docs/plans/v1-implementation-plan.md](docs/plans/v1-implementation-plan.md)
Importance #12: README.md @@ -47,11 +47,13 @@
4747
48Following the other iolabs point-cloud packages (guardrails, asphaltedge, …),48Following the other iolabs point-cloud packages (guardrails, asphaltedge, …),
49the package owns an algorithm config49the package owns an algorithm config
50`src/iolabs_point_cloud_tablecloth/tablecloth.default.json`.50`src/iolabs_point_cloud_tablecloth/tablecloth.default.json`.
51`config.py` is the loader/schema: the frozen `TableclothConfig` dataclass is the51`config.py` is the loader/schema: the frozen `TableclothConfig` pydantic model
52typed params object and its field set is the schema. Every dataclass default is52(`config_loader.ConfigModel`) is the typed params object and its fields are the
53kept identical to the JSON (asserted by `tests/test_config.py`).53schema. To add a config key, add a field to the model and a matching entry in
54`tablecloth.default.json` — nothing else. Packaged defaults must validate with
55zero overrides (asserted by `tests/test_config.py`).
5456
55Runtime overrides use the repeatable `--set KEY=VALUE` CLI flag (values are57Runtime overrides use the repeatable `--set KEY=VALUE` CLI flag (values are
56JSON-decoded), never repo-local JSON files. Each value is coerced strictly to58JSON-decoded), never repo-local JSON files. Each value is coerced strictly to
57its field's declared type: a bool for a float field, a non-integral value for59its field's declared type: a bool for a float field, a non-integral value for
Importance #13: README.md @@ -121,9 +123,9 @@
121 "runtime_seconds": 12.345,123 "runtime_seconds": 12.345,
122 "peak_rss_gb": 3.21,124 "peak_rss_gb": 3.21,
123 "memory_budget_gb": 10.0,125 "memory_budget_gb": 10.0,
124 "mechanism": "smrf_numpy",126 "mechanism": "smrf_numpy",
125 "config": { "...": "full TableclothConfig asdict" }127 "config": { "...": "full TableclothConfig model_dump" }
126}128}
127```129```
128130
129## Memory / streaming131## Memory / streaming
Importance #14: pyproject.toml @@ -1,15 +1,16 @@
1[project]1[project]
2name = "iolabs-point-cloud-tablecloth"2name = "iolabs-point-cloud-tablecloth"
3version = "0.2.0"3version = "0.2.1"
4description = "Ground/hard-surface separation for MLS point clouds before lanefinder rasterization"4description = "Ground/hard-surface separation for MLS point clouds before lanefinder rasterization"
5requires-python = ">=3.11"5requires-python = ">=3.11"
6dependencies = [6dependencies = [
7 "numpy>=1.26",7 "numpy>=1.26",
8 "scipy>=1.13",8 "scipy>=1.13",
9 "pillow>=10.0",9 "pillow>=10.0",
10 "matplotlib>=3.8",10 "matplotlib>=3.8",
11 "iolabs-common>=0.7.0",11 "pydantic>=2.7",
12 "iolabs-common>=0.8.0",
12 "iolabs-geometry-geometry>=0.11.0",13 "iolabs-geometry-geometry>=0.11.0",
13]14]
1415
15[project.optional-dependencies]16[project.optional-dependencies]
Importance #15: src/iolabs_point_cloud_tablecloth/config.py @@ -1,48 +1,45 @@
1"""Tablecloth configuration.1"""Tablecloth configuration.
22
3Package-owned JSON defaults plus a frozen dataclass schema. Runtime overrides3Package-owned JSON defaults plus a frozen pydantic schema. Runtime overrides
4come from repeatable ``--set KEY=VALUE`` flags (JSON-decoded), never repo-local4come from repeatable ``--set KEY=VALUE`` flags (JSON-decoded), never repo-local
5JSON. Config plumbing (packaged load, deep merge, unknown-key rejection) comes5JSON. Config plumbing (packaged load, deep merge, unknown-key rejection,
6from :mod:`iolabs.common.config_loader`; only the mechanism check stays local.6coercion) comes from :mod:`iolabs.common.config_loader`.
7
8To add a config key, add a field to :class:`TableclothConfig` and a matching
9entry in ``tablecloth.default.json`` nothing else. Packaged defaults must
10validate with zero overrides.
711
8Coercion is strict, by design: a value must be valid for the field's declared12Coercion is strict, by design: a value must be valid for the field's declared
9type or the load fails. ``--set cell_m=true`` (bool for a float field),13type or the load fails. ``--set cell_m=true`` (bool for a float field),
10``--set mechanism=5`` (non-string for a str field), ``--set14``--set mechanism=5`` (non-string for a ``Literal`` field), ``--set
11csf_iterations=3.7`` (non-integral for an int field) and ``--set15csf_iterations=3.7`` (non-integral for an int field) and ``--set
12overlay_enabled=flase`` (bool typo) are all rejected rather than silently16overlay_enabled=flase`` (bool typo) are all rejected rather than silently
13coerced.17coerced.
14"""18"""
1519
20from __future__ import annotations
21
16import logging22import logging
17from dataclasses import dataclass23from typing import Any, Literal
18from typing import Any
1924
20from iolabs.common.config_loader import (25from iolabs.common import config_loader
21 ConfigError,
22 dataclass_from_mapping,
23 deep_merge_dicts,
24 load_packaged_json,
25)
26from iolabs.common.config_loader import parse_set_overrides as common_parse_set_overrides
2726
28logger = logging.getLogger(__name__)27logger = logging.getLogger(__name__)
2928
30_ALLOWED_MECHANISMS = frozenset({"none", "smrf_numpy", "csf_cloth"})
31_DEFAULT_CONFIG_NAME = "tablecloth.default.json"29_DEFAULT_CONFIG_NAME = "tablecloth.default.json"
32_PACKAGE_NAME = "iolabs_point_cloud_tablecloth"30_PACKAGE_NAME = "iolabs_point_cloud_tablecloth"
3331
3432
35class TableclothConfigError(ConfigError):33class TableclothConfigError(config_loader.ConfigError):
36 """Raised when the tablecloth config contains unsupported keys or values."""34 """Raised when the tablecloth config contains unsupported keys or values."""
3735
3836
39@dataclass(frozen=True)37class TableclothConfig(config_loader.ConfigModel):
40class TableclothConfig:
41 """Filter and runtime thresholds for tablecloth ground classification."""38 """Filter and runtime thresholds for tablecloth ground classification."""
4239
43 # Mechanism: none | smrf_numpy | csf_cloth40 # Mechanism: none | smrf_numpy | csf_cloth
44 mechanism: str = "smrf_numpy"41 mechanism: Literal["none", "smrf_numpy", "csf_cloth"] = "smrf_numpy"
4542
46 # SMRF (lip-first defaults; see plan §3)43 # SMRF (lip-first defaults; see plan §3)
47 cell_m: float = 0.20 # Lip-safe fine grid; band 0.15–0.2544 cell_m: float = 0.20 # Lip-safe fine grid; band 0.15–0.25
48 slope_threshold: float = 0.15 # Progressive SMRF slope; curb vs wall45 slope_threshold: float = 0.15 # Progressive SMRF slope; curb vs wall
Importance #16: src/iolabs_point_cloud_tablecloth/config.py @@ -74,25 +71,17 @@
7471
75def load_default_config_dict() -> dict[str, Any]:72def load_default_config_dict() -> dict[str, Any]:
76 """Return the package-owned default config as a plain dict."""73 """Return the package-owned default config as a plain dict."""
77 package = __package__ or _PACKAGE_NAME74 package = __package__ or _PACKAGE_NAME
78 return load_packaged_json(package, _DEFAULT_CONFIG_NAME)75 return config_loader.load_packaged_json(package, _DEFAULT_CONFIG_NAME)
79
80
81def _validate_mechanism(mechanism: str) -> None:
82 if mechanism not in _ALLOWED_MECHANISMS:
83 raise TableclothConfigError(
84 f"Unknown mechanism '{mechanism}'. "
85 f"Expected one of: {', '.join(sorted(_ALLOWED_MECHANISMS))}."
86 )
8776
8877
89def config_from_dict(raw: dict[str, Any]) -> TableclothConfig:78def config_from_dict(raw: dict[str, Any]) -> TableclothConfig:
90 """Build a validated :class:`TableclothConfig` from a raw mapping.79 """Build a validated :class:`TableclothConfig` from a raw mapping.
9180
92 Unknown-key rejection and per-field value coercion come from81 Unknown-key rejection and per-field value coercion come from
93 :func:`iolabs.common.config_loader.dataclass_from_mapping`; only the82 :func:`iolabs.common.config_loader.validate_config`. ``mechanism`` is
94 mechanism check is tablecloth-specific.83 restricted to the declared ``Literal`` choices.
9584
96 Args:85 Args:
97 raw: Merged config mapping (packaged defaults plus ``--set``86 raw: Merged config mapping (packaged defaults plus ``--set``
98 overrides).87 overrides).
Importance #17: src/iolabs_point_cloud_tablecloth/config.py @@ -103,25 +92,30 @@
103 Raises:92 Raises:
104 TableclothConfigError: *raw* holds an unknown key, a value that is not93 TableclothConfigError: *raw* holds an unknown key, a value that is not
105 valid for its declared field type, or an unsupported mechanism.94 valid for its declared field type, or an unsupported mechanism.
106 """95 """
107 config = dataclass_from_mapping(96 return config_loader.validate_config(
108 TableclothConfig,97 TableclothConfig,
109 raw,98 raw,
110 context="tablecloth config",99 context="tablecloth config",
111 error_cls=TableclothConfigError,100 error_cls=TableclothConfigError,
112 )101 )
113 _validate_mechanism(config.mechanism)
114 return config
115102
116103
117def load_config(overrides: dict[str, Any] | None = None) -> TableclothConfig:104def load_config(overrides: dict[str, Any] | None = None) -> TableclothConfig:
118 """Load the default config and apply flat ``KEY=VALUE`` overrides.105 """Load the default config and apply flat ``KEY=VALUE`` overrides.
119106
120 Overrides come from the CLI ``--set`` flag (already parsed into a dict).107 Overrides come from the CLI ``--set`` flag (already parsed into a dict).
121 """108 """
122 merged = deep_merge_dicts(load_default_config_dict(), dict(overrides or {}))109 package = __package__ or _PACKAGE_NAME
123 config = config_from_dict(merged)110 config = config_loader.load_config(
111 TableclothConfig,
112 package=package,
113 filename=_DEFAULT_CONFIG_NAME,
114 overrides=overrides,
115 context="tablecloth config",
116 error_cls=TableclothConfigError,
117 )
124 if overrides:118 if overrides:
125 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))119 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))
126 return config120 return config
127121
Importance #18: src/iolabs_point_cloud_tablecloth/config.py @@ -140,5 +134,5 @@
140134
141 Raises:135 Raises:
142 TableclothConfigError: An override is missing its ``=``.136 TableclothConfigError: An override is missing its ``=``.
143 """137 """
144 return common_parse_set_overrides(raw_overrides, error_cls=TableclothConfigError)138 return config_loader.parse_set_overrides(raw_overrides, error_cls=TableclothConfigError)
Importance #19: src/iolabs_point_cloud_tablecloth/summary.py @@ -1,8 +1,7 @@
1"""Aggregate run summary → ``out/run_summary.json``."""1"""Aggregate run summary → ``out/run_summary.json``."""
22
3import logging3import logging
4from dataclasses import asdict
5from pathlib import Path4from pathlib import Path
6from typing import Any5from typing import Any
76
8from iolabs.common.run_stats import write_stats7from iolabs.common.run_stats import write_stats
Importance #20: src/iolabs_point_cloud_tablecloth/summary.py @@ -62,9 +61,9 @@
62 "runtime_seconds": round(float(runtime_seconds), 3),61 "runtime_seconds": round(float(runtime_seconds), 3),
63 "peak_rss_gb": round(float(peak_rss_gb), 3),62 "peak_rss_gb": round(float(peak_rss_gb), 3),
64 "memory_budget_gb": float(config.memory_budget_gb),63 "memory_budget_gb": float(config.memory_budget_gb),
65 "mechanism": config.mechanism,64 "mechanism": config.mechanism,
66 "config": asdict(config),65 "config": config.model_dump(),
67 }66 }
6867
6968
70def write_run_summary(out_dir: Path, entries: list[dict[str, Any]]) -> Path:69def write_run_summary(out_dir: Path, entries: list[dict[str, Any]]) -> Path:
Importance #21: tests/test_config.py @@ -1,15 +1,30 @@
1import pytest1import pytest
2from iolabs.common import config_loader
23
3from iolabs_point_cloud_tablecloth.config import (4from iolabs_point_cloud_tablecloth.config import (
5 TableclothConfig,
4 TableclothConfigError,6 TableclothConfigError,
5 config_from_dict,7 config_from_dict,
6 load_config,8 load_config,
7 load_default_config_dict,9 load_default_config_dict,
8 parse_set_overrides,10 parse_set_overrides,
9)11)
1012
1113
14def test_packaged_defaults_validate_as_config_model() -> None:
15 """Zero-override load must yield a ConfigModel whose dump matches the JSON."""
16 config = load_config()
17 assert isinstance(config, config_loader.ConfigModel)
18 assert issubclass(TableclothConfigError, config_loader.ConfigError)
19 assert load_default_config_dict() == config.model_dump()
20
21
22def test_model_defaults_match_packaged_json() -> None:
23 """Field defaults must stay identical to the packaged default JSON."""
24 assert TableclothConfig().model_dump() == load_default_config_dict()
25
26
12def test_parse_set_overrides_json_decodes_values() -> None:27def test_parse_set_overrides_json_decodes_values() -> None:
13 parsed = parse_set_overrides(["cell_m=0.15", "overlay_enabled=true", "mechanism=none"])28 parsed = parse_set_overrides(["cell_m=0.15", "overlay_enabled=true", "mechanism=none"])
14 assert parsed == {"cell_m": 0.15, "overlay_enabled": True, "mechanism": "none"}29 assert parsed == {"cell_m": 0.15, "overlay_enabled": True, "mechanism": "none"}
1530
Importance #22: tests/test_config.py @@ -121,6 +136,6 @@
121 load_config({"cell_m": True})136 load_config({"cell_m": True})
122137
123138
124def test_non_string_mechanism_rejected() -> None:139def test_non_string_mechanism_rejected() -> None:
125 with pytest.raises(TableclothConfigError, match="Invalid str"):140 with pytest.raises(TableclothConfigError, match="Expected one of"):
126 load_config({"mechanism": 5})141 load_config({"mechanism": 5})
Importance #23: tests/test_ground_smrf.py @@ -70,9 +70,9 @@
7070
71def test_unknown_mechanism_raises() -> None:71def test_unknown_mechanism_raises() -> None:
72 cloud = flat_plane(n=100)72 cloud = flat_plane(n=100)
73 # Bypass config validation to hit the dispatcher.73 # Bypass config validation to hit the dispatcher.
74 bad = TableclothConfig(mechanism="not_a_mechanism") # type: ignore[arg-type]74 bad = TableclothConfig.model_construct(mechanism="not_a_mechanism")
75 with pytest.raises(TableclothConfigError):75 with pytest.raises(TableclothConfigError):
76 classify_ground(cloud.points, bad)76 classify_ground(cloud.points, bad)
7777
7878