From 7b4313491de3944aa1880342ec53a811a8bfa549 Mon Sep 17 00:00:00 2001
From: Markus Scheidgen <markus.scheidgen@gmail.com>
Date: Sat, 28 Sep 2019 21:07:00 +0200
Subject: [PATCH] Metainfo refactorings.

---
 nomad/datamodel/base.py      |   4 +-
 nomad/metainfo/__init__.py   |   2 +-
 nomad/metainfo/api_tryout.py |   6 +-
 nomad/metainfo/metainfo.py   | 124 ++++++++++++++++++++---------------
 nomad/metainfo/optimade.py   |  10 +--
 nomad/parsing/backend.py     |   6 +-
 tests/test_metainfo.py       |  31 ++++-----
 7 files changed, 100 insertions(+), 83 deletions(-)

diff --git a/nomad/datamodel/base.py b/nomad/datamodel/base.py
index ed962647b4..5bfaced103 100644
--- a/nomad/datamodel/base.py
+++ b/nomad/datamodel/base.py
@@ -17,7 +17,7 @@ import datetime
 from elasticsearch_dsl import Keyword
 
 from nomad import utils, config
-from nomad.metainfo import MObject
+from nomad.metainfo import MSection
 
 
 class UploadWithMetadata():
@@ -113,7 +113,7 @@ class CalcWithMetadata():
                 if value is None or key in ['backend']:
                     continue
 
-                if isinstance(value, MObject):
+                if isinstance(value, MSection):
                     value = value.m_to_dict()
 
                 yield key, value
