Back to report index

Step 6 filteringclusters 40561e9: AI3D-379 Review fixes: null config sections fall back to section defaults

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

Commit #34 · 5 snippets

 README.md                                           |  2 ++
 .../_config.py                                      | 21 +++++++++++++++++++++
 tests/test_clustering_gpu_config.py                 | 15 +++++++++++++++
 3 files changed, 38 insertions(+)
Importance #1: src/iolabs_point_cloud_filtering_clusters/_config.py @@ -130,8 +130,29 @@
130 )130 )
131 postprocess: ClusterFinderGPUPostprocessConfig = ClusterFinderGPUPostprocessConfig()131 postprocess: ClusterFinderGPUPostprocessConfig = ClusterFinderGPUPostprocessConfig()
132 file_naming: ClusterFinderGPUFileNamingConfig = ClusterFinderGPUFileNamingConfig()132 file_naming: ClusterFinderGPUFileNamingConfig = ClusterFinderGPUFileNamingConfig()
133133
134 @pydantic.model_validator(mode="before")
135 @classmethod
136 def _drop_null_sections(cls, value: Any) -> Any:
137 """Treat an explicitly ``null`` nested section as "use section defaults"."""
138 if not isinstance(value, dict):
139 return value
140 null_sections = [
141 name
142 for name, field in cls.model_fields.items()
143 if value.get(name, ...) is None
144 and isinstance(field.annotation, type)
145 and issubclass(field.annotation, config_loader.ConfigModel)
146 ]
147 if not null_sections:
148 return value
149 data = dict(value)
150 for name in null_sections:
151 data.pop(name)
152 logger.debug("Using defaults for null %s section(s): %s", _CONTEXT, null_sections)
153 return data
154
134155
135def normalize_cluster_finder_gpu_config(raw_config: dict[str, Any]) -> dict[str, Any]:156def normalize_cluster_finder_gpu_config(raw_config: dict[str, Any]) -> dict[str, Any]:
136 """Validate *raw_config* against the model tree and return a plain dict."""157 """Validate *raw_config* against the model tree and return a plain dict."""
137 return config_loader.validate_config(158 return config_loader.validate_config(
Importance #2: tests/test_clustering_gpu_config.py @@ -63,4 +63,19 @@
63 ).read_text(encoding="utf-8")63 ).read_text(encoding="utf-8")
64 )64 )
6565
66 assert _config.ClusterFinderGPUConfig().model_dump() == packaged66 assert _config.ClusterFinderGPUConfig().model_dump() == packaged
67
68
69def test_null_section_falls_back_to_section_defaults() -> None:
70 """An explicit ``null`` section means "use defaults", as before the pydantic move."""
71 config = _config.normalize_cluster_finder_gpu_config(
72 {"voxelization": None, "clustering": None}
73 )
74
75 assert config["voxelization"] == {"enabled": False, "voxel_size": 0.03}
76 assert config["clustering"]["dbscan_min_points"] == 50
77
78
79def test_non_mapping_section_is_still_rejected() -> None:
80 with pytest.raises(_config.ClusterFinderGPUConfigError, match="voxelization"):
81 _config.normalize_cluster_finder_gpu_config({"voxelization": 5})
Importance #3: README.md @@ -35,8 +35,10 @@
35GPU Step 6 defaults live in `src/iolabs_point_cloud_filtering_clusters/clustering_gpu.default.json` and are mirrored by the pydantic model tree in `_config.py` (`ClusterFinderGPUConfig`, nested sections as nested models).35GPU Step 6 defaults live in `src/iolabs_point_cloud_filtering_clusters/clustering_gpu.default.json` and are mirrored by the pydantic model tree in `_config.py` (`ClusterFinderGPUConfig`, nested sections as nested models).
3636
37To add a config key: add a field to the matching `config_loader.ConfigModel` and the same key to the packaged JSON default. Nothing else. Unknown keys are rejected; overrides deep-merge onto the packaged defaults.37To add a config key: add a field to the matching `config_loader.ConfigModel` and the same key to the packaged JSON default. Nothing else. Unknown keys are rejected; overrides deep-merge onto the packaged defaults.
3838
39To add a whole section: declare a new `config_loader.ConfigModel` subclass, add it as a field on `ClusterFinderGPUConfig` with a default instance, and mirror the section in the packaged JSON. A section given as `null` falls back to that section's defaults; a section of any non-mapping type is rejected.
40
39## Develop locally (Nexus)41## Develop locally (Nexus)
4042
41Internal `iolabs-*` dependencies resolve through the private Nexus index declared in `pyproject.toml`. Export Nexus credentials before any `uv` command that touches private deps — e.g. by sourcing `../3dai.lanefinder/scripts/nexus_credentials.sh` from your shell rc — then:43Internal `iolabs-*` dependencies resolve through the private Nexus index declared in `pyproject.toml`. Export Nexus credentials before any `uv` command that touches private deps — e.g. by sourcing `../3dai.lanefinder/scripts/nexus_credentials.sh` from your shell rc — then:
4244
Importance #4: src/iolabs_point_cloud_filtering_clusters/_config.py @@ -130,8 +130,29 @@
130 )130 )
131 postprocess: ClusterFinderGPUPostprocessConfig = ClusterFinderGPUPostprocessConfig()131 postprocess: ClusterFinderGPUPostprocessConfig = ClusterFinderGPUPostprocessConfig()
132 file_naming: ClusterFinderGPUFileNamingConfig = ClusterFinderGPUFileNamingConfig()132 file_naming: ClusterFinderGPUFileNamingConfig = ClusterFinderGPUFileNamingConfig()
133133
134 @pydantic.model_validator(mode="before")
135 @classmethod
136 def _drop_null_sections(cls, value: Any) -> Any:
137 """Treat an explicitly ``null`` nested section as "use section defaults"."""
138 if not isinstance(value, dict):
139 return value
140 null_sections = [
141 name
142 for name, field in cls.model_fields.items()
143 if value.get(name, ...) is None
144 and isinstance(field.annotation, type)
145 and issubclass(field.annotation, config_loader.ConfigModel)
146 ]
147 if not null_sections:
148 return value
149 data = dict(value)
150 for name in null_sections:
151 data.pop(name)
152 logger.debug("Using defaults for null %s section(s): %s", _CONTEXT, null_sections)
153 return data
154
134155
135def normalize_cluster_finder_gpu_config(raw_config: dict[str, Any]) -> dict[str, Any]:156def normalize_cluster_finder_gpu_config(raw_config: dict[str, Any]) -> dict[str, Any]:
136 """Validate *raw_config* against the model tree and return a plain dict."""157 """Validate *raw_config* against the model tree and return a plain dict."""
137 return config_loader.validate_config(158 return config_loader.validate_config(
Importance #5: tests/test_clustering_gpu_config.py @@ -63,4 +63,19 @@
63 ).read_text(encoding="utf-8")63 ).read_text(encoding="utf-8")
64 )64 )
6565
66 assert _config.ClusterFinderGPUConfig().model_dump() == packaged66 assert _config.ClusterFinderGPUConfig().model_dump() == packaged
67
68
69def test_null_section_falls_back_to_section_defaults() -> None:
70 """An explicit ``null`` section means "use defaults", as before the pydantic move."""
71 config = _config.normalize_cluster_finder_gpu_config(
72 {"voxelization": None, "clustering": None}
73 )
74
75 assert config["voxelization"] == {"enabled": False, "voxel_size": 0.03}
76 assert config["clustering"]["dbscan_min_points"] == 50
77
78
79def test_non_mapping_section_is_still_rejected() -> None:
80 with pytest.raises(_config.ClusterFinderGPUConfigError, match="voxelization"):
81 _config.normalize_cluster_finder_gpu_config({"voxelization": 5})