From 4d748363e86ee4707f1f688b33274981fadf27c5 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 8 Oct 2026 13:01:03 +0200 Subject: [PATCH 1/3] Fix issue 324 and add tests --- src/easyscience/base_classes/new_base.py | 11 +++---- src/easyscience/io/serializer_base.py | 4 +-- tests/unit/base_classes/test_new_base.py | 37 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/easyscience/base_classes/new_base.py b/src/easyscience/base_classes/new_base.py index 115677f1..e1797166 100644 --- a/src/easyscience/base_classes/new_base.py +++ b/src/easyscience/base_classes/new_base.py @@ -139,13 +139,14 @@ def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: EasyScience object. """ serializer = SerializerBase() + temp_dict = serializer._convert_to_dict(self, skip=skip, full_encode=False) if skip is None: skip = [] - if self._default_unique_name and 'unique_name' not in skip: - skip.append('unique_name') - if self._display_name is None: - skip.append('display_name') - return serializer._convert_to_dict(self, skip=skip, full_encode=False) + if self._default_unique_name or 'unique_name' in skip: + temp_dict.pop('unique_name', None) + if self._display_name is None or 'display_name' in skip: + temp_dict.pop('display_name', None) + return temp_dict @classmethod def from_dict(cls, obj_dict: Dict[str, Any]) -> NewBase: diff --git a/src/easyscience/io/serializer_base.py b/src/easyscience/io/serializer_base.py index 5d9b0e51..3fc22d06 100644 --- a/src/easyscience/io/serializer_base.py +++ b/src/easyscience/io/serializer_base.py @@ -434,7 +434,7 @@ def _recursive_encoder( ): # strings have encode return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) elif hasattr(obj, 'to_dict') and obj.__class__.__module__.startswith('easy'): - return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) + return obj.to_dict(skip=skip) else: return [ self._recursive_encoder(it, skip, encoder, full_encode, **kwargs) for it in obj @@ -449,5 +449,5 @@ def _recursive_encoder( ): # strings have encode return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) elif hasattr(obj, 'to_dict') and obj.__class__.__module__.startswith('easy'): - return encoder._convert_to_dict(obj, skip, full_encode, **kwargs) + return obj.to_dict(skip=skip) return obj diff --git a/tests/unit/base_classes/test_new_base.py b/tests/unit/base_classes/test_new_base.py index e1562860..7d74cac2 100644 --- a/tests/unit/base_classes/test_new_base.py +++ b/tests/unit/base_classes/test_new_base.py @@ -7,6 +7,12 @@ from easyscience.base_classes import NewBase +class NestedNewBase(NewBase): + def __init__(self, unique_name=None, display_name=None, nested_new_base=None): + super().__init__(unique_name=unique_name, display_name=display_name) + self.nested_new_base = nested_new_base or NewBase() + + class TestNewBase: @pytest.fixture def clear(self): @@ -132,6 +138,37 @@ def test_to_dict_with_skip(self): assert obj_dict['@class'] == 'NewBase' assert '@version' in obj_dict + def test_to_dict_nested_new_base(self): + # When + obj = NewBase() + parent_obj = NestedNewBase(unique_name='parent', display_name='Parent Object') + parent_obj.nested_new_base = obj + # Then + obj_dict = parent_obj.to_dict() + # Expect + assert isinstance(obj_dict, dict) + assert 'nested_new_base' in obj_dict + assert 'unique_name' in obj_dict + assert 'display_name' in obj_dict + assert 'unique_name' not in obj_dict['nested_new_base'] + assert 'display_name' not in obj_dict['nested_new_base'] + + def test_to_dict_nested_new_base_reverse(self): + # When + obj = NewBase(unique_name='child', display_name='Child Object') + parent_obj = NestedNewBase() + parent_obj.nested_new_base = obj + # Then + obj_dict = parent_obj.to_dict() + # Expect + assert isinstance(obj_dict, dict) + assert 'nested_new_base' in obj_dict + assert 'unique_name' not in obj_dict + assert 'display_name' not in obj_dict + assert 'unique_name' in obj_dict['nested_new_base'] + assert 'display_name' in obj_dict['nested_new_base'] + + def test_from_dict(self): # When obj_dict = { From 25a5f091d44afc725263932dd9d9e5bea8223d5a Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Thu, 8 Oct 2026 14:43:56 +0200 Subject: [PATCH 2/3] Add `skip` to easy_lists `to_dict`. --- src/easyscience/base_classes/easy_list.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/easyscience/base_classes/easy_list.py b/src/easyscience/base_classes/easy_list.py index 1b99ab85..2effca51 100644 --- a/src/easyscience/base_classes/easy_list.py +++ b/src/easyscience/base_classes/easy_list.py @@ -356,22 +356,31 @@ def pop(self, index: int | str = -1) -> ProtectedType_: # Serialization support - def to_dict(self) -> dict: + def to_dict(self, skip: list[str] | None = None) -> dict: """ Convert the EasyList to a dictionary for serialization. + Parameters + ---------- + skip : list[str] | None, default=None + List of field names as strings to skip when forming the + dictionary. + Returns ------- dict Dictionary representation of the EasyList. """ - dict_repr = super().to_dict() + skip = list(skip or []) + # protected_types holds classes, which the generic encoder + # cannot handle; they are encoded explicitly below. + dict_repr = super().to_dict(skip=[*skip, 'protected_types']) if self._protected_types != [NewBase]: dict_repr['protected_types'] = [ {'@module': cls_.__module__, '@class': cls_.__name__} for cls_ in self._protected_types ] # noqa: E501 - dict_repr['data'] = [item.to_dict() for item in self._data] + dict_repr['data'] = [item.to_dict(skip=list(skip)) for item in self._data] return dict_repr @classmethod From cbe1e3f13afd753bf6476c10cfd2f01df0dc10e6 Mon Sep 17 00:00:00 2001 From: Christian Vedel Date: Fri, 9 Oct 2026 11:47:28 +0200 Subject: [PATCH 3/3] Add some easylist tests --- tests/unit/base_classes/test_easy_list.py | 28 ++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/unit/base_classes/test_easy_list.py b/tests/unit/base_classes/test_easy_list.py index e3f7b35f..978d5571 100644 --- a/tests/unit/base_classes/test_easy_list.py +++ b/tests/unit/base_classes/test_easy_list.py @@ -32,10 +32,13 @@ def __init__(self, unique_name=None, display_name=None): class MockModel(ModelBase): """A ModelBase subclass with a Parameter and a DescriptorNumber for testing get_all_variables.""" - def __init__(self, unique_name=None, display_name=None, temperature=25, volume=1.0): + def __init__(self, unique_name=None, display_name=None, temperature=25, volume=1.0, items=None): super().__init__(unique_name=unique_name, display_name=display_name) self._temperature = Parameter(name='temperature', value=temperature) self._volume = DescriptorNumber(name='volume', value=volume) + if items is None: + items = EasyList() + self._items = items @property def temperature(self): @@ -557,6 +560,29 @@ def test_to_dict_default_protected_type_not_serialized(self): d = el.to_dict() assert 'protected_types' not in d + def test_to_dict_as_attribute(self): + # When + el = EasyList(Alpha(unique_name='a1'), unique_name='my_list', protected_types=Alpha) + model = MockModel(unique_name='m1', temperature=10, volume=5.0, items=el) + # Then + d = model.to_dict() + assert 'items' in d + assert len(d['items']['data']) == 1 + assert d['items']['protected_types'][0]['@class'] == 'Alpha' + + def test_to_dict_as_attribute_with_skip(self): + # When + el = EasyList(Alpha(unique_name='a1'), unique_name='my_list', protected_types=Alpha) + model = MockModel(unique_name='m1', temperature=10, volume=5.0, items=el) + # Then + d = model.to_dict(skip=['unique_name']) + assert 'unique_name' not in d + assert 'items' in d + assert len(d['items']['data']) == 1 + assert d['items']['protected_types'][0]['@class'] == 'Alpha' + assert 'unique_name' not in d['items'] + assert 'unique_name' not in d['items']['data'][0] + def test_from_dict_round_trip(self): a1 = Alpha(unique_name='a1') a2 = Alpha(unique_name='a2')