diff --git a/nomad/metainfo/__init__.py b/nomad/metainfo/__init__.py
index ede9449121..34206203fd 100644
--- a/nomad/metainfo/__init__.py
+++ b/nomad/metainfo/__init__.py
@@ -1 +1 @@
-from .metainfo import MObject, Section, Quantity, Enum, units
+from .metainfo import MSection, Section, Quantity, Enum, units
diff --git a/nomad/metainfo/api_tryout.py b/nomad/metainfo/api_tryout.py
index de5ad7a154..1c0bdab846 100644
--- a/nomad/metainfo/api_tryout.py
+++ b/nomad/metainfo/api_tryout.py
@@ -3,7 +3,7 @@ Some playground to try the API_CONCEPT.md ideas.
 """
 
 
-class MObject:
+class MSection:
     def __init__(self, m_definition: 'MElementDef', m_def: 'MSection' = None):
         self.m_definition = m_definition
         self.m_def = m_def
@@ -26,7 +26,7 @@ class MObject:
             subsection.append(self)
 
 
-class MSection(MObject):
+class MSection(MSection):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
@@ -60,7 +60,7 @@ class MSection(MObject):
         return ':%s' % self.m_definition.name
 
 
-class MProperty(MObject):
+class MProperty(MSection):
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py
index 7fcc281e20..bc919e86b1 100644
--- a/nomad/metainfo/metainfo.py
+++ b/nomad/metainfo/metainfo.py
@@ -27,10 +27,10 @@ Here is a simple example that demonstrates the definition of System related quan
 
 .. code-block:: python
 
-    class Run(MObject):
+    class Run(MSection):
         pass
 
-    class System(MObject):
+    class System(MSection):
         \"\"\"
         A system section includes all quantities that describe a single a simulated
         system (a.k.a. geometry).
@@ -109,18 +109,18 @@ quantities:
 
 
 All meta-info definitions and classes for meta-info data objects (i.e. section instances)
-inherit from :class:` MObject`. This base-class provides common functions and attributes
+inherit from :class:` MSection`. This base-class provides common functions and attributes
 for all meta-info data objects. Names of these common parts are prefixed with ``m_``
 to distinguish them from user defined quantities. This also constitute's the `reflection`
 interface (in addition to Python's build in ``getattr``, ``setattr``) that allows to
 create and manipulate meta-info data, without prior program time knowledge of the underlying
 definitions.
 
-.. autoclass:: MObject
+.. autoclass:: MSection
 
 The following classes can be used to define and structure meta-info data:
 
-- sections are defined by sub-classes :class:`MObject` and using :class:`Section` to
+- sections are defined by sub-classes :class:`MSection` and using :class:`Section` to
   populate the classattribute `m_def`
 - quantities are defined by assigning classattributes of a section with :class:`Quantity`
   instances
@@ -150,8 +150,8 @@ from pint.unit import _Unit
 from pint import UnitRegistry
 import inflection
 
-__module__ = sys.modules[__name__]
-MObjectBound = TypeVar('MObjectBound', bound='MObject')
+is_bootstrapping = True
+MSectionBound = TypeVar('MSectionBound', bound='MSection')
 
 
 # Reflection
@@ -212,22 +212,22 @@ class MObjectMeta(type):
 
     def __new__(self, cls_name, bases, dct):
         cls = super().__new__(self, cls_name, bases, dct)
-        init = getattr(cls, '__init_section_cls__')
-        if init is not None:
+        init = getattr(cls, '__init_cls__')
+        if init is not None and not is_bootstrapping:
             init()
         return cls
 
 
-Content = Tuple[MObjectBound, Union[List[MObjectBound], MObjectBound], str, MObjectBound]
-SectionDef = Union[str, 'Section', Type[MObjectBound]]
+Content = Tuple[MSectionBound, Union[List[MSectionBound], MSectionBound], str, MSectionBound]
+SectionDef = Union[str, 'Section', Type[MSectionBound]]
 
 
-class MObject(metaclass=MObjectMeta):
-    """Base class for all section objects on all meta-info levels.
+class MSection(metaclass=MObjectMeta):
+    """Base class for all section instances on all meta-info levels.
 
-    All metainfo objects instantiate classes that inherit from ``MObject``. Each
-    section or quantity definition is an ``MObject``, each actual (meta-)data carrying
-    section is an ``MObject``. This class consitutes the reflection interface of the
+    All metainfo objects instantiate classes that inherit from ``MSection``. Each
+    section or quantity definition is an ``MSection``, each actual (meta-)data carrying
+    section is an ``MSection``. This class consitutes the reflection interface of the
     meta-info, since it allows to manipulate sections (and therefore all meta-info data)
     without having to know the specific sub-class.
 
@@ -262,9 +262,9 @@ class MObject(metaclass=MObjectMeta):
 
     m_def: 'Section' = None
 
-    def __init__(self, m_def: 'Section' = None, m_parent: 'MObject' = None, _bs: bool = False, **kwargs):
+    def __init__(self, m_def: 'Section' = None, m_parent: 'MSection' = None, _bs: bool = False, **kwargs):
         self.m_def: 'Section' = m_def
-        self.m_parent: 'MObject' = m_parent
+        self.m_parent: 'MSection' = m_parent
         self.m_parent_index = -1
 
         cls = self.__class__
@@ -291,14 +291,10 @@ class MObject(metaclass=MObjectMeta):
         #     self.m_update(**kwargs)
 
     @classmethod
-    def __init_section_cls__(cls):
-        # only works after bootstrapping, since functionality is still missing
-        if not all([hasattr(__module__, cls) for cls in ['Quantity', 'Section', 'Package', 'Category', 'sub_section']]):
-            return
-
+    def __init_cls__(cls):
         # ensure that the m_def is defined
         m_def = cls.m_def
-        if m_def is None and cls != MObject:
+        if m_def is None:
             m_def = Section()
             setattr(cls, 'm_def', m_def)
 
@@ -353,7 +349,7 @@ class MObject(metaclass=MObjectMeta):
                     raise TypeError('Value has wrong type.')
 
             elif isinstance(definition.type, Section):
-                if not isinstance(value, MObject) or value.m_def != definition.type:
+                if not isinstance(value, MSection) or value.m_def != definition.type:
                     raise TypeError('The value is not a section of wrong section definition')
 
             else:
@@ -404,7 +400,7 @@ class MObject(metaclass=MObjectMeta):
 
         return section
 
-    def m_sub_sections(self, definition: SectionDef) -> List[MObjectBound]:
+    def m_sub_sections(self, definition: SectionDef) -> List[MSectionBound]:
         """Returns all sub sections for the given section definition
 
         Args:
@@ -425,7 +421,7 @@ class MObject(metaclass=MObjectMeta):
         else:
             return [m_data_value]
 
-    def m_sub_section(self, definition: SectionDef, parent_index: int = -1) -> MObjectBound:
+    def m_sub_section(self, definition: SectionDef, parent_index: int = -1) -> MSectionBound:
         """Returns the sub section for the given section definition and possible
            parent_index (for repeatable sections).
 
@@ -465,7 +461,7 @@ class MObject(metaclass=MObjectMeta):
 
             return m_data_value
 
-    def m_add_sub_section(self, sub_section: MObjectBound) -> MObjectBound:
+    def m_add_sub_section(self, sub_section: MSectionBound) -> MSectionBound:
         """Adds the given section instance as a sub section to this section."""
 
         section_def = sub_section.m_def
@@ -480,7 +476,7 @@ class MObject(metaclass=MObjectMeta):
 
         return sub_section
 
-    def m_create(self, definition: SectionDef, **kwargs) -> 'MObject':
+    def m_create(self, definition: SectionDef, **kwargs) -> 'MSection':
         """Creates a subsection and adds it this this section
 
         Args:
