diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index 00da3a39be927b3e482b95310a9b7fe198685e43..9733fbb5edd8ce81988abc9b70f2e51c519615d6 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -401,7 +401,7 @@ class Reference(DataType): class QuantityReference(Reference): ''' Datatype used for reference quantities that reference other quantities. ''' - def __init__(self, quantity_def: Union['Quantity']): + def __init__(self, quantity_def: 'Quantity'): super().__init__(cast(Section, quantity_def.m_parent)) self.target_quantity_def = quantity_def @@ -2063,6 +2063,10 @@ class Definition(MSection): sections, which organize the data (e.g. quantity values) and not the definitions of data (e.g. quantities definitions). See :ref:`metainfo-categories` for more details. + + more: A dictionary that contains additional definition properties that are not + part of the metainfo. Those can be passed as additional kwargs to definition + constructors. The values must be JSON serializable. ''' name: 'Quantity' = _placeholder_quantity @@ -2071,6 +2075,27 @@ class Definition(MSection): categories: 'Quantity' = _placeholder_quantity deprecated: 'Quantity' = _placeholder_quantity aliases: 'Quantity' = _placeholder_quantity + more: 'Quantity' = _placeholder_quantity + + def __init__(self, *args, **kwargs): + if is_bootstrapping: + super().__init__(*args, **kwargs) + return + + # We add all kwargs that are not meta props, annotations, or metainfo properties + # to the more property. + more = {} + new_kwargs = {} + for key, value in kwargs.items(): + if key.startswith('m_') or key.startswith('a_') or key in m_package.all_properties: + new_kwargs[key] = value + else: + more[key] = value + + if len(more) > 0: + new_kwargs['more'] = more + + super().__init__(*args, **new_kwargs) def __init_metainfo__(self): ''' @@ -2093,6 +2118,12 @@ class Definition(MSection): for content in self.m_all_contents(depth_first=True): content.__init_metainfo__() + def __getattr__(self, name): + if name in self.more: + return self.more[name] + + raise super().__getattr__(name) + def qualified_name(self): names = [] current = self @@ -2768,12 +2799,17 @@ class Package(Definition): all_definitions: A helper attribute that provides all section definitions by name. + + all_properties: A helper attribute that provides all properties in all sections + of this package by name. The values are lists of properties as property names + do not necesseraly need to be unique for different containing sections. ''' section_definitions: 'SubSection' = None category_definitions: 'SubSection' = None all_definitions: 'Quantity' = _placeholder_quantity + all_properties: 'Quantity' = _placeholder_quantity dependencies: 'Quantity' = _placeholder_quantity registry: Dict[str, 'Package'] = {} @@ -2893,6 +2929,7 @@ Definition.categories = Quantity( type=Reference(Category.m_def), shape=['0..*'], default=[], name='categories') Definition.deprecated = Quantity(type=str, name='deprecated') Definition.aliases = Quantity(type=str, shape=['0..*'], default=[], name='aliases') +Definition.more = Quantity(type=JSON, name='more', default={}) Section.quantities = SubSection( sub_section=Quantity.m_def, name='quantities', repeats=True) @@ -3023,6 +3060,17 @@ def all_definitions(self): return all_definitions +@derived(cached=True) +def package_all_properties(self): + all_properties: Dict[str, List[Property]] = dict() + for section_def in self.section_definitions: + for sub_section_def in [Section.quantities, Section.sub_sections]: + for property in section_def.m_get_sub_sections(sub_section_def): + properties = all_properties.setdefault(property.name, []) + properties.append(property) + return all_properties + + @derived(cached=True) def dependencies(self): ''' @@ -3060,6 +3108,7 @@ def dependencies(self): Package.all_definitions = all_definitions +Package.all_properties = package_all_properties Package.dependencies = dependencies is_bootstrapping = False @@ -3107,7 +3156,7 @@ class Environment(MSection): return [ definition - for definition in self.all_definitions_by_name.get(name, []) + for definition in self.all_definitions_by_name.get(name, []) # pylint: disable=no-member if isinstance(definition, section_cls) if not (isinstance(definition, Section) and definition.extends_base_section) if filter is None or filter(definition)] diff --git a/tests/metainfo/test_metainfo.py b/tests/metainfo/test_metainfo.py index 9f5430d62e025f2db30becea6af30a053a5477cf..98b0e71238973600f2f21977fef7944dd406e1db 100644 --- a/tests/metainfo/test_metainfo.py +++ b/tests/metainfo/test_metainfo.py @@ -313,6 +313,28 @@ class TestM2: assert len(TestSection.list_test_quantity.m_get_annotations(TestDefinitionAnnotation)) == 2 assert TestSection.test_sub_section.a_test is not None + def test_more_property(self): + class TestSection(MSection): + m_def = Section(this_does_not_exist_in_metainfo='value') + test_quantity = Quantity(type=str, also_no_metainfo_quantity=1, one_more=False) + another_test_quantity = Quantity(type=str) + + assert TestSection.m_def.more['this_does_not_exist_in_metainfo'] == 'value' + assert TestSection.test_quantity.more['also_no_metainfo_quantity'] == 1 + assert not TestSection.test_quantity.more['one_more'] + assert len(TestSection.another_test_quantity.more) == 0 + + assert TestSection.m_def.this_does_not_exist_in_metainfo == 'value' + assert TestSection.test_quantity.also_no_metainfo_quantity == 1 + assert not TestSection.test_quantity.one_more + with pytest.raises(AttributeError): + assert TestSection.not_even_in_more is None + + serialized = TestSection.m_def.m_to_dict() + assert 'more' in serialized + assert 'this_does_not_exist_in_metainfo' in serialized['more'] + assert 'this_does_not_exist_in_metainfo' not in serialized + class TestM1: ''' Test for meta-info instances. ''' @@ -509,7 +531,7 @@ class TestM1: def derived(self): return self.value + self.list[0] - assert TestSection.derived.cached + assert TestSection.derived.cached # pylint: disable=no-member test_section = TestSection(value='test', list=['1']) assert test_section.derived == 'test1' test_section.value = '2'