Miroslav Simko <ms@iolabs.ch> 2026-09-02T09:12:22+02:00
Commit #73 ยท 12 snippets
src/train/config.py | 10 ++------- src/train/config_schema.py | 19 +++++++++++++++++ src/train/config_sections.py | 49 +++++++++++++++++++++++++++++++++++++++----- src/train/config_values.py | 47 ++++++++++++++++++++++++++++++++++++++++++ tests/test_config.py | 34 ++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 13 deletions(-)
| 27 | from iolabs.common import config_loader | 27 | from iolabs.common import config_loader |
| 28 | 28 | ||
| 29 | from src.train import ( | 29 | from src.train import ( |
| 30 | config_rules, | 30 | config_rules, |
| 31 | config_sections, | ||
| 32 | config_study, | 31 | config_study, |
| 33 | config_values, | 32 | config_values, |
| 34 | ) | 33 | ) |
| 35 | from src.train.config_rules import is_canonical_metric, resolve_ontology_path | 34 | from src.train.config_rules import is_canonical_metric, resolve_ontology_path |
| 263 | 262 | ||
| 264 | def _build_config(raw: Any, config_path: Path, sha256: str) -> HarnessConfig: | 263 | def _build_config(raw: Any, config_path: Path, sha256: str) -> HarnessConfig: |
| 265 | """Validate one raw document into a configuration carrying its digest.""" | 264 | """Validate one raw document into a configuration carrying its digest.""" |
| 266 | root = config_values.mapping(raw, str(config_path)) | 265 | root = config_values.mapping(raw, str(config_path)) |
| 267 | payload = { | ||
| 268 | **root, | ||
| 269 | config_sections.SOURCE_PATH_ALIAS: config_path, | ||
| 270 | config_sections.SHA256_ALIAS: sha256, | ||
| 271 | } | ||
| 272 | config = config_loader.validate_config( | 266 | config = config_loader.validate_config( |
| 273 | HarnessConfig, payload, context=_CONTEXT, error_cls=ConfigError | 267 | HarnessConfig, dict(root), context=_CONTEXT, error_cls=ConfigError |
| 274 | ) | 268 | ) |
| 269 | config.attach_provenance(config_path, sha256, copy.deepcopy(dict(root))) | ||
| 275 | config_rules.validate_config(config, root) | 270 | config_rules.validate_config(config, root) |
| 276 | config._raw_document = copy.deepcopy(dict(root)) | ||
| 277 | return config | 271 | return config |
| 278 | 272 | ||
| 279 | 273 | ||
| 280 | def _raw_document(config: HarnessConfig) -> Mapping[str, Any]: | 274 | def _raw_document(config: HarnessConfig) -> Mapping[str, Any]: |
| 36 | if field is None or field.annotation is None: | 36 | if field is None or field.annotation is None: |
| 37 | return value | 37 | return value |
| 38 | return config_values.typed_value(field.annotation, value, name) | 38 | return config_values.typed_value(field.annotation, value, name) |
| 39 | 39 | ||
| 40 | @pydantic.model_validator(mode="after") | ||
| 41 | def _freeze_mapping_fields(self) -> StrictConfigModel: | ||
| 42 | """Replace validated mapping fields with read-only views.""" | ||
| 43 | for name in type(self).model_fields: | ||
| 44 | value = getattr(self, name) | ||
| 45 | frozen = config_values.freeze_value(value) | ||
| 46 | if frozen is not value: | ||
| 47 | object.__setattr__(self, name, frozen) | ||
| 48 | return self | ||
| 49 | |||
| 50 | @pydantic.field_serializer("*", mode="wrap") | ||
| 51 | def _serialize_frozen_mappings( | ||
| 52 | self, | ||
| 53 | value: Any, | ||
| 54 | handler: pydantic.SerializerFunctionWrapHandler, | ||
| 55 | ) -> Any: | ||
| 56 | """Serialize read-only mapping views as plain dicts.""" | ||
| 57 | return handler(config_values.thaw_value(value)) | ||
| 58 | |||
| 40 | 59 | ||
| 41 | class SptPartitionOracleParams(StrictConfigModel): | 60 | class SptPartitionOracleParams(StrictConfigModel): |
| 42 | """Required SPT partition-purity report and per-class thresholds.""" | 61 | """Required SPT partition-purity report and per-class thresholds.""" |
| 43 | 62 |
| 18 | from src.train import config_schema, config_values | 18 | from src.train import config_schema, config_values |
| 19 | 19 | ||
| 20 | logger = logging.getLogger(__name__) | 20 | logger = logging.getLogger(__name__) |
| 21 | 21 | ||
| 22 | SOURCE_PATH_ALIAS = "__source_path__" | ||
| 23 | SHA256_ALIAS = "__sha256__" | ||
| 24 | _MODEL_ARG_KEYS = frozenset( | 22 | _MODEL_ARG_KEYS = frozenset( |
| 25 | { | 23 | { |
| 26 | "aggregation", "annotation_mode", "balanced_crops", | 24 | "aggregation", "annotation_mode", "balanced_crops", |
| 27 | "confidence_only_forbidden", "enable_flash", "geometry_context", | 25 | "confidence_only_forbidden", "enable_flash", "geometry_context", |
| 402 | train: TrainConfig | 400 | train: TrainConfig |
| 403 | evaluation: EvaluationConfig | 401 | evaluation: EvaluationConfig |
| 404 | runtime: RuntimeConfig | 402 | runtime: RuntimeConfig |
| 405 | provenance: ProvenanceConfig | 403 | provenance: ProvenanceConfig |
| 406 | source_path: Path = pydantic.Field(validation_alias=SOURCE_PATH_ALIAS) | ||
| 407 | sha256: str = pydantic.Field(validation_alias=SHA256_ALIAS) | ||
| 408 | visualization: VisualizationConfig | None = None | 404 | visualization: VisualizationConfig | None = None |
| 409 | 405 | ||
| 406 | _source_path: Path = pydantic.PrivateAttr(default=Path()) | ||
| 407 | _sha256: str = pydantic.PrivateAttr(default="") | ||
| 410 | _raw_document: Mapping[str, Any] | None = pydantic.PrivateAttr(default=None) | 408 | _raw_document: Mapping[str, Any] | None = pydantic.PrivateAttr(default=None) |
| 411 | 409 | ||
| 410 | @pydantic.model_validator(mode="before") | ||
| 411 | @classmethod | ||
| 412 | def _reject_null_sections(cls, data: Any) -> Any: | ||
| 413 | """Reject an explicitly null optional section, as the old parser did.""" | ||
| 414 | if isinstance(data, Mapping) and data.get("visualization", False) is None: | ||
| 415 | raise ValueError("visualization must be a mapping") | ||
| 416 | return data | ||
| 417 | |||
| 418 | @property | ||
| 419 | def source_path(self) -> Path: | ||
| 420 | """Path of the YAML document this configuration was parsed from.""" | ||
| 421 | return self._source_path | ||
| 422 | |||
| 423 | @property | ||
| 424 | def sha256(self) -> str: | ||
| 425 | """SHA-256 digest of the resolved document this configuration holds.""" | ||
| 426 | return self._sha256 | ||
| 427 | |||
| 428 | def attach_provenance( | ||
| 429 | self, | ||
| 430 | source_path: Path, | ||
| 431 | sha256: str, | ||
| 432 | raw_document: Mapping[str, Any] | None = None, | ||
| 433 | ) -> None: | ||
| 434 | """Record where this configuration came from and what it hashes to. | ||
| 435 | |||
| 436 | Provenance is kept out of the validated field set so its keys can never | ||
| 437 | be smuggled in through the YAML document. | ||
| 438 | |||
| 439 | Args: | ||
| 440 | source_path: Path the document was read from. | ||
| 441 | sha256: Digest of the resolved document. | ||
| 442 | raw_document: Raw mapping the model was validated from, if known. | ||
| 443 | """ | ||
| 444 | self._source_path = source_path | ||
| 445 | self._sha256 = sha256 | ||
| 446 | self._raw_document = raw_document | ||
| 447 | |||
| 412 | def as_dict(self) -> dict[str, Any]: | 448 | def as_dict(self) -> dict[str, Any]: |
| 413 | """Return the resolved model tree as JSON-safe primitives.""" | 449 | """Return the resolved model tree as JSON-safe primitives.""" |
| 414 | return self.model_dump(mode="json") | 450 | payload = self.model_dump(mode="json") |
| 451 | payload["source_path"] = self._source_path.as_posix() | ||
| 452 | payload["sha256"] = self._sha256 | ||
| 453 | return payload |
| 27 | class ConfigError(config_loader.ConfigError): | 27 | class ConfigError(config_loader.ConfigError): |
| 28 | """Raised when an experiment configuration violates its strict schema.""" | 28 | """Raised when an experiment configuration violates its strict schema.""" |
| 29 | 29 | ||
| 30 | 30 | ||
| 31 | def freeze_value(value: Any) -> Any: | ||
| 32 | """Return *value* with every nested mapping wrapped read-only. | ||
| 33 | |||
| 34 | Pydantic rebuilds mapping fields as plain dicts, so a frozen model would | ||
| 35 | otherwise hand out mutable ``args``/``overrides`` tables. Sequences are | ||
| 36 | rebuilt with the same container type so declared list fields stay lists. | ||
| 37 | |||
| 38 | Args: | ||
| 39 | value: Any validated field value. | ||
| 40 | |||
| 41 | Returns: | ||
| 42 | The value with dicts replaced by :class:`~types.MappingProxyType`. | ||
| 43 | """ | ||
| 44 | if isinstance(value, MappingProxyType): | ||
| 45 | return value | ||
| 46 | if isinstance(value, Mapping): | ||
| 47 | return MappingProxyType( | ||
| 48 | {key: freeze_value(item) for key, item in value.items()} | ||
| 49 | ) | ||
| 50 | if isinstance(value, tuple): | ||
| 51 | return tuple(freeze_value(item) for item in value) | ||
| 52 | if isinstance(value, list): | ||
| 53 | return [freeze_value(item) for item in value] | ||
| 54 | return value | ||
| 55 | |||
| 56 | |||
| 57 | def thaw_value(value: Any) -> Any: | ||
| 58 | """Return *value* with every read-only mapping view turned back into a dict. | ||
| 59 | |||
| 60 | Pydantic's serializer cannot handle ``mappingproxy``, so frozen fields are | ||
| 61 | thawed on the way out of :meth:`~pydantic.BaseModel.model_dump`. | ||
| 62 | |||
| 63 | Args: | ||
| 64 | value: Any validated field value. | ||
| 65 | |||
| 66 | Returns: | ||
| 67 | The value with :class:`~types.MappingProxyType` replaced by dicts. | ||
| 68 | """ | ||
| 69 | if isinstance(value, Mapping): | ||
| 70 | return {key: thaw_value(item) for key, item in value.items()} | ||
| 71 | if isinstance(value, tuple): | ||
| 72 | return tuple(thaw_value(item) for item in value) | ||
| 73 | if isinstance(value, list): | ||
| 74 | return [thaw_value(item) for item in value] | ||
| 75 | return value | ||
| 76 | |||
| 77 | |||
| 31 | def typed_value(annotation: Any, value: Any, where: str) -> Any: | 78 | def typed_value(annotation: Any, value: Any, where: str) -> Any: |
| 32 | """Parse one config leaf strictly as its declared annotation. | 79 | """Parse one config leaf strictly as its declared annotation. |
| 33 | 80 | ||
| 34 | Nested models, ``Annotated`` aliases and unresolved annotations are | 81 | Nested models, ``Annotated`` aliases and unresolved annotations are |
| 607 | config = load_config(config_path) | 607 | config = load_config(config_path) |
| 608 | 608 | ||
| 609 | assert config.task.ontology == Path("configs/contracts/ontology_v1.yaml") | 609 | assert config.task.ontology == Path("configs/contracts/ontology_v1.yaml") |
| 610 | assert config.task.num_classes == 9 | 610 | assert config.task.num_classes == 9 |
| 611 | |||
| 612 | |||
| 613 | def test_provenance_keys_cannot_be_smuggled_through_the_document( | ||
| 614 | tmp_path: Path, | ||
| 615 | ) -> None: | ||
| 616 | """source_path/sha256 are recorded out of band, never taken from the YAML.""" | ||
| 617 | raw = yaml.safe_load(CONFIGS[0].read_text(encoding="utf-8")) | ||
| 618 | raw["__sha256__"] = "deadbeef" | ||
| 619 | path = tmp_path / "smuggled.yaml" | ||
| 620 | path.write_text(yaml.safe_dump(raw), encoding="utf-8") | ||
| 621 | with pytest.raises(ConfigError, match="Unknown.*__sha256__"): | ||
| 622 | load_config(path) | ||
| 623 | |||
| 624 | |||
| 625 | def test_null_visualization_block_is_rejected(tmp_path: Path) -> None: | ||
| 626 | """An explicitly null visualization block is a typo, not an omission.""" | ||
| 627 | raw = yaml.safe_load(CONFIGS[8].read_text(encoding="utf-8")) | ||
| 628 | raw["visualization"] = None | ||
| 629 | path = tmp_path / "viz_null.yaml" | ||
| 630 | path.write_text(yaml.safe_dump(raw), encoding="utf-8") | ||
| 631 | with pytest.raises(ConfigError, match="visualization must be a mapping"): | ||
| 632 | load_config(path) | ||
| 633 | |||
| 634 | |||
| 635 | def test_mapping_sections_are_read_only() -> None: | ||
| 636 | """Frozen configs hand out read-only mappings, not mutable dicts.""" | ||
| 637 | config = load_config(CONFIGS[0]) | ||
| 638 | with pytest.raises(TypeError): | ||
| 639 | config.model.args["num_classes"] = 99 # type: ignore[index] | ||
| 640 | with pytest.raises(TypeError): | ||
| 641 | config.loss.args["gamma"] = 0.0 # type: ignore[index] | ||
| 642 | with pytest.raises(TypeError): | ||
| 643 | config.evaluation.precision_floors["curb"] = 0.0 # type: ignore[index] | ||
| 644 | assert isinstance(config.as_dict()["model"]["args"], dict) |
| 36 | if field is None or field.annotation is None: | 36 | if field is None or field.annotation is None: |
| 37 | return value | 37 | return value |
| 38 | return config_values.typed_value(field.annotation, value, name) | 38 | return config_values.typed_value(field.annotation, value, name) |
| 39 | 39 | ||
| 40 | @pydantic.model_validator(mode="after") | ||
| 41 | def _freeze_mapping_fields(self) -> StrictConfigModel: | ||
| 42 | """Replace validated mapping fields with read-only views.""" | ||
| 43 | for name in type(self).model_fields: | ||
| 44 | value = getattr(self, name) | ||
| 45 | frozen = config_values.freeze_value(value) | ||
| 46 | if frozen is not value: | ||
| 47 | object.__setattr__(self, name, frozen) | ||
| 48 | return self | ||
| 49 | |||
| 50 | @pydantic.field_serializer("*", mode="wrap") | ||
| 51 | def _serialize_frozen_mappings( | ||
| 52 | self, | ||
| 53 | value: Any, | ||
| 54 | handler: pydantic.SerializerFunctionWrapHandler, | ||
| 55 | ) -> Any: | ||
| 56 | """Serialize read-only mapping views as plain dicts.""" | ||
| 57 | return handler(config_values.thaw_value(value)) | ||
| 58 | |||
| 40 | 59 | ||
| 41 | class SptPartitionOracleParams(StrictConfigModel): | 60 | class SptPartitionOracleParams(StrictConfigModel): |
| 42 | """Required SPT partition-purity report and per-class thresholds.""" | 61 | """Required SPT partition-purity report and per-class thresholds.""" |
| 43 | 62 |
| 18 | from src.train import config_schema, config_values | 18 | from src.train import config_schema, config_values |
| 19 | 19 | ||
| 20 | logger = logging.getLogger(__name__) | 20 | logger = logging.getLogger(__name__) |
| 21 | 21 | ||
| 22 | SOURCE_PATH_ALIAS = "__source_path__" | ||
| 23 | SHA256_ALIAS = "__sha256__" | ||
| 24 | _MODEL_ARG_KEYS = frozenset( | 22 | _MODEL_ARG_KEYS = frozenset( |
| 25 | { | 23 | { |
| 26 | "aggregation", "annotation_mode", "balanced_crops", | 24 | "aggregation", "annotation_mode", "balanced_crops", |
| 27 | "confidence_only_forbidden", "enable_flash", "geometry_context", | 25 | "confidence_only_forbidden", "enable_flash", "geometry_context", |
| 402 | train: TrainConfig | 400 | train: TrainConfig |
| 403 | evaluation: EvaluationConfig | 401 | evaluation: EvaluationConfig |
| 404 | runtime: RuntimeConfig | 402 | runtime: RuntimeConfig |
| 405 | provenance: ProvenanceConfig | 403 | provenance: ProvenanceConfig |
| 406 | source_path: Path = pydantic.Field(validation_alias=SOURCE_PATH_ALIAS) | ||
| 407 | sha256: str = pydantic.Field(validation_alias=SHA256_ALIAS) | ||
| 408 | visualization: VisualizationConfig | None = None | 404 | visualization: VisualizationConfig | None = None |
| 409 | 405 | ||
| 406 | _source_path: Path = pydantic.PrivateAttr(default=Path()) | ||
| 407 | _sha256: str = pydantic.PrivateAttr(default="") | ||
| 410 | _raw_document: Mapping[str, Any] | None = pydantic.PrivateAttr(default=None) | 408 | _raw_document: Mapping[str, Any] | None = pydantic.PrivateAttr(default=None) |
| 411 | 409 | ||
| 410 | @pydantic.model_validator(mode="before") | ||
| 411 | @classmethod | ||
| 412 | def _reject_null_sections(cls, data: Any) -> Any: | ||
| 413 | """Reject an explicitly null optional section, as the old parser did.""" | ||
| 414 | if isinstance(data, Mapping) and data.get("visualization", False) is None: | ||
| 415 | raise ValueError("visualization must be a mapping") | ||
| 416 | return data | ||
| 417 | |||
| 418 | @property | ||
| 419 | def source_path(self) -> Path: | ||
| 420 | """Path of the YAML document this configuration was parsed from.""" | ||
| 421 | return self._source_path | ||
| 422 | |||
| 423 | @property | ||
| 424 | def sha256(self) -> str: | ||
| 425 | """SHA-256 digest of the resolved document this configuration holds.""" | ||
| 426 | return self._sha256 | ||
| 427 | |||
| 428 | def attach_provenance( | ||
| 429 | self, | ||
| 430 | source_path: Path, | ||
| 431 | sha256: str, | ||
| 432 | raw_document: Mapping[str, Any] | None = None, | ||
| 433 | ) -> None: | ||
| 434 | """Record where this configuration came from and what it hashes to. | ||
| 435 | |||
| 436 | Provenance is kept out of the validated field set so its keys can never | ||
| 437 | be smuggled in through the YAML document. | ||
| 438 | |||
| 439 | Args: | ||
| 440 | source_path: Path the document was read from. | ||
| 441 | sha256: Digest of the resolved document. | ||
| 442 | raw_document: Raw mapping the model was validated from, if known. | ||
| 443 | """ | ||
| 444 | self._source_path = source_path | ||
| 445 | self._sha256 = sha256 | ||
| 446 | self._raw_document = raw_document | ||
| 447 | |||
| 412 | def as_dict(self) -> dict[str, Any]: | 448 | def as_dict(self) -> dict[str, Any]: |
| 413 | """Return the resolved model tree as JSON-safe primitives.""" | 449 | """Return the resolved model tree as JSON-safe primitives.""" |
| 414 | return self.model_dump(mode="json") | 450 | payload = self.model_dump(mode="json") |
| 451 | payload["source_path"] = self._source_path.as_posix() | ||
| 452 | payload["sha256"] = self._sha256 | ||
| 453 | return payload |
| 27 | class ConfigError(config_loader.ConfigError): | 27 | class ConfigError(config_loader.ConfigError): |
| 28 | """Raised when an experiment configuration violates its strict schema.""" | 28 | """Raised when an experiment configuration violates its strict schema.""" |
| 29 | 29 | ||
| 30 | 30 | ||
| 31 | def freeze_value(value: Any) -> Any: | ||
| 32 | """Return *value* with every nested mapping wrapped read-only. | ||
| 33 | |||
| 34 | Pydantic rebuilds mapping fields as plain dicts, so a frozen model would | ||
| 35 | otherwise hand out mutable ``args``/``overrides`` tables. Sequences are | ||
| 36 | rebuilt with the same container type so declared list fields stay lists. | ||
| 37 | |||
| 38 | Args: | ||
| 39 | value: Any validated field value. | ||
| 40 | |||
| 41 | Returns: | ||
| 42 | The value with dicts replaced by :class:`~types.MappingProxyType`. | ||
| 43 | """ | ||
| 44 | if isinstance(value, MappingProxyType): | ||
| 45 | return value | ||
| 46 | if isinstance(value, Mapping): | ||
| 47 | return MappingProxyType( | ||
| 48 | {key: freeze_value(item) for key, item in value.items()} | ||
| 49 | ) | ||
| 50 | if isinstance(value, tuple): | ||
| 51 | return tuple(freeze_value(item) for item in value) | ||
| 52 | if isinstance(value, list): | ||
| 53 | return [freeze_value(item) for item in value] | ||
| 54 | return value | ||
| 55 | |||
| 56 | |||
| 57 | def thaw_value(value: Any) -> Any: | ||
| 58 | """Return *value* with every read-only mapping view turned back into a dict. | ||
| 59 | |||
| 60 | Pydantic's serializer cannot handle ``mappingproxy``, so frozen fields are | ||
| 61 | thawed on the way out of :meth:`~pydantic.BaseModel.model_dump`. | ||
| 62 | |||
| 63 | Args: | ||
| 64 | value: Any validated field value. | ||
| 65 | |||
| 66 | Returns: | ||
| 67 | The value with :class:`~types.MappingProxyType` replaced by dicts. | ||
| 68 | """ | ||
| 69 | if isinstance(value, Mapping): | ||
| 70 | return {key: thaw_value(item) for key, item in value.items()} | ||
| 71 | if isinstance(value, tuple): | ||
| 72 | return tuple(thaw_value(item) for item in value) | ||
| 73 | if isinstance(value, list): | ||
| 74 | return [thaw_value(item) for item in value] | ||
| 75 | return value | ||
| 76 | |||
| 77 | |||
| 31 | def typed_value(annotation: Any, value: Any, where: str) -> Any: | 78 | def typed_value(annotation: Any, value: Any, where: str) -> Any: |
| 32 | """Parse one config leaf strictly as its declared annotation. | 79 | """Parse one config leaf strictly as its declared annotation. |
| 33 | 80 | ||
| 34 | Nested models, ``Annotated`` aliases and unresolved annotations are | 81 | Nested models, ``Annotated`` aliases and unresolved annotations are |
| 607 | config = load_config(config_path) | 607 | config = load_config(config_path) |
| 608 | 608 | ||
| 609 | assert config.task.ontology == Path("configs/contracts/ontology_v1.yaml") | 609 | assert config.task.ontology == Path("configs/contracts/ontology_v1.yaml") |
| 610 | assert config.task.num_classes == 9 | 610 | assert config.task.num_classes == 9 |
| 611 | |||
| 612 | |||
| 613 | def test_provenance_keys_cannot_be_smuggled_through_the_document( | ||
| 614 | tmp_path: Path, | ||
| 615 | ) -> None: | ||
| 616 | """source_path/sha256 are recorded out of band, never taken from the YAML.""" | ||
| 617 | raw = yaml.safe_load(CONFIGS[0].read_text(encoding="utf-8")) | ||
| 618 | raw["__sha256__"] = "deadbeef" | ||
| 619 | path = tmp_path / "smuggled.yaml" | ||
| 620 | path.write_text(yaml.safe_dump(raw), encoding="utf-8") | ||
| 621 | with pytest.raises(ConfigError, match="Unknown.*__sha256__"): | ||
| 622 | load_config(path) | ||
| 623 | |||
| 624 | |||
| 625 | def test_null_visualization_block_is_rejected(tmp_path: Path) -> None: | ||
| 626 | """An explicitly null visualization block is a typo, not an omission.""" | ||
| 627 | raw = yaml.safe_load(CONFIGS[8].read_text(encoding="utf-8")) | ||
| 628 | raw["visualization"] = None | ||
| 629 | path = tmp_path / "viz_null.yaml" | ||
| 630 | path.write_text(yaml.safe_dump(raw), encoding="utf-8") | ||
| 631 | with pytest.raises(ConfigError, match="visualization must be a mapping"): | ||
| 632 | load_config(path) | ||
| 633 | |||
| 634 | |||
| 635 | def test_mapping_sections_are_read_only() -> None: | ||
| 636 | """Frozen configs hand out read-only mappings, not mutable dicts.""" | ||
| 637 | config = load_config(CONFIGS[0]) | ||
| 638 | with pytest.raises(TypeError): | ||
| 639 | config.model.args["num_classes"] = 99 # type: ignore[index] | ||
| 640 | with pytest.raises(TypeError): | ||
| 641 | config.loss.args["gamma"] = 0.0 # type: ignore[index] | ||
| 642 | with pytest.raises(TypeError): | ||
| 643 | config.evaluation.precision_floors["curb"] = 0.0 # type: ignore[index] | ||
| 644 | assert isinstance(config.as_dict()["model"]["args"], dict) |
nullvisualization rejected.