@@ -518,7 +514,7 @@ class MObject(metaclass=MObjectMeta):
 
         quantity = self.__resolve_quantity(definition)
 
-        MObject.m_type_check(quantity, value, check_item=True)
+        MSection.m_type_check(quantity, value, check_item=True)
 
         m_data_values = self.m_data.setdefault(quantity.name, [])
         m_data_values.append(value)
@@ -529,7 +525,7 @@ class MObject(metaclass=MObjectMeta):
         quantity = self.__resolve_quantity(definition)
 
         for value in values:
-            MObject.m_type_check(quantity, value, check_item=True)
+            MSection.m_type_check(quantity, value, check_item=True)
 
         m_data_values = self.m_data.setdefault(quantity.name, [])
         for value in values:
@@ -582,7 +578,7 @@ class MObject(metaclass=MObjectMeta):
         return {key: value for key, value in items()}
 
     @classmethod
-    def m_from_dict(cls: Type[MObjectBound], dct: Dict[str, Any]) -> MObjectBound:
+    def m_from_dict(cls: Type[MSectionBound], dct: Dict[str, Any]) -> MSectionBound:
         section_def = cls.m_def
 
         # remove m_def and m_parent_index, they set themselves automatically
@@ -604,7 +600,7 @@ class MObject(metaclass=MObjectMeta):
                 yield key, value
 
         dct = {key: value for key, value in items()}
-        section_instance = cast(MObjectBound, section_def.section_cls())
+        section_instance = cast(MSectionBound, section_def.section_cls())
         section_instance.m_update(**dct)
         return section_instance
 
@@ -625,10 +621,10 @@ class MObject(metaclass=MObjectMeta):
         for name, attr in self.m_data.items():
             if isinstance(attr, list):
                 for value in attr:
-                    if isinstance(value, MObject):
+                    if isinstance(value, MSection):
                         yield value, attr, name, self
 
-            elif isinstance(attr, MObject):
+            elif isinstance(attr, MSection):
                 yield value, value, name, self
 
     def __repr__(self):
@@ -640,6 +636,29 @@ class MObject(metaclass=MObjectMeta):
         return '%s:%s' % (name, m_section_name)
 
 
+class MCategory(metaclass=MObjectMeta):
+
+    m_def: 'Category' = None
+
+    @classmethod
+    def __init_cls__(cls):
+        # ensure that the m_def is defined
+        m_def = cls.m_def
+        if m_def is None:
+            m_def = Category()
+            setattr(cls, 'm_def', m_def)
+
+        # transfer name and description to m_def
+        m_def.name = cls.__name__
+        if cls.__doc__ is not None:
+            m_def.description = inspect.cleandoc(cls.__doc__)
+
+        # add section cls' section to the module's package
+        module_name = cls.__module__
+        pkg = Package.from_module(module_name)
+        pkg.m_add_sub_section(cls.m_def)
+
+
 # M3, the definitions that are used to write definitions. These are the section definitions
 # for sections Section and Quantity.They define themselves; i.e. the section definition
 # for Section is the same section definition.
@@ -680,9 +699,9 @@ class cached_property:
         return value
 
 
-class Definition(MObject):
+class Definition(MSection):
 
-    __all_definitions: Dict[Type[MObject], List[MObject]] = {}
+    __all_definitions: Dict[Type[MSection], List[MSection]] = {}
 
     name: 'Quantity' = None
     description: 'Quantity' = None
@@ -699,9 +718,9 @@ class Definition(MObject):
             definitions.append(self)
 
     @classmethod
-    def all_definitions(cls: Type[MObjectBound]) -> Iterable[MObjectBound]:
+    def all_definitions(cls: Type[MSectionBound]) -> Iterable[MSectionBound]:
         """ Returns all definitions of this definition class. """
-        return cast(Iterable[MObjectBound], Definition.__all_definitions.get(cls, []))
+        return cast(Iterable[MSectionBound], Definition.__all_definitions.get(cls, []))
 
     @cached_property
     def all_categories(self):
@@ -770,7 +789,7 @@ class Quantity(Definition):
         elif type(value) == np.ndarray:
             value = value.tolist()
 
