From 25ab777c33dbd02c67136d7387383871886dc66e Mon Sep 17 00:00:00 2001 From: Victor Morand Date: Thu, 8 Oct 2026 16:14:34 +0200 Subject: [PATCH 1/4] feat: Warn loudly if unrecognized keys are found in configurations --- src/experimaestro/tests/test_grid_validation.py | 13 +++++++++++++ src/experimaestro/utils/config.py | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/experimaestro/tests/test_grid_validation.py b/src/experimaestro/tests/test_grid_validation.py index a7d49be7..5f6335fe 100644 --- a/src/experimaestro/tests/test_grid_validation.py +++ b/src/experimaestro/tests/test_grid_validation.py @@ -166,3 +166,16 @@ class CustomConfig(ConfigurationBase): assert tags[0] == {"lr": 0.05, "batch_size": 16} assert tags[1] == {"lr": 0.005, "batch_size": 64} + + +def test_validate_attrs_warns_on_unrecognized_keys(caplog): + data = { + "id": "test", + "lr": 0.05, + "unexpected_key_xyz": 123, + } + + cfg = validate_attrs(MainConfig, data) + assert cfg.id == "test" + assert any("WARNING: Unrecognized configuration keys" in record.message for record in caplog.records) + assert any("unexpected_key_xyz" in record.message for record in caplog.records) diff --git a/src/experimaestro/utils/config.py b/src/experimaestro/utils/config.py index 4fb92a13..b35526bb 100644 --- a/src/experimaestro/utils/config.py +++ b/src/experimaestro/utils/config.py @@ -164,6 +164,18 @@ def wrap_type(t: Any) -> Any: # Validate validated_model = PydanticModel(**data) + # Warn if unknown keys are passed + expected_field_names = {f.name for f in fields} + extra_keys = set(data.keys()) - expected_field_names + if extra_keys: + logger.warning( + "\n" + "=" * 80 + "\n" + f"⚠️ WARNING: Unrecognized configuration keys for {cls.__name__}:\n" + f" Unknown keys: {sorted(list(extra_keys))}\n" + f" Valid keys are: {sorted(list(expected_field_names))}\n" + + "=" * 80 + "\n" + ) + # Extract only the fields that the attrs class expects attrs_data = {} for f in fields: @@ -182,3 +194,4 @@ def wrap_type(t: Any) -> Any: attrs_data[f.name] = val return cls(**attrs_data) + From 579bcae868d179a64567648df86aed20f79694e1 Mon Sep 17 00:00:00 2001 From: Victor Morand Date: Fri, 9 Oct 2026 10:16:47 +0200 Subject: [PATCH 2/4] fix: Skip progress reporting if running outside an experimaestro task --- src/experimaestro/notifications.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/experimaestro/notifications.py b/src/experimaestro/notifications.py index 798bbf1b..ccd285c7 100644 --- a/src/experimaestro/notifications.py +++ b/src/experimaestro/notifications.py @@ -153,6 +153,9 @@ def progress(value: float, level=0, desc: Optional[str] = None, console=False): if TaskEnv.instance().slave: # Skip if in a slave process return + if TaskEnv.instance().taskpath is None: + # Skip progress reporting if running outside an experimaestro task (e.g. unit tests or standalone scripts) + return Reporter.instance().set_progress(value, level, desc, console=console) From 22e3c0c27df72f48d2897118b330284576a85768 Mon Sep 17 00:00:00 2001 From: Victor Morand Date: Fri, 9 Oct 2026 22:22:49 +0200 Subject: [PATCH 3/4] fix: implement __getstate__ for Identifier pickling --- src/experimaestro/core/types.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/experimaestro/core/types.py b/src/experimaestro/core/types.py index 6a8fd06d..b507edaa 100644 --- a/src/experimaestro/core/types.py +++ b/src/experimaestro/core/types.py @@ -60,6 +60,12 @@ def __hash__(self): def __eq__(self, other): return other.name.__eq__(self.name) + def __getstate__(self): + return self.name + + def __setstate__(self, state): + self.name = state + def __getattr__(self, key): return self(key) From 176ee9dc97cb0f81f1c3473b339cd96e47b580ec Mon Sep 17 00:00:00 2001 From: Victor Morand Date: Tue, 20 Oct 2026 09:21:42 +0200 Subject: [PATCH 4/4] fix[Configuration]: raise error if invalid keys are passed (and ruff) --- src/experimaestro/tests/test_grid_validation.py | 10 ++++------ src/experimaestro/tests/test_objects.py | 1 - src/experimaestro/tests/test_serializers.py | 3 +-- src/experimaestro/utils/config.py | 17 ++--------------- 4 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/experimaestro/tests/test_grid_validation.py b/src/experimaestro/tests/test_grid_validation.py index 5f6335fe..bf42fd01 100644 --- a/src/experimaestro/tests/test_grid_validation.py +++ b/src/experimaestro/tests/test_grid_validation.py @@ -2,7 +2,7 @@ from experimaestro.experiments.grid import GridSearch, GenericParams from experimaestro.experiments.configuration import ConfigurationBase, configuration from experimaestro.utils.config import validate_attrs - +import pytest @configuration() class SubConfig: @@ -168,14 +168,12 @@ class CustomConfig(ConfigurationBase): assert tags[1] == {"lr": 0.005, "batch_size": 64} -def test_validate_attrs_warns_on_unrecognized_keys(caplog): +def test_error_attrs_raises_on_unrecognized_keys(): data = { "id": "test", "lr": 0.05, "unexpected_key_xyz": 123, } - cfg = validate_attrs(MainConfig, data) - assert cfg.id == "test" - assert any("WARNING: Unrecognized configuration keys" in record.message for record in caplog.records) - assert any("unexpected_key_xyz" in record.message for record in caplog.records) + with pytest.raises(ValueError, match="unexpected_key_xyz"): + validate_attrs(MainConfig, data) diff --git a/src/experimaestro/tests/test_objects.py b/src/experimaestro/tests/test_objects.py index c1672cd8..27c04bf0 100644 --- a/src/experimaestro/tests/test_objects.py +++ b/src/experimaestro/tests/test_objects.py @@ -438,4 +438,3 @@ def test_field_names_and_load_objects_missing_defaults(): # In-place mutations on default containers work as expected inst.items.append("test") assert inst.items == ["test"] - diff --git a/src/experimaestro/tests/test_serializers.py b/src/experimaestro/tests/test_serializers.py index 712a97cf..ac9e3ca7 100644 --- a/src/experimaestro/tests/test_serializers.py +++ b/src/experimaestro/tests/test_serializers.py @@ -257,8 +257,7 @@ def test_taskstub_serialization_roundtrip(): [reloaded_config] = from_state_dict(data2, partial_loading=False) assert isinstance(reloaded_config.__xpm__.task, TaskStub) assert ( - reloaded_config.__xpm__.task.identifier - == loaded_config.__xpm__.task.identifier + reloaded_config.__xpm__.task.identifier == loaded_config.__xpm__.task.identifier ) assert reloaded_config.__xpm__.task.typename == loaded_config.__xpm__.task.typename diff --git a/src/experimaestro/utils/config.py b/src/experimaestro/utils/config.py index b35526bb..3a89949c 100644 --- a/src/experimaestro/utils/config.py +++ b/src/experimaestro/utils/config.py @@ -157,25 +157,13 @@ def wrap_type(t: Any) -> Any: # Create a dynamic Pydantic model PydanticModel = create_model( cls.__name__, - __config__=ConfigDict(extra="allow", arbitrary_types_allowed=True), + __config__=ConfigDict(extra="forbid", arbitrary_types_allowed=True), **model_fields, ) - # Validate + # Validate - will raise ValidationError if invalid keys or types are present validated_model = PydanticModel(**data) - # Warn if unknown keys are passed - expected_field_names = {f.name for f in fields} - extra_keys = set(data.keys()) - expected_field_names - if extra_keys: - logger.warning( - "\n" + "=" * 80 + "\n" - f"⚠️ WARNING: Unrecognized configuration keys for {cls.__name__}:\n" - f" Unknown keys: {sorted(list(extra_keys))}\n" - f" Valid keys are: {sorted(list(expected_field_names))}\n" - + "=" * 80 + "\n" - ) - # Extract only the fields that the attrs class expects attrs_data = {} for f in fields: @@ -194,4 +182,3 @@ def wrap_type(t: Any) -> Any: attrs_data[f.name] = val return cls(**attrs_data) -