diff --git a/nomad/app/api/repo.py b/nomad/app/api/repo.py index b83f9322bc4ceefe4140bdfa5b95418773ee5bee..f49f8c298a20c46d3baaf0c25b587c95b8c8da5c 100644 --- a/nomad/app/api/repo.py +++ b/nomad/app/api/repo.py @@ -331,9 +331,8 @@ def edit(parsed_query: Dict[str, Any], mongo_update: Dict[str, Any] = None, re_i if re_index: def elastic_updates(): for calc in proc.Calc.objects(calc_id__in=calc_ids): - entry = datamodel.EntryMetadata.m_def.m_x('elastic').create_index_entry( - datamodel.EntryMetadata.m_from_dict(calc['metadata'])) - entry = entry.to_dict(include_meta=True) + entry_metadata = datamodel.EntryMetadata.m_from_dict(calc['metadata']) + entry = entry_metadata.a_elastic.create_index_entry().to_dict(include_meta=True) entry['_op_type'] = 'index' yield entry diff --git a/nomad/app/api/upload.py b/nomad/app/api/upload.py index 0475fd8a3c3e6ba2a6d6f2a2ff2260046562342a..f84aa6012f616e3436d99699edb4ff9ca19bb249 100644 --- a/nomad/app/api/upload.py +++ b/nomad/app/api/upload.py @@ -46,7 +46,7 @@ ns = api.namespace( class CalcMetadata(fields.Raw): def format(self, value): entry_metadata = datamodel.EntryMetadata.m_from_dict(value) - return datamodel.EntryMetadata.m_def.m_x('elastic').create_index_entry(entry_metadata).to_dict() + return entry_metadata.a_elastic.create_index_entry().to_dict() proc_model = api.model('Processing', { diff --git a/nomad/cli/admin/admin.py b/nomad/cli/admin/admin.py index d72dddfb4f3fb612b5ecaaeb339b520e95e909d0..b919711e2c0436ac3f01a0ad4988c252ceef0069 100644 --- a/nomad/cli/admin/admin.py +++ b/nomad/cli/admin/admin.py @@ -179,10 +179,8 @@ def index(threads, dry): with utils.ETA(all_calcs, ' index %10d or %10d calcs, ETA %s') as eta: for calc in proc.Calc.objects(): eta.add() - entry = None - entry = datamodel.EntryMetadata.m_def.m_x('elastic').create_index_entry( - datamodel.EntryMetadata.m_from_dict(calc.metadata)) - entry = entry.to_dict(include_meta=True) + entry_metadata = datamodel.EntryMetadata.m_from_dict(calc.metadata) + entry = entry_metadata.a_elastic.create_index_entry().to_dict(include_meta=True) entry['_op_type'] = 'index' yield entry diff --git a/nomad/metainfo/elastic_extension.py b/nomad/metainfo/elastic_extension.py index ee3d5fe8a0ac2090e6425de3591b2a5476eff112..f3f061a725ee695b1776091a11e58987d7a98d86 100644 --- a/nomad/metainfo/elastic_extension.py +++ b/nomad/metainfo/elastic_extension.py @@ -16,7 +16,7 @@ from typing import Callable, Any, Dict, cast import uuid -from .metainfo import Section, Quantity, MSection, Annotation, MEnum, Datetime, Reference +from .metainfo import Section, Quantity, MSection, MEnum, Datetime, Reference, Annotation, SectionAnnotation, DefinitionAnnotation ''' This module provides metainfo annotation class :class:`Elastic` and @@ -25,7 +25,7 @@ metainfo data in elastic search. ''' -class ElasticDocument(Annotation): +class ElasticDocument(SectionAnnotation): ''' This annotation class can be used to extend metainfo sections. It allows to detail how section instances (and their sub sections and quantities) should be represented in @@ -56,6 +56,9 @@ class ElasticDocument(Annotation): self.m_def: Section = None self.fields: Dict[Quantity, str] = {} + def new(self, section): + return dict(elastic=ElasticEntry(section)) + def init_annotation(self, definition): assert isinstance(definition, Section), 'The ElasticDocument annotation is only usable with Sections.' self.m_def = definition @@ -194,7 +197,18 @@ class ElasticDocument(Annotation): return document -class Elastic(Annotation): +class ElasticEntry(Annotation): + def __init__(self, section: MSection): + self.section = section + + def index(self, **kwargs): + return ElasticDocument.index(self.section, **kwargs) + + def create_index_entry(self): + return ElasticDocument.create_index_entry(self.section) + + +class Elastic(DefinitionAnnotation): ''' This annotation class can be used to extend metainfo quantities. It allows to detail how this quantity should be represented in an elastic search index. diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index 163ca1d3a07b592624ee5024821b06b2968c7e61..ca8b9f1663a16b16dc3518760cc7007c93a23c96 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -656,13 +656,23 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas MetainfoError('Section has not m_def.') # get annotations from kwargs - self.m_annotations: Dict[Union[str, type], Any] = {} - rest = {} + self.m_annotations: Dict[str, Any] = {} + other_kwargs = {} for key, value in kwargs.items(): if key.startswith('a_'): self.m_annotations[key[2:]] = value else: - rest[key] = value + other_kwargs[key] = value + + # get additional annotations from the section definition + if not is_bootstrapping: + for section_annotation in self.m_def.m_x(SectionAnnotation, as_list=True): + for name, annotation in section_annotation.new(self).items(): + self.m_annotations[name] = annotation + + # add annotation attributes for names annotations + for annotation_name, annotation in self.m_annotations.items(): + setattr(self, 'a_%s' % annotation_name, annotation) # initialize data self.m_data = m_data @@ -671,9 +681,9 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas # set remaining kwargs if is_bootstrapping: - self.m_data.dct.update(**rest) # type: ignore + self.m_data.dct.update(**other_kwargs) # type: ignore else: - self.m_update(**rest) + self.m_update(**other_kwargs) @classmethod def __init_cls__(cls): @@ -805,6 +815,12 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas m_def.__init_metainfo__() + def __getattr__(self, name): + # This will make mypy and pylint ignore 'missing' dynamic attributes and functions + # and wrong types of those. + # Ideally we have a plugin for both that add the corrent type info + return super().__getattr__(name) # pylint: disable=no-member + def __check_np(self, quantity_def: 'Quantity', value: np.ndarray) -> np.ndarray: # TODO this feels expensive, first check, then possible convert very often? # if quantity_ref.type != value.dtype: @@ -1351,9 +1367,13 @@ class MSection(metaclass=MObjectMeta): # TODO find a way to make this a subclas return cast(MSectionBound, context) - def m_x(self, key: Union[str, type], default=None, as_list: bool = False): + def m_x(self, *args, **kwargs): + # TODO remove + return self.m_get_annotations(*args, **kwargs) + + def m_get_annotations(self, key: Union[str, type], default=None, as_list: bool = False): ''' - Convinience method for get annotations + Convinience method to get annotations Arguments: key: Either the optional annoation name or an annotation class. In the first @@ -1552,14 +1572,9 @@ class Definition(MSection): a class context, this method must be called manually on all definitions. ''' - # initialize annotations - for annotation in self.m_annotations.values(): - if isinstance(annotation, (tuple, list)): - for single_annotation in annotation: - if isinstance(single_annotation, Annotation): - single_annotation.init_annotation(self) - if isinstance(annotation, Annotation): - annotation.init_annotation(self) + # initialize definition annotations + for annotation in self.m_x(DefinitionAnnotation, as_list=True): + annotation.init_annotation(self) @classmethod def all_definitions(cls: Type[MSectionBound]) -> Iterable[MSectionBound]: @@ -2066,6 +2081,31 @@ class Category(Definition): self.definitions: Set[Definition] = set() +class Annotation: + ''' Base class for annotations. ''' + pass + + +class DefinitionAnnotation(Annotation): + ''' Base class for annotations for definitions. ''' + + def __init__(self): + self.definition: Definition = None + + def init_annotation(self, definition: Definition): + self.definition = definition + + +class SectionAnnotation(DefinitionAnnotation): + ''' + Special annotation class for section definition that allows to auto add annotations + to section instances. + ''' + + def new(self, section) -> Dict[str, Any]: + return {} + + Section.m_def = Section(name='Section') Section.m_def.m_def = Section.m_def Section.m_def.section_cls = Section @@ -2174,13 +2214,3 @@ class Environment(MSection): if isinstance(definition, Definition): definitions = self.all_definitions_by_name.setdefault(definition.name, []) definitions.append(definition) - - -class Annotation: - ''' Base class for annotations. ''' - - def __init__(self): - self.definition: Definition = None - - def init_annotation(self, definition: Definition): - self.definition = definition diff --git a/nomad/metainfo/pylint_plugin.py b/nomad/metainfo/pylint_plugin.py new file mode 100644 index 0000000000000000000000000000000000000000..4f4d19489dc06b16406253e6104358fe1a10dc56 --- /dev/null +++ b/nomad/metainfo/pylint_plugin.py @@ -0,0 +1,54 @@ +# Copyright 2018 Markus Scheidgen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an"AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +''' The beginning of a pylint plugin. Unfotunately it is kinda nonsensical without a +partnering mypy plugin. ''' + +import astroid +from astroid import MANAGER + + +annotation_names = { + 'MSection': { + 'a_test': '*' + }, + 'Section': { + 'a_elastic': 'nomad.metainfo.elastic_extension.ElasticDocument' + }, + 'Quantity': { + 'a_elastic': 'nomad.metainfo.elastic_extension.Elastic' + } +} + + +def register(linter): + # Needed for registering the plugin. + pass + + +def transform(cls): + for cls_name, annotations in annotation_names.items(): + if cls.name == cls_name: + for name, type_spec in annotations.items(): + if type_spec == '*': + cls.locals[name] = [astroid.Instance()] + else: + type_path = type_spec.split('.') + type_module = '.'.join(type_path[:-1]) + type_name = type_path[-1] + module = MANAGER.ast_from_module_name(type_module) + cls.locals[name] = [cls.instantiate_class() for cls in module.lookup(type_name)[1]] + + +MANAGER.register_transform(astroid.ClassDef, transform) diff --git a/nomad/processing/data.py b/nomad/processing/data.py index af114814c72395fd002d19a6732627ad6df77d3f..b5441025fa275d26c8fd4cd7c4642110e819a1db 100644 --- a/nomad/processing/data.py +++ b/nomad/processing/data.py @@ -302,8 +302,7 @@ class Calc(Proc): entry_metadata.processed = False self.metadata = entry_metadata.m_to_dict(include_defaults=True) - - datamodel.EntryMetadata.m_def.m_x('elastic').index(entry_metadata) + entry_metadata.a_elastic.index() except Exception as e: self.get_logger().error('could not index after processing failure', exc_info=e) @@ -432,7 +431,7 @@ class Calc(Proc): # index in search with utils.timer(logger, 'indexed', step='index'): - datamodel.EntryMetadata.m_def.m_x('elastic').index(entry_metadata) + entry_metadata.a_elastic.index() # persist the archive with utils.timer( diff --git a/nomad/search.py b/nomad/search.py index 2fd6de16e236a139752a110262bcd7f6b334731a..9506dd784f4bb29e3515c099a146e2c0c397a559 100644 --- a/nomad/search.py +++ b/nomad/search.py @@ -65,7 +65,7 @@ def publish(calcs: Iterable[datamodel.EntryMetadata]) -> None: ''' Update all given calcs with their metadata and set ``publish = True``. ''' def elastic_updates(): for calc in calcs: - entry = calc.m_def.m_x('elastic').create_index_entry(calc) + entry = calc.a_elastic.create_index_entry() entry.published = True entry = entry.to_dict(include_meta=True) source = entry.pop('_source') @@ -86,7 +86,7 @@ def index_all(calcs: Iterable[datamodel.EntryMetadata], do_refresh=True) -> None ''' def elastic_updates(): for calc in calcs: - entry = calc.m_def.m_x('elastic').create_index_entry(calc) + entry = calc.a_elastic.create_index_entry() entry = entry.to_dict(include_meta=True) entry['_op_type'] = 'index' yield entry diff --git a/tests/app/test_api.py b/tests/app/test_api.py index 5a3ed5580de03aa7f13090c507e275c8cab415ab..92eca9409ca246bb7304eb9897f60d11dcd6159d 100644 --- a/tests/app/test_api.py +++ b/tests/app/test_api.py @@ -717,7 +717,7 @@ class TestRepo(): entry_metadata.m_update( calc_id='1', uploader=test_user.user_id, published=True, with_embargo=False) - EntryMetadata.m_def.m_x('elastic').index(entry_metadata, refresh=True) + entry_metadata.a_elastic.index(refresh=True) entry_metadata.m_update( calc_id='2', uploader=other_test_user.user_id, published=True, @@ -726,17 +726,17 @@ class TestRepo(): entry_metadata.m_update( atoms=['Fe'], comment='this is a specific word', formula='AAA') entry_metadata.dft.basis_set = 'zzz' - EntryMetadata.m_def.m_x('elastic').index(entry_metadata, refresh=True) + entry_metadata.a_elastic.index(refresh=True) entry_metadata.m_update( calc_id='3', uploader=other_test_user.user_id, published=False, with_embargo=False, pid=3, external_id='external_3') - EntryMetadata.m_def.m_x('elastic').index(entry_metadata, refresh=True) + entry_metadata.a_elastic.index(refresh=True) entry_metadata.m_update( calc_id='4', uploader=other_test_user.user_id, published=True, with_embargo=True, pid=4, external_id='external_4') - EntryMetadata.m_def.m_x('elastic').index(entry_metadata, refresh=True) + entry_metadata.a_elastic.index(refresh=True) yield @@ -1798,7 +1798,7 @@ class TestDataset: Calc( calc_id='1', upload_id='1', create_time=datetime.datetime.now(), metadata=entry_metadata.m_to_dict()).save() - EntryMetadata.m_def.m_x('elastic').index(entry_metadata, refresh=True) + entry_metadata.a_elastic.index(refresh=True) def test_delete_dataset(self, api, test_user_auth, example_dataset_with_entry): rv = api.delete('/datasets/ds1', headers=test_user_auth) diff --git a/tests/conftest.py b/tests/conftest.py index 3ea88cdf74eab657640a6e19a3cc1a915e8b315e..b5bbfe51e67f57afcca8f5b80efb1b5218f3a99c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -698,6 +698,6 @@ def create_test_structure( proc_calc = processing.Calc.from_entry_metadata(calc) proc_calc.save() - calc.m_def.m_x('elastic').index(calc) + calc.a_elastic.index() assert processing.Calc.objects(calc_id__in=[calc.calc_id]).count() == 1 diff --git a/tests/metainfo/test_metainfo.py b/tests/metainfo/test_metainfo.py index cd90acce6379397881ab521a089326f8be8df241..d6cf050c55499524ba6fca9662d8f8f29f77e3fb 100644 --- a/tests/metainfo/test_metainfo.py +++ b/tests/metainfo/test_metainfo.py @@ -21,7 +21,8 @@ from nomadcore.local_meta_info import InfoKindEl, InfoKindEnv from nomad.metainfo.metainfo import ( MSection, MCategory, Section, Quantity, SubSection, Definition, Package, DeriveError, - MetainfoError, Environment, MResource, Datetime, units, Annotation) + MetainfoError, Environment, MResource, Datetime, units, Annotation, SectionAnnotation, + DefinitionAnnotation) from nomad.metainfo.example import Run, VaspRun, System, SystemHash, Parsing, m_package as example_package from nomad.metainfo.legacy import LegacyMetainfoEnvironment from nomad.parsing.metainfo import MetainfoBackend @@ -237,19 +238,24 @@ class TestM2: assert System.n_atoms.virtual def test_annotations(self): - class TestSectionAnnotation(Annotation): + class TestSectionAnnotation(SectionAnnotation): def init_annotation(self, definition): super().init_annotation(definition) + section_cls = definition.section_cls assert definition.name == 'TestSection' assert 'test_quantity' in definition.all_quantities - assert definition.all_quantities['test_quantity'].m_x('test').initialized - assert definition.all_quantities['test_quantity'].m_x('test', as_list=True)[0].initialized - assert definition.all_quantities['test_quantity'].m_x(Annotation).initialized - assert all(a.initialized for a in definition.all_quantities['list_test_quantity'].m_x('test')) - assert all(a.initialized for a in definition.all_quantities['list_test_quantity'].m_x(Annotation)) + assert section_cls.test_quantity.m_get_annotations('test').initialized + assert section_cls.test_quantity.a_test.initialized + assert section_cls.test_quantity.m_get_annotations('test', as_list=True)[0].initialized + assert section_cls.test_quantity.m_get_annotations(Annotation).initialized + assert all(a.initialized for a in section_cls.list_test_quantity.a_test) + assert all(a.initialized for a in section_cls.list_test_quantity.m_get_annotations(Annotation)) self.initialized = True - class TestQuantityAnnotation(Annotation): + def new(self, section): + return dict(test='test annotation') + + class TestQuantityAnnotation(DefinitionAnnotation): def init_annotation(self, definition): super().init_annotation(definition) assert definition.name in ['test_quantity', 'list_test_quantity'] @@ -264,8 +270,10 @@ class TestM2: type=str, a_test=[TestQuantityAnnotation(), TestQuantityAnnotation()]) - assert TestSection.m_def.m_x('test').initialized - assert TestSection.m_def.m_x(TestSectionAnnotation).initialized + assert TestSection.m_def.a_test.initialized + assert TestSection.m_def.m_get_annotations(TestSectionAnnotation).initialized + + assert TestSection().a_test == 'test annotation' class TestM1: diff --git a/tests/test_datamodel.py b/tests/test_datamodel.py index 0b2d6810f83c2cb35126a88092cfd378f3b2944c..a933f55177e7d3b2169d6b4e8699ce58d158ad2a 100644 --- a/tests/test_datamodel.py +++ b/tests/test_datamodel.py @@ -150,7 +150,7 @@ if __name__ == '__main__': with upload_files.archive_log_file(calc.calc_id, 'wt') as f: f.write('this is a generated test file') - search_entry = calc.m_def.m_x('elastic').create_index_entry(calc) + search_entry = calc.a_elastic.create_index_entry() search_entry.n_total_energies = random.choice(low_numbers_for_total_energies) search_entry.n_geometries = low_numbers_for_geometries for _ in range(0, random.choice(search_entry.n_geometries)): diff --git a/tests/test_search.py b/tests/test_search.py index cfb33577d29265603349ed19ec282e7987310f08..1e987c4150d542b9d9ab07d588d0fecc44499087 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -233,8 +233,7 @@ def refresh_index(): def create_entry(entry_metadata: datamodel.EntryMetadata): - entry = datamodel.EntryMetadata.m_def.m_x('elastic').index(entry_metadata) - entry.save() + entry = entry_metadata.a_elastic.index() assert_entry(entry_metadata.calc_id) return entry