-        MObject.m_type_check(self, value)
+        MSection.m_type_check(self, value)
         obj.m_data[self.__name] = value
 
     def __delete__(self, obj):
@@ -796,7 +815,7 @@ class Section(Definition):
     'section class'.
     """
 
-    section_cls: Type[MObject] = None
+    section_cls: Type[MSection] = None
     """ The section class that corresponse to this section definition. """
 
     repeats: 'Quantity' = None
@@ -848,7 +867,7 @@ class Section(Definition):
 
         .. code-block:: Python
 
-        class System(MObject):
+        class System(MSection):
             pass
 
         System.m_def.add_quantity(Quantity(name='n_atoms', type=int))
@@ -885,11 +904,11 @@ class sub_section:
 
     def __init__(self, section: SectionDef, **kwargs):
         if isinstance(section, type):
-            self.section_def = cast(MObject, section).m_def
+            self.section_def = cast(MSection, section).m_def
         else:
             self.section_def = cast(Section, section)
 
-    def __get__(self, obj: MObject, type=None) -> Union[MObject, Section]:
+    def __get__(self, obj: MSection, type=None) -> Union[MSection, Section]:
         if obj is None:
             # the class attribute case
             return self.section_def
@@ -903,7 +922,7 @@ class sub_section:
 
             return m_data_value
 
-    def __set__(self, obj: MObject, value: Union[MObject, List[MObject]]):
+    def __set__(self, obj: MSection, value: Union[MSection, List[MSection]]):
         raise NotImplementedError('Sub sections cannot be set directly. Use m_create.')
 
     def __delete__(self, obj):
@@ -920,11 +939,6 @@ class Category(Definition):
     In the old meta-info this was known as `abstract types`.
     """
 
-    def __init__(self, module_name, *args, **kwargs):
-        super().__init__(*args, **kwargs)
-
-        Package.from_module(module_name).m_add_sub_section(self)
-
     @cached_property
     def definitions(self) -> Iterable[Definition]:
         """ All definitions that are directly or indirectly in this category. """
@@ -1021,10 +1035,12 @@ Section.m_def.parent = Package.m_def
 
 Category.m_def = Section(repeats=True, parent=Package.m_def)
 
-Package.__init_section_cls__()
-Category.__init_section_cls__()
-Section.__init_section_cls__()
-Quantity.__init_section_cls__()
+is_bootstrapping = False
+
+Package.__init_cls__()
+Category.__init_cls__()
+Section.__init_cls__()
+Quantity.__init_cls__()
 
 units = UnitRegistry()
 """ The default pint unit registry that should be used to give units to quantity definitions. """
diff --git a/nomad/metainfo/optimade.py b/nomad/metainfo/optimade.py
index 2f287f5c3f..f1f2009b6b 100644
--- a/nomad/metainfo/optimade.py
+++ b/nomad/metainfo/optimade.py
@@ -2,7 +2,7 @@ from ase.data import chemical_symbols
 from elasticsearch_dsl import Keyword, Integer, Float, InnerDoc, Nested
 import numpy as np
 
-from nomad.metainfo import MObject, Section, Quantity, Enum, units
+from nomad.metainfo import MSection, Section, Quantity, Enum, units
 
 
 def optimade_links(section: str):
@@ -27,7 +27,7 @@ class Optimade():
         pass
 
 
