diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index e6db237063252bdc2359b1ac512134aa2b0080ae..355c6b984e70f8c57fd3dbdd7dd63e7d5ecf2919 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -1782,6 +1782,47 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas else: self.m_set(prop, value) + def m_setdefault(self, path): + '''Given a root section and a path, looks if a unique section can be found + under that path and returns it. Will create the sections along the path if + no instances are found. + ''' + parts = path.split(".") + root = self + for part in parts: + error = AttributeError(f'Could not find section definition for path "{part}"') + try: + child = getattr(root, part) + except Exception as e: + raise error from e + sub_sections = root.m_def.all_sub_sections + try: + child_section = sub_sections[part] + except Exception as e: + raise error from e + repeats = child_section.repeats + + # See if child exists. Repeating subsection is accepted only if + # there is one instance. + if child: + if repeats: + if len(child) != 1: + raise ValueError(f'Cannot resolve "{part}" as several instances were found') + root = child[0] + else: + root = child + # Otherwise create child + else: + child_cls = child_section.sub_section.section_cls + child_instance = child_cls() + if repeats: + root.m_add_sub_section(child_section, child_instance) + else: + setattr(root, part, child_instance) + root = child_instance + + return root + def m_as(self, section_cls: Type[MSectionBound]) -> MSectionBound: ''' 'Casts' this section to the given extending sections. ''' return cast(MSectionBound, self) diff --git a/tests/metainfo/test_metainfo.py b/tests/metainfo/test_metainfo.py index 55055b6398250adaddea83a329130502354e2ad7..9a610b26bc461224852899e2830c66e3556eba31 100644 --- a/tests/metainfo/test_metainfo.py +++ b/tests/metainfo/test_metainfo.py @@ -137,7 +137,7 @@ class TestM2: def test_unset_sub_section(self): run = Run() - assert run.systems == [] + assert run.systems == [] # pylint: disable=use-implicit-booleaness-not-comparison assert run.parsing is None def test_properties(self): @@ -426,6 +426,14 @@ class TestM2: assert 'this_does_not_exist_in_metainfo' not in serialized +existing_repeating = Run() +existing_repeating.systems.append(System()) +existing_nonrepeating = Run() +existing_nonrepeating.parsing = Parsing() +existing_multiple = Run() +existing_multiple.systems = [System(), System()] + + class TestM1: ''' Test for meta-info instances. ''' @@ -488,7 +496,7 @@ class TestM1: def test_sub_section_lst(self): run = Run() - assert run.systems == [] + assert run.systems == [] # pylint: disable=use-implicit-booleaness-not-comparison run.systems.append(System()) assert len(run.systems) == 1 @@ -817,6 +825,24 @@ class TestM1: assert parent.single_sub_section is not None assert len(parent.many_sub_section) == 2 + @pytest.mark.parametrize('root,path,exception', [ + pytest.param(Run(), 'parsing', None, id="non-existing non-repeating section"), + pytest.param(Run(), 'systems', None, id="non-existing repeating section"), + pytest.param(existing_nonrepeating, 'parsing', None, id="existing non-repeating section"), + pytest.param(existing_repeating, 'systems', None, id="existing repeating section"), + pytest.param(Run(), 'code_name', 'Could not find section definition for path "code_name"', id="cannot target quantity"), + pytest.param(Run(), 'missing', 'Could not find section definition for path "missing"', id="invalid path"), + pytest.param(existing_multiple, 'systems', 'Cannot resolve "systems" as several instances were found', id="ambiguous path"), + ]) + def test_m_setdefault(self, root, path, exception): + if not exception: + system = root.m_setdefault(path) + assert system + else: + with pytest.raises(Exception) as exc_info: + system = root.m_setdefault(path) + assert exception in str(exc_info.value) + class TestEnvironment: