Back to report index

Step 7b visualizationoverlays 5148712: AI3D-379 Review fixes: reject bool/junk collage_parts with one clean error, run colors map values through the fleet RGBA matrix

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

Commit #46 ยท 4 snippets

 .../_config.py                                     | 47 ++++++++++++++++++++++
 tests/test_config.py                               | 44 ++++++++++++++++++++
 2 files changed, 91 insertions(+)
Importance #1: src/iolabs_point_cloud_visualization_overlays/_config.py @@ -18,8 +18,9 @@
18from __future__ import annotations18from __future__ import annotations
1919
20import json20import json
21import logging21import logging
22from collections.abc import Mapping
22from pathlib import Path23from pathlib import Path
23from typing import Annotated, Any, Literal24from typing import Annotated, Any, Literal
2425
25import pydantic26import pydantic
Importance #2: src/iolabs_point_cloud_visualization_overlays/_config.py @@ -99,8 +100,54 @@
99 "Single-Side Central Axis": (120, 170, 210, 255),100 "Single-Side Central Axis": (120, 170, 210, 255),
100 "default": (255, 255, 255, 255),101 "default": (255, 255, 255, 255),
101 }102 }
102103
104 @pydantic.field_validator("collage_parts", mode="before")
105 @classmethod
106 def _coerce_collage_parts(cls, value: Any) -> Any:
107 """Accept ``"auto"`` or a positive int, and nothing else.
108
109 The declared union is validated member-wise by pydantic, whose lax mode
110 would read ``True`` as ``1`` and would report one error per member; this
111 applies the fleet int matrix (which rejects ``bool``) and reports a
112 single message naming the field.
113 """
114 if isinstance(value, str) and value == "auto":
115 return value
116 try:
117 parts = config_loader.coerce_config_value(
118 "collage_parts", value, int, error_cls=ValueError
119 )
120 except ValueError as exc:
121 raise ValueError(
122 f"Invalid value for 'collage_parts': {value!r}. "
123 "Expected 'auto' or an integer >= 1."
124 ) from exc
125 if parts < 1:
126 raise ValueError(
127 f"Invalid value for 'collage_parts': {value!r}. "
128 "Expected 'auto' or an integer >= 1."
129 )
130 return parts
131
132 @pydantic.field_validator("colors", mode="before")
133 @classmethod
134 def _coerce_colors(cls, value: Any) -> Any:
135 """Apply the fleet RGBA matrix to every entry of the open colour map.
136
137 Values of a ``dict``-typed field are not run through the fleet coercion
138 by `ConfigModel`, so without this a ``true`` channel would land as ``1``
139 and a short colour would be reported as a missing key.
140 """
141 if not isinstance(value, Mapping):
142 return value
143 return {
144 key: config_loader.coerce_config_value(
145 f"colors.{key}", item, Rgba, error_cls=ValueError
146 )
147 for key, item in value.items()
148 }
149
103 @pydantic.model_validator(mode="after")150 @pydantic.model_validator(mode="after")
104 def _check_default_raster_sets(self) -> XmlTopdownOverlayConfig:151 def _check_default_raster_sets(self) -> XmlTopdownOverlayConfig:
105 """Reject default raster-set names that no raster set defines."""152 """Reject default raster-set names that no raster set defines."""
106 unknown = sorted(set(self.default_raster_sets) - set(self.raster_sets))153 unknown = sorted(set(self.default_raster_sets) - set(self.raster_sets))
Importance #3: tests/test_config.py @@ -199,4 +199,48 @@
199 path = tmp_path / "overlay.json"199 path = tmp_path / "overlay.json"
200 path.write_text("{oops", encoding="utf-8")200 path.write_text("{oops", encoding="utf-8")
201 with pytest.raises(_config.XmlTopdownOverlayConfigError, match="Invalid JSON"):201 with pytest.raises(_config.XmlTopdownOverlayConfigError, match="Invalid JSON"):
202 _config.load_xml_topdown_overlay_config(path)202 _config.load_xml_topdown_overlay_config(path)
203
204
205@pytest.mark.parametrize(
206 "overrides",
207 [
208 {"collage_parts": True},
209 {"collage_parts": "nope"},
210 {"collage_parts": 2.5},
211 {"colors": {"default": [True, 0, 0, 255]}},
212 {"colors": {"default": "red"}},
213 {"flagged_color": [True, 0, 0, 255]},
214 ],
215)
216def test_bools_and_junk_never_slip_through_unions_or_color_maps(
217 tmp_path: Path, overrides: dict[str, object]
218) -> None:
219 path = _write_overrides(tmp_path, overrides)
220 with pytest.raises(_config.XmlTopdownOverlayConfigError):
221 _config.load_xml_topdown_overlay_config(path)
222
223
224def test_collage_parts_error_names_the_field_once(tmp_path: Path) -> None:
225 path = _write_overrides(tmp_path, {"collage_parts": 0})
226 with pytest.raises(_config.XmlTopdownOverlayConfigError) as excinfo:
227 _config.load_xml_topdown_overlay_config(path)
228 message = str(excinfo.value)
229 assert message == (
230 "Invalid value for 'collage_parts': 0. Expected 'auto' or an integer >= 1."
231 )
232
233
234def test_short_color_reports_a_tuple_length_error(tmp_path: Path) -> None:
235 path = _write_overrides(tmp_path, {"colors": {"default": [255, 255, 255]}})
236 with pytest.raises(
237 _config.XmlTopdownOverlayConfigError, match=r"expected 4 item\(s\), got 3"
238 ) as excinfo:
239 _config.load_xml_topdown_overlay_config(path)
240 assert "colors.default" in str(excinfo.value)
241
242
243def test_color_map_accepts_set_style_strings(tmp_path: Path) -> None:
244 path = _write_overrides(tmp_path, {"colors": {"default": ["10", "20", "30", "40"]}})
245 config = _config.load_xml_topdown_overlay_config(path)
246 assert config["colors"]["default"] == (10, 20, 30, 40)
Importance #4: tests/test_config.py @@ -199,4 +199,48 @@
199 path = tmp_path / "overlay.json"199 path = tmp_path / "overlay.json"
200 path.write_text("{oops", encoding="utf-8")200 path.write_text("{oops", encoding="utf-8")
201 with pytest.raises(_config.XmlTopdownOverlayConfigError, match="Invalid JSON"):201 with pytest.raises(_config.XmlTopdownOverlayConfigError, match="Invalid JSON"):
202 _config.load_xml_topdown_overlay_config(path)202 _config.load_xml_topdown_overlay_config(path)
203
204
205@pytest.mark.parametrize(
206 "overrides",
207 [
208 {"collage_parts": True},
209 {"collage_parts": "nope"},
210 {"collage_parts": 2.5},
211 {"colors": {"default": [True, 0, 0, 255]}},
212 {"colors": {"default": "red"}},
213 {"flagged_color": [True, 0, 0, 255]},
214 ],
215)
216def test_bools_and_junk_never_slip_through_unions_or_color_maps(
217 tmp_path: Path, overrides: dict[str, object]
218) -> None:
219 path = _write_overrides(tmp_path, overrides)
220 with pytest.raises(_config.XmlTopdownOverlayConfigError):
221 _config.load_xml_topdown_overlay_config(path)
222
223
224def test_collage_parts_error_names_the_field_once(tmp_path: Path) -> None:
225 path = _write_overrides(tmp_path, {"collage_parts": 0})
226 with pytest.raises(_config.XmlTopdownOverlayConfigError) as excinfo:
227 _config.load_xml_topdown_overlay_config(path)
228 message = str(excinfo.value)
229 assert message == (
230 "Invalid value for 'collage_parts': 0. Expected 'auto' or an integer >= 1."
231 )
232
233
234def test_short_color_reports_a_tuple_length_error(tmp_path: Path) -> None:
235 path = _write_overrides(tmp_path, {"colors": {"default": [255, 255, 255]}})
236 with pytest.raises(
237 _config.XmlTopdownOverlayConfigError, match=r"expected 4 item\(s\), got 3"
238 ) as excinfo:
239 _config.load_xml_topdown_overlay_config(path)
240 assert "colors.default" in str(excinfo.value)
241
242
243def test_color_map_accepts_set_style_strings(tmp_path: Path) -> None:
244 path = _write_overrides(tmp_path, {"colors": {"default": ["10", "20", "30", "40"]}})
245 config = _config.load_xml_topdown_overlay_config(path)
246 assert config["colors"]["default"] == (10, 20, 30, 40)