-class OptimadeStructureEntry(MObject):
+class OptimadeStructureEntry(MSection):
     m_def = Section(
         links=optimade_links('h.6.2'),
         a_flask=dict(skip_none=True),
@@ -166,7 +166,7 @@ class OptimadeStructureEntry(MObject):
         ''')
 
 
-class Species(MObject):
+class Species(MSection):
     """
     Used to describe the species of the sites of this structure. Species can be pure
     chemical elements, or virtual-crystal atoms representing a statistical occupation of a
@@ -244,11 +244,11 @@ def elastic_mapping(section: Section, base_cls: type) -> type:
     return type(section.name, (base_cls,), dct)
 
 
-def elastic_obj(source: MObject, target_cls: type):
+def elastic_obj(source: MSection, target_cls: type):
     if source is None:
         return None
 
-    assert isinstance(source, MObject)
+    assert isinstance(source, MSection)
 
     target = target_cls()
 
diff --git a/nomad/parsing/backend.py b/nomad/parsing/backend.py
index 395fabeaa6..b8c0c9807e 100644
--- a/nomad/parsing/backend.py
+++ b/nomad/parsing/backend.py
@@ -23,7 +23,7 @@ from nomadcore.local_backend import LocalBackend as LegacyLocalBackend
 from nomadcore.local_backend import Section, Results
 
 from nomad.utils import get_logger
-from nomad.metainfo import MObject, Section as MI2Section
+from nomad.metainfo import MSection, Section as MI2Section
 
 logger = get_logger(__name__)
 
@@ -337,7 +337,7 @@ class LocalBackend(LegacyParserBackend):
         delegate = LegacyLocalBackend(*args, **kwargs)
         super().__init__(delegate)
 
-        self.mi2_data: Dict[str, MObject] = {}
+        self.mi2_data: Dict[str, MSection] = {}
 
         self.reset_status()
 
@@ -349,7 +349,7 @@ class LocalBackend(LegacyParserBackend):
         self._known_attributes = ['results']
         self.fileOut = io.StringIO()
 
-    def add_mi2_section(self, section: MObject):
+    def add_mi2_section(self, section: MSection):
         """ Allows to mix a metainfo2 style section into backend. """
         self.mi2_data[section.m_def.name] = section
 
diff --git a/tests/test_metainfo.py b/tests/test_metainfo.py
index 800562bc2c..4b7f8cbae6 100644
--- a/tests/test_metainfo.py
+++ b/tests/test_metainfo.py
@@ -15,7 +15,7 @@
 import pytest
 import numpy as np
 
-from nomad.metainfo.metainfo import MObject, Section, Quantity, Definition, Category, Package, sub_section
+from nomad.metainfo.metainfo import MSection, MCategory, Section, Quantity, Definition, Category, Package, sub_section
 
 
 def assert_section_def(section_def: Section):
@@ -35,7 +35,7 @@ def assert_section_def(section_def: Section):
         assert section_def.parent is not None
 
 
-def assert_section_instance(section: MObject):
+def assert_section_instance(section: MSection):
     assert_section_def(section.m_def)
 
     if section.m_parent is not None:
@@ -72,7 +72,7 @@ class TestPureReflection:
         test_section_def = Section(name='TestSection')
         test_section_def.m_create(Quantity, name='test_quantity')
 
-        obj = MObject(m_def=test_section_def)
+        obj = MSection(m_def=test_section_def)
         assert obj.m_def.name == 'TestSection'
         # FIXME assert obj.m_get('test_quantity') is None
         setattr(obj, 'test_quantity', 'test_value')
@@ -81,12 +81,13 @@ class TestPureReflection:
 
 m_package = Package(description='package doc')
 
-material_defining = Category(
-    __name__, name='material_defining',
-    description='Quantities that add to what constitutes a different material.')
 
+class MaterialDefining(MCategory):
+    """Quantities that add to what constitutes a different material."""
+    pass
 
-class Run(MObject):
+
+class Run(MSection):
     """ This is the description.
 
     And some more description.
@@ -98,14 +99,14 @@ class Run(MObject):
         ''')
 
 
-class System(MObject):
+class System(MSection):
     m_def = Section(repeats=True, parent=Run.m_def)
-    n_atoms = Quantity(type=int, default=0, categories=[material_defining])
-    atom_label = Quantity(type=str, shape=['n_atoms'], categories=[material_defining])
+    n_atoms = Quantity(type=int, default=0, categories=[MaterialDefining.m_def])
+    atom_label = Quantity(type=str, shape=['n_atoms'], categories=[MaterialDefining.m_def])
     atom_positions = Quantity(type=np.dtype('f8'), shape=['n_atoms', 3])
 
 
-class Parsing(MObject):
+class Parsing(MSection):
     m_def = Section(parent=Run.m_def)
 
 
@@ -166,8 +167,8 @@ class TestM2:
 
     def test_direct_category(self):
         assert len(System.atom_label.categories)
-        assert material_defining in System.atom_label.categories
-        assert System.atom_label in material_defining.definitions
+        assert MaterialDefining.m_def in System.atom_label.categories
+        assert System.atom_label in MaterialDefining.m_def.definitions
 
     def test_package(self):
         assert m_package.name == __name__
@@ -180,7 +181,7 @@ class TestM1:
     """ Test for meta-info instances. """
 
     def test_run(self):
-        class Run(MObject):
+        class Run(MSection):
             pass
 
         run = Run()
@@ -192,7 +193,7 @@ class TestM1:
         assert_section_instance(run)
 
     def test_system(self):
-        class System(MObject):
+        class System(MSection):
             m_def = Section()
             atom_labels = Quantity(type=str, shape=['1..*'])
 
-- 
GitLab