Back to report index

Step 1 trajectorydetect c882a1b: AI3D-379 Align config module with fleet pattern

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

Commit #13 · 15 snippets

 README.md                                          |  4 +-
 .../_config.py                                     | 80 +++++++++++++---------
 tests/test_config.py                               | 69 ++++++++++---------
 3 files changed, 84 insertions(+), 69 deletions(-)
Importance #1: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -1,17 +1,31 @@
1"""Configuration for trajectory extraction from LIDAR point clouds (Step 1).
2
3The schema is `TrajectoryConfig` (a `config_loader.ConfigModel`), mirroring
4`trajectories.default.json` key for key.
5
6Adding a config key means adding the field to the model and the same key to
7`trajectories.default.json` nothing else. Unknown keys are rejected.
8
9The entry points return a plain `dict[str, Any]`, not the frozen model.
10"""
11
1from __future__ import annotations12from __future__ import annotations
213
3import logging14import logging
4from collections.abc import Mapping15from collections.abc import Mapping
5from pathlib import Path16from pathlib import Path
6from typing import Any17from typing import Any
718
19import pydantic
20
8from iolabs.common import config_loader21from iolabs.common import config_loader
922
10logger = logging.getLogger(__name__)23logger = logging.getLogger(__name__)
1124
12_PACKAGE = "iolabs_point_cloud_trajectory_detect"25_PACKAGE_NAME = "iolabs_point_cloud_trajectory_detect"
13_DEFAULT_FILENAME = "trajectories.default.json"26_DEFAULT_FILENAME = "trajectories.default.json"
27_CONTEXT = "trajectory config"
1428
1529
16class TrajectoryConfig(config_loader.ConfigModel):30class TrajectoryConfig(config_loader.ConfigModel):
17 """Packaged trajectory-detect configuration."""31 """Packaged trajectory-detect configuration."""
Importance #2: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -70,21 +99,14 @@
70 Raises:99 Raises:
71 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.100 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.
72 OSError: The config file could not be read.101 OSError: The config file could not be read.
73 """102 """
74 return config_loader.load_config(103 return _load_model(config_path=config_path).model_dump()
75 TrajectoryConfig,
76 package=_PACKAGE,
77 filename=_DEFAULT_FILENAME,
78 config_path=config_path,
79 context="trajectory config",
80 error_cls=TrajectoryConfigError,
81 ).model_dump()
82104
83105
84def build_trajectory_config(106def build_trajectory_config(
85 *,107 *,
86 overrides: dict[str, Any] | None = None,108 overrides: Mapping[str, Any] | None = None,
87 config_path: str | Path | None = None,109 config_path: str | Path | None = None,
88) -> dict[str, Any]:110) -> dict[str, Any]:
89 """Load defaults (or *config_path*) and deep-merge *overrides* on top.111 """Load defaults (or *config_path*) and deep-merge *overrides* on top.
90112
Importance #3: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -20,41 +34,56 @@
20 save_ply: bool = False34 save_ply: bool = False
21 only_within_80_degrees: bool = True35 only_within_80_degrees: bool = True
22 pcd_extension: str = ""36 pcd_extension: str = ""
23 spline_pcd_extension: str = "_run1_spline_points"37 spline_pcd_extension: str = "_run1_spline_points"
24 trajectory_max_angle: float = 5.038 trajectory_max_angle: float = pydantic.Field(default=5.0, gt=0.0, le=180.0)
25 trajectory_first_downsample: float = 0.0139 trajectory_first_downsample: float = pydantic.Field(default=0.01, gt=0.0)
26 trajectory_second_downsample: float = 0.00540 trajectory_second_downsample: float = pydantic.Field(default=0.005, gt=0.0)
27 trajectory_outlier_removal_nb_points: int = 741 trajectory_outlier_removal_nb_points: int = pydantic.Field(default=7, ge=1)
28 trajectory_outlier_removal_search_radius: float = 1.042 trajectory_outlier_removal_search_radius: float = pydantic.Field(default=1.0, gt=0.0)
2943
3044
31class TrajectoryConfigError(config_loader.ConfigError):45class TrajectoryConfigError(config_loader.ConfigError):
32 """Raised when trajectory config contains unsupported keys or values."""46 """Raised when trajectory config contains unsupported keys or values."""
3347
3448
35def normalize_trajectory_config(49def _load_model(
36 raw_config: Mapping[str, Any] | TrajectoryConfig,50 *,
37) -> dict[str, Any]:51 overrides: Mapping[str, Any] | None = None,
52 config_path: str | Path | None = None,
53) -> TrajectoryConfig:
54 """Load the packaged defaults (or *config_path*) with *overrides* merged on top."""
55 if overrides:
56 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))
57 if config_path is not None:
58 logger.info("Config file applied: %s", config_path)
59 return config_loader.load_config(
60 TrajectoryConfig,
61 package=_PACKAGE_NAME,
62 filename=_DEFAULT_FILENAME,
63 overrides=dict(overrides) if overrides else None,
64 config_path=config_path,
65 context=_CONTEXT,
66 error_cls=TrajectoryConfigError,
67 )
68
69
70def normalize_trajectory_config(raw_config: Mapping[str, Any]) -> dict[str, Any]:
38 """Validate *raw_config* and fill field defaults.71 """Validate *raw_config* and fill field defaults.
3972
40 Args:73 Args:
41 raw_config: A raw or partial trajectory config mapping, or an already74 raw_config: A raw or partial trajectory config mapping.
42 validated :class:`TrajectoryConfig`.
4375
44 Returns:76 Returns:
45 A plain dict of the validated config.77 A plain dict of the validated config.
4678
47 Raises:79 Raises:
48 TrajectoryConfigError: Unknown keys or values that cannot be coerced.80 TrajectoryConfigError: Unknown keys or values that cannot be coerced.
49 """81 """
50 if isinstance(raw_config, TrajectoryConfig):
51 return raw_config.model_dump()
52 logger.debug("Normalizing trajectory config keys=%s", sorted(raw_config))
53 return config_loader.validate_config(82 return config_loader.validate_config(
54 TrajectoryConfig,83 TrajectoryConfig,
55 raw_config,84 raw_config,
56 context="trajectory config",85 context=_CONTEXT,
57 error_cls=TrajectoryConfigError,86 error_cls=TrajectoryConfigError,
58 ).model_dump()87 ).model_dump()
5988
6089
Importance #4: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -98,13 +120,5 @@
98 Raises:120 Raises:
99 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.121 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.
100 OSError: The config file could not be read.122 OSError: The config file could not be read.
101 """123 """
102 return config_loader.load_config(124 return _load_model(overrides=overrides, config_path=config_path).model_dump()
103 TrajectoryConfig,
104 package=_PACKAGE,
105 filename=_DEFAULT_FILENAME,
106 overrides=overrides,
107 config_path=config_path,
108 context="trajectory config",
109 error_cls=TrajectoryConfigError,
110 ).model_dump()
Importance #5: tests/test_config.py @@ -1,17 +1,14 @@
1from __future__ import annotations1from __future__ import annotations
22
3import json3import json
4import sys
5from pathlib import Path4from pathlib import Path
65
7import pytest6import pytest
87
9sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))8from iolabs.common import config_loader
109from iolabs_point_cloud_trajectory_detect import _config
11from iolabs.common import config_loader # noqa: E40210from iolabs_point_cloud_trajectory_detect._config import (
12from iolabs_point_cloud_trajectory_detect import _config # noqa: E402
13from iolabs_point_cloud_trajectory_detect._config import ( # noqa: E402
14 TrajectoryConfig,11 TrajectoryConfig,
15 TrajectoryConfigError,12 TrajectoryConfigError,
16 build_trajectory_config,13 build_trajectory_config,
17 load_trajectory_config,14 load_trajectory_config,
Importance #6: tests/test_config.py @@ -36,45 +33,51 @@
36 path = Path(_config.__file__).with_name(_config._DEFAULT_FILENAME)33 path = Path(_config.__file__).with_name(_config._DEFAULT_FILENAME)
37 return json.loads(path.read_text(encoding="utf-8"))34 return json.loads(path.read_text(encoding="utf-8"))
3835
3936
40def test_packaged_json_matches_model_field_defaults() -> None:37def test_model_defaults_match_packaged_json() -> None:
41 packaged = _packaged_defaults()
42 model_defaults = {38 model_defaults = {
43 name: field.get_default(call_default_factory=True)39 name: field.get_default(call_default_factory=True)
44 for name, field in TrajectoryConfig.model_fields.items()40 for name, field in TrajectoryConfig.model_fields.items()
45 }41 }
46 assert packaged == model_defaults42 assert _packaged_defaults() == model_defaults
43 assert TrajectoryConfig().model_dump() == _EXPECTED_DEFAULTS
4744
4845
49def test_normalize_accepts_a_validated_model() -> None:46def test_load_trajectory_config_returns_packaged_defaults() -> None:
50 model = TrajectoryConfig(**_EXPECTED_DEFAULTS)47 config = load_trajectory_config()
51 assert normalize_trajectory_config(model) == _EXPECTED_DEFAULTS48 assert isinstance(config, dict)
49 assert config == _EXPECTED_DEFAULTS
5250
5351
54def test_error_class_is_config_error() -> None:52def test_error_class_is_config_error() -> None:
55 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)53 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)
56 assert issubclass(TrajectoryConfigError, ValueError)54 assert issubclass(TrajectoryConfigError, ValueError)
5755
5856
59def test_load_trajectory_config_returns_packaged_defaults() -> None:57def test_unknown_top_level_key_is_rejected() -> None:
60 config = load_trajectory_config()58 with pytest.raises(TrajectoryConfigError, match="not_a_real_key") as exc:
61 assert isinstance(config, dict)59 normalize_trajectory_config({"not_a_real_key": 1})
62 assert config == _EXPECTED_DEFAULTS60 message = str(exc.value)
61 assert "Unknown trajectory config key" in message
62 assert "Allowed keys:" in message
63 with pytest.raises(TrajectoryConfigError, match="typo_max_angle"):
64 build_trajectory_config(overrides={"typo_max_angle": 1.0})
6365
6466
65def test_normalize_fills_defaults_and_rejects_unknown_keys() -> None:67def test_normalize_fills_defaults() -> None:
66 config = normalize_trajectory_config({"pcd_extension": "_custom"})68 config = normalize_trajectory_config({"pcd_extension": "_custom"})
67 assert config["pcd_extension"] == "_custom"69 assert config["pcd_extension"] == "_custom"
68 assert config["trajectory_max_angle"] == 5.070 assert config["trajectory_max_angle"] == 5.0
69 with pytest.raises(TrajectoryConfigError, match="Unknown trajectory config key") as exc:
70 normalize_trajectory_config({"not_a_real_key": 1})
71 message = str(exc.value)
72 assert "not_a_real_key" in message
73 assert "Allowed keys:" in message
7471
7572
76def test_build_deep_merges_overrides_and_coerces_set_strings() -> None:73def test_overrides_deep_merge_onto_defaults() -> None:
74 config = build_trajectory_config(overrides={"save_npz": True})
75 assert config["save_npz"] is True
76 assert config["spline_pcd_extension"] == "_run1_spline_points"
77
78
79def test_set_override_coercion_and_rejection() -> None:
77 overrides = config_loader.parse_set_overrides(80 overrides = config_loader.parse_set_overrides(
78 [81 [
79 "save_npz=true",82 "save_npz=true",
80 "trajectory_max_angle=2.5",83 "trajectory_max_angle=2.5",
Importance #7: tests/test_config.py @@ -84,25 +87,23 @@
84 config = build_trajectory_config(overrides=overrides)87 config = build_trajectory_config(overrides=overrides)
85 assert config["save_npz"] is True88 assert config["save_npz"] is True
86 assert config["trajectory_max_angle"] == 2.589 assert config["trajectory_max_angle"] == 2.5
87 assert config["trajectory_outlier_removal_nb_points"] == 990 assert config["trajectory_outlier_removal_nb_points"] == 9
88 assert config["spline_pcd_extension"] == "_run1_spline_points"91 with pytest.raises(TrajectoryConfigError):
92 normalize_trajectory_config({"trajectory_max_angle": "not-a-float"})
93 with pytest.raises(TrajectoryConfigError):
94 normalize_trajectory_config({"save_npz": "flase"})
8995
9096
91def test_build_rejects_unknown_override_key() -> None:97def test_out_of_range_value_is_rejected() -> None:
92 with pytest.raises(TrajectoryConfigError, match="Unknown trajectory config key"):98 with pytest.raises(TrajectoryConfigError):
93 build_trajectory_config(overrides={"typo_max_angle": 1.0})99 normalize_trajectory_config({"trajectory_first_downsample": 0.0})
100 with pytest.raises(TrajectoryConfigError):
101 normalize_trajectory_config({"trajectory_outlier_removal_nb_points": 0})
94102
95103
96def test_load_config_path_replaces_packaged_defaults(tmp_path: Path) -> None:104def test_load_config_path_replaces_packaged_defaults(tmp_path: Path) -> None:
97 path = tmp_path / "custom.json"105 path = tmp_path / "custom.json"
98 path.write_text(json.dumps({"trajectory_max_angle": 12.0}), encoding="utf-8")106 path.write_text(json.dumps({"trajectory_max_angle": 12.0}), encoding="utf-8")
99 config = load_trajectory_config(path)107 config = load_trajectory_config(path)
100 assert config["trajectory_max_angle"] == 12.0108 assert config["trajectory_max_angle"] == 12.0
101 assert config["save_npz"] is False109 assert config["save_npz"] is False
102
103
104def test_bad_value_is_rejected() -> None:
105 with pytest.raises(TrajectoryConfigError):
106 normalize_trajectory_config({"trajectory_max_angle": "not-a-float"})
107 with pytest.raises(TrajectoryConfigError):
108 normalize_trajectory_config({"save_npz": "flase"})
Importance #8: README.md @@ -22,11 +22,11 @@
22## Usage22## Usage
2323
24Use this package to detect and extract trajectories (e.g. scanner path or vehicle path) from LIDAR point cloud data.24Use this package to detect and extract trajectories (e.g. scanner path or vehicle path) from LIDAR point cloud data.
2525
26## Config26## Configuration
2727
28Defaults live in `src/iolabs_point_cloud_trajectory_detect/trajectories.default.json`. The schema is `TrajectoryConfig` in `_config.py` (a `config_loader.ConfigModel`). To add a key, add the field to the model and the matching default in the JSON; nothing else. Unknown keys are rejected. `load_trajectory_config` / `build_trajectory_config` / `normalize_trajectory_config` return a plain `dict`, not the frozen model.28Defaults live in `src/iolabs_point_cloud_trajectory_detect/trajectories.default.json`. The schema is `TrajectoryConfig` in `_config.py` (a `config_loader.ConfigModel`); nested JSON sections are nested models and unknown keys are rejected. **To add a config key: add the field (with its type, default and any `Field` range) to the model and the same key with the same default to the JSON — nothing else.** `load_trajectory_config` / `build_trajectory_config` / `normalize_trajectory_config` return a plain `dict`. Runtime overrides come from repeatable `--set KEY=VALUE`, never repo-local JSON.
2929
30## Develop locally (Nexus)30## Develop locally (Nexus)
3131
32Internal `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:32Internal `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:
Importance #9: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -1,17 +1,31 @@
1"""Configuration for trajectory extraction from LIDAR point clouds (Step 1).
2
3The schema is `TrajectoryConfig` (a `config_loader.ConfigModel`), mirroring
4`trajectories.default.json` key for key.
5
6Adding a config key means adding the field to the model and the same key to
7`trajectories.default.json` nothing else. Unknown keys are rejected.
8
9The entry points return a plain `dict[str, Any]`, not the frozen model.
10"""
11
1from __future__ import annotations12from __future__ import annotations
213
3import logging14import logging
4from collections.abc import Mapping15from collections.abc import Mapping
5from pathlib import Path16from pathlib import Path
6from typing import Any17from typing import Any
718
19import pydantic
20
8from iolabs.common import config_loader21from iolabs.common import config_loader
922
10logger = logging.getLogger(__name__)23logger = logging.getLogger(__name__)
1124
12_PACKAGE = "iolabs_point_cloud_trajectory_detect"25_PACKAGE_NAME = "iolabs_point_cloud_trajectory_detect"
13_DEFAULT_FILENAME = "trajectories.default.json"26_DEFAULT_FILENAME = "trajectories.default.json"
27_CONTEXT = "trajectory config"
1428
1529
16class TrajectoryConfig(config_loader.ConfigModel):30class TrajectoryConfig(config_loader.ConfigModel):
17 """Packaged trajectory-detect configuration."""31 """Packaged trajectory-detect configuration."""
Importance #10: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -20,41 +34,56 @@
20 save_ply: bool = False34 save_ply: bool = False
21 only_within_80_degrees: bool = True35 only_within_80_degrees: bool = True
22 pcd_extension: str = ""36 pcd_extension: str = ""
23 spline_pcd_extension: str = "_run1_spline_points"37 spline_pcd_extension: str = "_run1_spline_points"
24 trajectory_max_angle: float = 5.038 trajectory_max_angle: float = pydantic.Field(default=5.0, gt=0.0, le=180.0)
25 trajectory_first_downsample: float = 0.0139 trajectory_first_downsample: float = pydantic.Field(default=0.01, gt=0.0)
26 trajectory_second_downsample: float = 0.00540 trajectory_second_downsample: float = pydantic.Field(default=0.005, gt=0.0)
27 trajectory_outlier_removal_nb_points: int = 741 trajectory_outlier_removal_nb_points: int = pydantic.Field(default=7, ge=1)
28 trajectory_outlier_removal_search_radius: float = 1.042 trajectory_outlier_removal_search_radius: float = pydantic.Field(default=1.0, gt=0.0)
2943
3044
31class TrajectoryConfigError(config_loader.ConfigError):45class TrajectoryConfigError(config_loader.ConfigError):
32 """Raised when trajectory config contains unsupported keys or values."""46 """Raised when trajectory config contains unsupported keys or values."""
3347
3448
35def normalize_trajectory_config(49def _load_model(
36 raw_config: Mapping[str, Any] | TrajectoryConfig,50 *,
37) -> dict[str, Any]:51 overrides: Mapping[str, Any] | None = None,
52 config_path: str | Path | None = None,
53) -> TrajectoryConfig:
54 """Load the packaged defaults (or *config_path*) with *overrides* merged on top."""
55 if overrides:
56 logger.info("Config overrides applied: %s", ", ".join(sorted(overrides)))
57 if config_path is not None:
58 logger.info("Config file applied: %s", config_path)
59 return config_loader.load_config(
60 TrajectoryConfig,
61 package=_PACKAGE_NAME,
62 filename=_DEFAULT_FILENAME,
63 overrides=dict(overrides) if overrides else None,
64 config_path=config_path,
65 context=_CONTEXT,
66 error_cls=TrajectoryConfigError,
67 )
68
69
70def normalize_trajectory_config(raw_config: Mapping[str, Any]) -> dict[str, Any]:
38 """Validate *raw_config* and fill field defaults.71 """Validate *raw_config* and fill field defaults.
3972
40 Args:73 Args:
41 raw_config: A raw or partial trajectory config mapping, or an already74 raw_config: A raw or partial trajectory config mapping.
42 validated :class:`TrajectoryConfig`.
4375
44 Returns:76 Returns:
45 A plain dict of the validated config.77 A plain dict of the validated config.
4678
47 Raises:79 Raises:
48 TrajectoryConfigError: Unknown keys or values that cannot be coerced.80 TrajectoryConfigError: Unknown keys or values that cannot be coerced.
49 """81 """
50 if isinstance(raw_config, TrajectoryConfig):
51 return raw_config.model_dump()
52 logger.debug("Normalizing trajectory config keys=%s", sorted(raw_config))
53 return config_loader.validate_config(82 return config_loader.validate_config(
54 TrajectoryConfig,83 TrajectoryConfig,
55 raw_config,84 raw_config,
56 context="trajectory config",85 context=_CONTEXT,
57 error_cls=TrajectoryConfigError,86 error_cls=TrajectoryConfigError,
58 ).model_dump()87 ).model_dump()
5988
6089
Importance #11: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -70,21 +99,14 @@
70 Raises:99 Raises:
71 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.100 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.
72 OSError: The config file could not be read.101 OSError: The config file could not be read.
73 """102 """
74 return config_loader.load_config(103 return _load_model(config_path=config_path).model_dump()
75 TrajectoryConfig,
76 package=_PACKAGE,
77 filename=_DEFAULT_FILENAME,
78 config_path=config_path,
79 context="trajectory config",
80 error_cls=TrajectoryConfigError,
81 ).model_dump()
82104
83105
84def build_trajectory_config(106def build_trajectory_config(
85 *,107 *,
86 overrides: dict[str, Any] | None = None,108 overrides: Mapping[str, Any] | None = None,
87 config_path: str | Path | None = None,109 config_path: str | Path | None = None,
88) -> dict[str, Any]:110) -> dict[str, Any]:
89 """Load defaults (or *config_path*) and deep-merge *overrides* on top.111 """Load defaults (or *config_path*) and deep-merge *overrides* on top.
90112
Importance #12: src/iolabs_point_cloud_trajectory_detect/_config.py @@ -98,13 +120,5 @@
98 Raises:120 Raises:
99 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.121 TrajectoryConfigError: Malformed JSON, unknown keys, or bad values.
100 OSError: The config file could not be read.122 OSError: The config file could not be read.
101 """123 """
102 return config_loader.load_config(124 return _load_model(overrides=overrides, config_path=config_path).model_dump()
103 TrajectoryConfig,
104 package=_PACKAGE,
105 filename=_DEFAULT_FILENAME,
106 overrides=overrides,
107 config_path=config_path,
108 context="trajectory config",
109 error_cls=TrajectoryConfigError,
110 ).model_dump()
Importance #13: tests/test_config.py @@ -1,17 +1,14 @@
1from __future__ import annotations1from __future__ import annotations
22
3import json3import json
4import sys
5from pathlib import Path4from pathlib import Path
65
7import pytest6import pytest
87
9sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))8from iolabs.common import config_loader
109from iolabs_point_cloud_trajectory_detect import _config
11from iolabs.common import config_loader # noqa: E40210from iolabs_point_cloud_trajectory_detect._config import (
12from iolabs_point_cloud_trajectory_detect import _config # noqa: E402
13from iolabs_point_cloud_trajectory_detect._config import ( # noqa: E402
14 TrajectoryConfig,11 TrajectoryConfig,
15 TrajectoryConfigError,12 TrajectoryConfigError,
16 build_trajectory_config,13 build_trajectory_config,
17 load_trajectory_config,14 load_trajectory_config,
Importance #14: tests/test_config.py @@ -36,45 +33,51 @@
36 path = Path(_config.__file__).with_name(_config._DEFAULT_FILENAME)33 path = Path(_config.__file__).with_name(_config._DEFAULT_FILENAME)
37 return json.loads(path.read_text(encoding="utf-8"))34 return json.loads(path.read_text(encoding="utf-8"))
3835
3936
40def test_packaged_json_matches_model_field_defaults() -> None:37def test_model_defaults_match_packaged_json() -> None:
41 packaged = _packaged_defaults()
42 model_defaults = {38 model_defaults = {
43 name: field.get_default(call_default_factory=True)39 name: field.get_default(call_default_factory=True)
44 for name, field in TrajectoryConfig.model_fields.items()40 for name, field in TrajectoryConfig.model_fields.items()
45 }41 }
46 assert packaged == model_defaults42 assert _packaged_defaults() == model_defaults
43 assert TrajectoryConfig().model_dump() == _EXPECTED_DEFAULTS
4744
4845
49def test_normalize_accepts_a_validated_model() -> None:46def test_load_trajectory_config_returns_packaged_defaults() -> None:
50 model = TrajectoryConfig(**_EXPECTED_DEFAULTS)47 config = load_trajectory_config()
51 assert normalize_trajectory_config(model) == _EXPECTED_DEFAULTS48 assert isinstance(config, dict)
49 assert config == _EXPECTED_DEFAULTS
5250
5351
54def test_error_class_is_config_error() -> None:52def test_error_class_is_config_error() -> None:
55 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)53 assert issubclass(TrajectoryConfigError, config_loader.ConfigError)
56 assert issubclass(TrajectoryConfigError, ValueError)54 assert issubclass(TrajectoryConfigError, ValueError)
5755
5856
59def test_load_trajectory_config_returns_packaged_defaults() -> None:57def test_unknown_top_level_key_is_rejected() -> None:
60 config = load_trajectory_config()58 with pytest.raises(TrajectoryConfigError, match="not_a_real_key") as exc:
61 assert isinstance(config, dict)59 normalize_trajectory_config({"not_a_real_key": 1})
62 assert config == _EXPECTED_DEFAULTS60 message = str(exc.value)
61 assert "Unknown trajectory config key" in message
62 assert "Allowed keys:" in message
63 with pytest.raises(TrajectoryConfigError, match="typo_max_angle"):
64 build_trajectory_config(overrides={"typo_max_angle": 1.0})
6365
6466
65def test_normalize_fills_defaults_and_rejects_unknown_keys() -> None:67def test_normalize_fills_defaults() -> None:
66 config = normalize_trajectory_config({"pcd_extension": "_custom"})68 config = normalize_trajectory_config({"pcd_extension": "_custom"})
67 assert config["pcd_extension"] == "_custom"69 assert config["pcd_extension"] == "_custom"
68 assert config["trajectory_max_angle"] == 5.070 assert config["trajectory_max_angle"] == 5.0
69 with pytest.raises(TrajectoryConfigError, match="Unknown trajectory config key") as exc:
70 normalize_trajectory_config({"not_a_real_key": 1})
71 message = str(exc.value)
72 assert "not_a_real_key" in message
73 assert "Allowed keys:" in message
7471
7572
76def test_build_deep_merges_overrides_and_coerces_set_strings() -> None:73def test_overrides_deep_merge_onto_defaults() -> None:
74 config = build_trajectory_config(overrides={"save_npz": True})
75 assert config["save_npz"] is True
76 assert config["spline_pcd_extension"] == "_run1_spline_points"
77
78
79def test_set_override_coercion_and_rejection() -> None:
77 overrides = config_loader.parse_set_overrides(80 overrides = config_loader.parse_set_overrides(
78 [81 [
79 "save_npz=true",82 "save_npz=true",
80 "trajectory_max_angle=2.5",83 "trajectory_max_angle=2.5",
Importance #15: tests/test_config.py @@ -84,25 +87,23 @@
84 config = build_trajectory_config(overrides=overrides)87 config = build_trajectory_config(overrides=overrides)
85 assert config["save_npz"] is True88 assert config["save_npz"] is True
86 assert config["trajectory_max_angle"] == 2.589 assert config["trajectory_max_angle"] == 2.5
87 assert config["trajectory_outlier_removal_nb_points"] == 990 assert config["trajectory_outlier_removal_nb_points"] == 9
88 assert config["spline_pcd_extension"] == "_run1_spline_points"91 with pytest.raises(TrajectoryConfigError):
92 normalize_trajectory_config({"trajectory_max_angle": "not-a-float"})
93 with pytest.raises(TrajectoryConfigError):
94 normalize_trajectory_config({"save_npz": "flase"})
8995
9096
91def test_build_rejects_unknown_override_key() -> None:97def test_out_of_range_value_is_rejected() -> None:
92 with pytest.raises(TrajectoryConfigError, match="Unknown trajectory config key"):98 with pytest.raises(TrajectoryConfigError):
93 build_trajectory_config(overrides={"typo_max_angle": 1.0})99 normalize_trajectory_config({"trajectory_first_downsample": 0.0})
100 with pytest.raises(TrajectoryConfigError):
101 normalize_trajectory_config({"trajectory_outlier_removal_nb_points": 0})
94102
95103
96def test_load_config_path_replaces_packaged_defaults(tmp_path: Path) -> None:104def test_load_config_path_replaces_packaged_defaults(tmp_path: Path) -> None:
97 path = tmp_path / "custom.json"105 path = tmp_path / "custom.json"
98 path.write_text(json.dumps({"trajectory_max_angle": 12.0}), encoding="utf-8")106 path.write_text(json.dumps({"trajectory_max_angle": 12.0}), encoding="utf-8")
99 config = load_trajectory_config(path)107 config = load_trajectory_config(path)
100 assert config["trajectory_max_angle"] == 12.0108 assert config["trajectory_max_angle"] == 12.0
101 assert config["save_npz"] is False109 assert config["save_npz"] is False
102
103
104def test_bad_value_is_rejected() -> None:
105 with pytest.raises(TrajectoryConfigError):
106 normalize_trajectory_config({"trajectory_max_angle": "not-a-float"})
107 with pytest.raises(TrajectoryConfigError):
108 normalize_trajectory_config({"save_npz": "flase"})