diff --git a/nomad/metainfo/__init__.py b/nomad/metainfo/__init__.py index 9dc7d03d94bcf4ee2baf0b0927e4fe2908c499c8..e9a217aa25570b17a0e72576fa42eb2082a4366e 100644 --- a/nomad/metainfo/__init__.py +++ b/nomad/metainfo/__init__.py @@ -1,2 +1,2 @@ from .metainfo import MSection, MCategory, Definition, Property, Quantity, SubSection, \ - Section, Category, Package, Enum, units + Section, Category, Package, Enum, m_package, units diff --git a/nomad/metainfo/example.py b/nomad/metainfo/example.py index e399e541bed3920a88225200146e5bdd16ff4bb3..14ac880dd2a37837d1ed89d652615698c50a87b3 100644 --- a/nomad/metainfo/example.py +++ b/nomad/metainfo/example.py @@ -28,21 +28,21 @@ class System(MSection): description='Number of atoms in the simulated system.') atom_labels = Quantity( - type=str, shape=['n_atoms'], categories=[SystemHash.m_def], + type=str, shape=['n_atoms'], categories=[SystemHash], description='The atoms in the simulated systems.') atom_positions = Quantity( - type=np.dtype('f'), shape=['n_atoms', 3], unit=units.m, categories=[SystemHash.m_def], + type=np.dtype('f'), shape=['n_atoms', 3], unit=units.m, categories=[SystemHash], description='The atom positions in the simulated system.') lattice_vectors = Quantity( - type=np.dtype('f'), shape=[3, 3], unit=units.m, categories=[SystemHash.m_def], + type=np.dtype('f'), shape=[3, 3], unit=units.m, categories=[SystemHash], description='The lattice vectors of the simulated unit cell.') unit_cell = Quantity(synonym_for='lattice_vectors') periodic_dimensions = Quantity( - type=bool, shape=[3], default=[False, False, False], categories=[SystemHash.m_def], + type=bool, shape=[3], default=[False, False, False], categories=[SystemHash], description='A vector of booleans indicating in which dimensions the unit cell is repeated.') @@ -50,7 +50,7 @@ class SCC(MSection): energy_total = Quantity(type=float, default=0.0, unit=units.J) - system = Quantity(type=System.m_def, description='The system that this calculation is based on.') + system = Quantity(type=System, description='The system that this calculation is based on.') class Run(MSection): @@ -59,9 +59,9 @@ class Run(MSection): code_name = Quantity(type=str, description='The name of the code that was run.') code_version = Quantity(type=str, description='The version of the code that was run.') - parsing = SubSection(sub_section=Parsing.m_def) - systems = SubSection(sub_section=System.m_def, repeats=True) - sccs = SubSection(sub_section=SCC.m_def, repeats=True) + parsing = SubSection(sub_section=Parsing) + systems = SubSection(sub_section=System, repeats=True) + sccs = SubSection(sub_section=SCC, repeats=True) class VaspRun(Run): @@ -117,4 +117,4 @@ if __name__ == '__main__': run = Run.m_from_dict(serializable) print(run.sccs[0].system) - # print(m_package.m_to_json(indent=2)) # type: ignore, pylint: disable=undefined-variable + print(m_package.m_to_dict()) # type: ignore, pylint: disable=undefined-variable diff --git a/nomad/metainfo/metainfo.py b/nomad/metainfo/metainfo.py index 1897225df7510566a116fa250e4df224bae8e070..227b751605c0db7576f28d8a34274936cd80deab 100644 --- a/nomad/metainfo/metainfo.py +++ b/nomad/metainfo/metainfo.py @@ -136,20 +136,22 @@ See the reference of classes :class:`Section` and :class:`Quantities` for detail # TODO validation -from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, Set, Callable, cast +from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, Set, \ + Callable as TypingCallable, cast from collections.abc import Iterable as IterableABC import sys import inspect import re import json import itertools -import time import numpy as np from pint.unit import _Unit from pint import UnitRegistry -start_time = time.time() + +m_package: 'Package' = None + is_bootstrapping = True MSectionBound = TypeVar('MSectionBound', bound='MSection') T = TypeVar('T') @@ -169,7 +171,12 @@ class DeriveError(MetainfoError): class Enum(list): """ Allows to define str types with values limited to a pre-set list of possible values. """ - pass + def __init__(self, *args): + if len(args) == 1 and isinstance(args[0], list): + super().__init__(args[0]) + + else: + super().__init__(args) class MProxy(): @@ -183,23 +190,34 @@ class DataType: """ Allows to define custom data types that can be used in the meta-info. - The metainfo supports most types out of the box. These includes the python build-in + The metainfo supports the most types out of the box. These includes the python build-in primitive types (int, bool, str, float, ...), references to sections, and enums. However, in some occasions you need to add custom data types. + + This base class lets you customize various aspects of value treatment. This includes + type checks and various value transformations. This allows to store values in the + section differently from how the usermight set/get them, and it allows to have non + serializeable values that are transformed on de-/serialization. """ - def type_check(self, section, value): - """ Checks the given value before it is set to the given section. Can modify the value. """ + def set_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + """ Transforms the given value before it is set and checks its type. """ + return value + + def get_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + """ Transforms the given value when it is get. """ return value - def to_json_serializable(self, section, value): + def serialize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + """ Transforms the given value when making the section serializeable. """ return value - def from_json_serializable(self, section, value): + def deserialize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + """ Transforms the given value from its serializeable form. """ return value -class Dimension(DataType): - def type_check(self, section, value): +class __Dimension(DataType): + def set_normalize(self, section, quantity_def: 'Quantity', value): if isinstance(value, int): return value @@ -218,21 +236,8 @@ class Dimension(DataType): raise TypeError('%s is not a valid dimension' % str(value)) -class Reference(DataType): - """ A datatype class that can be used to define reference types based on section definitions. - - A quantity can be used to define possible references between sections. Instantiate - this class to create a reference type that specified that a quantity with this type - is actually a reference (or references, depending on shape) to a section of the - given definition. - """ - # TODO not used yet - def __init__(self, section: 'Section'): - self.section = section - - -class Unit(DataType): - def type_check(self, section, value): +class __Unit(DataType): + def set_normalize(self, section, quantity_def: 'Quantity', value): if isinstance(value, str): value = units.parse_units(value) @@ -241,12 +246,150 @@ class Unit(DataType): return value - def to_json_serializable(self, section, value): + def serialize(self, section, quantity_def: 'Quantity', value): return value.__str__() - def from_json_serializable(self, section, value): + def deserialize(self, section, quantity_def: 'Quantity', value): return units.parse_units(value) + +class __Callable(DataType): + def serialize(self, section, quantity_def: 'Quantity', value): + raise MetainfoError('Callables cannot be serialized') + + def deserialize(self, section, quantity_def: 'Quantity', value): + raise MetainfoError('Callables cannot be serialized') + + +class __QuantityType(DataType): + """ Data type for defining the type of a metainfo quantity. + + A metainfo quantity type can be one of + + - python build-in primitives: int, float, bool, str + - numpy dtypes, e.g. f, int32 + - a section definition to define references + - an Enum instance to use it's values as possible str values + - a custom datatype, i.e. instance of :class:`DataType` + - Any + """ + + def set_normalize(self, section, quantity_def, value): + if value in [str, int, float, bool]: + return value + + if isinstance(value, Enum): + for enum_value in value: + if not isinstance(enum_value, str): + raise TypeError('Enum value %s is not a string.' % enum_value) + return value + + if type(value) == np.dtype: + return value + + if isinstance(value, Section): + return value + + if isinstance(value, DataType): + return value + + if value == Any: + return value + + if isinstance(value, type): + section = getattr(value, 'm_def', None) + if section is not None: + return Reference(section) + + raise MetainfoError( + 'Type %s of %s is not a valid metainfo quantity type' % + (value, quantity_def)) + + def serialize(self, section, quantity_def, value): + if value in [str, int, float, bool]: + return dict(type_kind='python', type_data=value.__name__) + + if isinstance(value, Enum): + return dict(type_kind='Enum', type_data=list(value)) + + if type(value) == np.dtype: + return dict(type_kind='numpy', type_data=str(value)) + + if isinstance(value, Reference): + return dict(type_kind='reference', type_data=value.target_section_def.m_path()) + + if isinstance(value, DataType): + module = value.__class__.__module__ + if module is None or module == str.__class__.__module__: + type_data = value.__class__.__name__ + else: + type_data = '%s.%s' % (module, value.__class__.__name__) + + return dict(type_kind='custom', type_data=type_data) + + if value == Any: + return dict(type_kind='Any') + + raise MetainfoError( + 'Type %s of %s is not a valid metainfo quantity type' % + (value, quantity_def)) + + +class Reference(DataType): + """ Datatype used for reference quantities. """ + + def __init__(self, section_def: 'Section'): + if not isinstance(section_def, Section): + raise MetainfoError('%s is not a section definition.' % section_def) + self.target_section_def = section_def + + def set_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + if self.target_section_def.m_follows(Definition.m_def): + # special case used in metainfo definitions, where we reference metainfo definitions + # using their Python class. E.g. referencing a section definition using its + # class instead of the object: Run vs. Run.m_def + if isinstance(value, type): + definition = getattr(value, 'm_def', None) + if definition is not None and definition.m_follows(self.target_section_def): + return definition + + if isinstance(value, MProxy): + return value + + if not isinstance(value, MSection): + raise TypeError( + 'The value %s is not a section and can not be used as a reference.' % value) + + if not value.m_follows(self.target_section_def): + raise TypeError( + '%s is not a %s and therefore an invalid value of %s.' % + (value, self.target_section_def, quantity_def)) + + return value + + def get_normalize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + if isinstance(value, MProxy): + resolved: 'MSection' = section.m_resolve(value.url) + if resolved is None: + raise ReferenceError('Could not resolve %s from %s.' % (value, section)) + section.m_set(quantity_def, value) + return resolved + + return value + + def serialize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + return value.m_path() + + def deserialize(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: + return MProxy(value) + + +Dimension = __Dimension() +Unit = __Unit() +QuantityType = __QuantityType() +Callable = __Callable() + + # TODO class Datetime(DataType) @@ -334,49 +477,6 @@ class MData: raise NotImplementedError() -class MDataDelegating(MData): - """ A simple delgating implementation of :class:`MData`. """ - def __init__(self, m_data: MData): - self.m_data = m_data - - def __getitem__(self, key): - return self.m_data[key] - - def __setitem__(self, key, value): - self.m_data[key] = value - - def m_set(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> None: - self.m_data.m_set(section, quantity_def, value) - - def m_get(self, section: 'MSection', quantity_def: 'Quantity') -> Any: - return self.m_data.m_get(section, quantity_def) - - def m_is_set(self, section: 'MSection', quantity_def: 'Quantity') -> bool: - return self.m_data.m_is_set(section, quantity_def) - - def m_add_values( - self, section: 'MSection', quantity_def: 'Quantity', values: Any, - offset: int) -> None: - self.m_data.m_add_values(section, quantity_def, values, offset) - - def m_add_sub_section( - self, section: 'MSection', sub_section_def: 'SubSection', - sub_section: 'MSection') -> None: - self.m_data.m_add_sub_section(section, sub_section_def, sub_section) - - def m_get_sub_section( - self, section: 'MSection', sub_section_def: 'SubSection', - index: int) -> 'MSection': - return self.m_data.m_get_sub_section(section, sub_section_def, index) - - def m_get_sub_sections( - self, section: 'MSection', sub_section_def: 'SubSection') -> Iterable['MSection']: - return self.m_data.m_get_sub_sections(section, sub_section_def) - - def m_sub_section_count(self, section: 'MSection', sub_section_def: 'SubSection') -> int: - return self.m_data.m_sub_section_count(section, sub_section_def) - - class MDataDict(MData): """ A simple dict backed implementaton of :class:`MData`. """ @@ -452,93 +552,6 @@ class MDataDict(MData): return len(self.dct[sub_section_name]) -class MDataTypeAndShapeChecks(MDataDelegating): - """ A :class:`MData` implementation that delegates to another :class:`MData` - instance after applying rigorous type/checks. It might also resolve potential - duck typed values, depending on quantity :class:`DataType`s. - """ - - def __init__(self, m_data: MData): - self.m_data = m_data - - def __check_np(self, quantity_ref: 'Quantity', value: np.ndarray) -> np.ndarray: - # TODO - return value - - def __check_single( - self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> Any: - - if isinstance(quantity_def.type, DataType): - return quantity_def.type.type_check(section, value) - - elif isinstance(quantity_def.type, Section): - if isinstance(value, MProxy): - return value - - if not isinstance(value, MSection): - raise TypeError( - 'The value %s for reference quantity %s is not a section instance.' % - (value, quantity_def)) - - if not value.m_follows(quantity_def.type): - raise TypeError( - 'The value %s for quantity %s does not follow %s' % - (value, quantity_def, quantity_def.type)) - - elif isinstance(quantity_def.type, Enum): - if value not in quantity_def.type: - raise TypeError( - 'The value %s is not an enum value for quantity %s.' % - (value, quantity_def)) - - elif quantity_def in [Quantity.type, Quantity.derived]: - # TODO check these special cases for Quantity quantities - pass - - elif quantity_def.type == Any: - pass - - else: - if type(value) != quantity_def.type: - raise TypeError( - 'The value %s with type %s for quantity %s is not of type %s' % - (value, type(value), quantity_def, quantity_def.type)) - - return value - - def m_set(self, section: 'MSection', quantity_def: 'Quantity', value: Any) -> None: - if type(quantity_def.type) == np.dtype: - if type(value) != np.ndarray: - try: - value = np.asarray(value) - except TypeError: - raise TypeError( - 'Could not convert value %s of %s to a numpy array' % - (value, quantity_def)) - - value = self.__check_np(quantity_def, value) - - else: - dimensions = len(quantity_def.shape) - if dimensions == 0: - value = self.__check_single(section, quantity_def, value) - - elif dimensions == 1: - if type(value) == str or not isinstance(value, IterableABC): - raise TypeError( - 'The shape of %s requires an iterable value, but %s is not iterable.' % - (quantity_def, value)) - - value = list(self.__check_single(section, quantity_def, item) for item in value) - - else: - raise MetainfoError( - 'Only numpy arrays and dtypes can be used for higher dimensional ' - 'quantities.') - - self.m_data.m_set(section, quantity_def, value) - - class MSection(metaclass=MObjectMeta): """Base class for all section instances on all meta-info levels. @@ -616,11 +629,11 @@ class MSection(metaclass=MObjectMeta): # initialize data self.m_data = m_data if self.m_data is None: - self.m_data = MDataTypeAndShapeChecks(MDataDict()) + self.m_data = MDataDict() # set remaining kwargs if is_bootstrapping: - self.m_data.m_data.dct.update(**rest) # type: ignore + self.m_data.dct.update(**rest) # type: ignore else: self.m_update(**rest) @@ -695,6 +708,50 @@ class MSection(metaclass=MObjectMeta): pkg = Package.from_module(module_name) pkg.m_add_sub_section(Package.section_definitions, cls.m_def) + def __check_np(self, quantity_ref: 'Quantity', value: np.ndarray) -> np.ndarray: + # TODO + return value + + def __set_normalize(self, quantity_def: 'Quantity', value: Any) -> Any: + + if isinstance(quantity_def.type, DataType): + return quantity_def.type.set_normalize(self, quantity_def, value) + + elif isinstance(quantity_def.type, Section): + if isinstance(value, MProxy): + return value + + if not isinstance(value, MSection): + raise TypeError( + 'The value %s for reference quantity %s is not a section instance.' % + (value, quantity_def)) + + if not value.m_follows(quantity_def.type): + raise TypeError( + 'The value %s for quantity %s does not follow %s' % + (value, quantity_def, quantity_def.type)) + + elif isinstance(quantity_def.type, Enum): + if value not in quantity_def.type: + raise TypeError( + 'The value %s is not an enum value for quantity %s.' % + (value, quantity_def)) + + elif quantity_def in [Quantity.type, Quantity.derived]: + # TODO check these special cases for Quantity quantities + pass + + elif quantity_def.type == Any: + pass + + else: + if type(value) != quantity_def.type: + raise TypeError( + 'The value %s with type %s for quantity %s is not of type %s' % + (value, type(value), quantity_def, quantity_def.type)) + + return value + def __resolve_synonym(self, quantity_def: 'Quantity') -> 'Quantity': if quantity_def.synonym_for is not None: return self.m_def.all_quantities[quantity_def.synonym_for] @@ -703,8 +760,39 @@ class MSection(metaclass=MObjectMeta): def m_set(self, quantity_def: 'Quantity', value: Any) -> None: """ Set the given value for the given quantity. """ quantity_def = self.__resolve_synonym(quantity_def) + if quantity_def.derived is not None: raise MetainfoError('The quantity %s is derived and cannot be set.' % quantity_def) + + if type(quantity_def.type) == np.dtype: + if type(value) != np.ndarray: + try: + value = np.asarray(value) + except TypeError: + raise TypeError( + 'Could not convert value %s of %s to a numpy array' % + (value, quantity_def)) + + value = self.__check_np(quantity_def, value) + + else: + dimensions = len(quantity_def.shape) + if dimensions == 0: + value = self.__set_normalize(quantity_def, value) + + elif dimensions == 1: + if type(value) == str or not isinstance(value, IterableABC): + raise TypeError( + 'The shape of %s requires an iterable value, but %s is not iterable.' % + (quantity_def, value)) + + value = list(self.__set_normalize(quantity_def, item) for item in value) + + else: + raise MetainfoError( + 'Only numpy arrays and dtypes can be used for higher dimensional ' + 'quantities.') + self.m_data.m_set(self, quantity_def, value) def m_get(self, quantity_def: 'Quantity') -> Any: @@ -718,10 +806,20 @@ class MSection(metaclass=MObjectMeta): value = self.m_data.m_get(self, quantity_def) - if isinstance(quantity_def.type, Section): - if isinstance(value, MProxy): - value = self.m_resolve(value.url) - self.m_data.m_set(self, quantity_def, value) + if isinstance(quantity_def.type, DataType) and quantity_def.type.get_normalize != DataType.get_normalize: + dimensions = len(quantity_def.shape) + if dimensions == 0: + value = quantity_def.type.get_normalize(self, quantity_def, value) + + elif dimensions == 1: + value = list( + quantity_def.type.get_normalize(self, quantity_def, item) + for item in value) + + else: + raise MetainfoError( + 'Only numpy arrays and dtypes can be used for higher dimensional ' + 'quantities.') return value @@ -805,46 +903,59 @@ class MSection(metaclass=MObjectMeta): def m_follows(self, definition: 'Section') -> bool: """ Determines if this section's definition is or is derived from the given definition. """ - return self.m_def == definition or self.m_def in definition.all_base_sections + return self.m_def == definition or definition in self.m_def.all_base_sections - def m_to_dict(self) -> Dict[str, Any]: + def m_to_dict(self, with_meta: bool = False) -> Dict[str, Any]: """Returns the data of this section as a json serializeable dictionary. """ def items() -> Iterable[Tuple[str, Any]]: - yield 'm_def', self.m_def.name - if self.m_parent_index != -1: - yield 'm_parent_index', self.m_parent_index - - for name, sub_section_def in self.m_def.all_sub_sections.items(): - if sub_section_def.repeats: - if self.m_sub_section_count(sub_section_def) > 0: - yield name, [ - item.m_to_dict() - for item in self.m_get_sub_sections(sub_section_def)] - else: - sub_section = self.m_get_sub_section(sub_section_def, -1) - if sub_section is not None: - yield name, sub_section.m_to_dict() - + # metadata + if with_meta: + yield 'm_def', self.m_def.name + if self.m_parent_index != -1: + yield 'm_parent_index', self.m_parent_index + if self.m_parent_sub_section is not None: + yield 'm_parent_sub_section', self.m_parent_sub_section.name + + # quantities for name, quantity in self.m_def.all_quantities.items(): + if quantity.virtual: + continue + if self.m_is_set(quantity) and quantity.derived is None: - serialize: Callable[[Any], Any] = str + serialize: TypingCallable[[Any], Any] = str if isinstance(quantity.type, DataType): - serialize = lambda v: quantity.type.to_json_serializable(self, v) - - elif isinstance(quantity.type, Section): - serialize = lambda s: s.m_path() + serialize = lambda v: quantity.type.serialize(self, quantity, v) elif quantity.type in [str, int, float, bool]: serialize = quantity.type - else: - # TODO + elif type(quantity.type) == np.dtype: pass + elif isinstance(quantity.type, Enum): + pass + + elif quantity.type == Any: + def _serialize(value: Any): + if type(value) not in [str, int, float, bool, list, type(None)]: + raise MetainfoError( + 'Only python primitives are allowed for Any typed non ' + 'virtual quantities: %s of quantity %s in section %s' % + (value, quantity, self)) + + return value + + serialize = _serialize + + else: + raise MetainfoError( + 'Do not know how to serialize data with type %s for quantity %s' % + (quantity.type, quantity)) + value = getattr(self, name) - if hasattr(value, 'tolist'): + if type(quantity.type) == np.dtype: serializable_value = value.tolist() else: @@ -857,11 +968,23 @@ class MSection(metaclass=MObjectMeta): yield name, serializable_value + # sub sections + for name, sub_section_def in self.m_def.all_sub_sections.items(): + if sub_section_def.repeats: + if self.m_sub_section_count(sub_section_def) > 0: + yield name, [ + item.m_to_dict() + for item in self.m_get_sub_sections(sub_section_def)] + else: + sub_section = self.m_get_sub_section(sub_section_def, -1) + if sub_section is not None: + yield name, sub_section.m_to_dict() + return {key: value for key, value in items()} @classmethod def m_from_dict(cls: Type[MSectionBound], dct: Dict[str, Any]) -> MSectionBound: - """ Creates a section from the given data dictionary. + """ Creates a section from the given serializable data dictionary. This is the 'opposite' of :func:`m_to_dict`. It takes a deserialised dict, e.g loaded from JSON, and turns it into a proper section, i.e. instance of the given @@ -870,9 +993,11 @@ class MSection(metaclass=MObjectMeta): section_def = cls.m_def - # remove m_def and m_parent_index, they set themselves automatically - assert section_def.name == dct.pop('m_def', None) - dct.pop('m_parent_index', -1) + # remove m_def, m_parent_index, m_parent_sub_section metadata, + # they set themselves automatically + dct.pop('m_def', None) + dct.pop('m_parent_index', None) + dct.pop('m_parent_sub_section', None) def items(): for name, sub_section_def in section_def.all_sub_sections.items(): @@ -889,13 +1014,28 @@ class MSection(metaclass=MObjectMeta): if name in dct: quantity_value = dct[name] - if isinstance(quantity_def.type, Section): - quantity_value = MProxy(quantity_value) + if type(quantity_def.type) == np.dtype: + quantity_value = np.asarray(quantity_value) + + if isinstance(quantity_def.type, DataType): + dimensions = len(quantity_def.shape) + # TODO hand in the context, which is currently create after! + if dimensions == 0: + quantity_value = quantity_def.type.deserialize( + None, quantity_def, quantity_value) + elif dimensions == 1: + quantity_value = list( + quantity_def.type.deserialize(None, quantity_def, item) + for item in quantity_value) + else: + raise MetainfoError( + 'Only numpy quantities can have more than 1 dimension.') yield name, quantity_value dct = {key: value for key, value in items()} section_instance = cast(MSectionBound, section_def.section_cls()) + # TODO !do not update, but set directly! section_instance.m_update(**dct) return section_instance @@ -1128,6 +1268,7 @@ class Quantity(Property): default: 'Quantity' = None synonym_for: 'Quantity' = None derived: 'Quantity' = None + virtual: 'Quantity' = None # TODO derived_from = Quantity(type=Quantity, shape=['0..*']) # TODO categories = Quantity(type=Category, shape=['0..*']) @@ -1348,7 +1489,7 @@ Definition.links = Quantity( A list of URLs to external resource that describe this definition. ''') Definition.categories = Quantity( - type=Category.m_def, shape=['0..*'], default=[], name='categories', + type=Reference(Category.m_def), shape=['0..*'], default=[], name='categories', description=''' The categories that this definition belongs to. See :class:`Category`. ''') @@ -1361,7 +1502,7 @@ Section.sub_sections = SubSection( sub_section=SubSection.m_def, name='sub_sections', repeats=True, description='''The sub sections of this section.''') Section.base_sections = Quantity( - type=Section, shape=['0..*'], default=[], name='base_sections', + type=Reference(Section.m_def), shape=['0..*'], default=[], name='base_sections', description=''' Inherit all quantity and sub section definitions from the given sections. Will be derived from Python base classes. @@ -1378,14 +1519,14 @@ SubSection.repeats = Quantity( description='''Wether this sub section can appear only once or multiple times. ''') SubSection.sub_section = Quantity( - type=Section.m_def, name='sub_section', description=''' + type=Reference(Section.m_def), name='sub_section', description=''' The section definition for the sub section. Only section instances of this definition can be contained as sub sections. ''') Quantity.m_def.section_cls = Quantity Quantity.type = DirectQuantity( - type=Union[type, Enum, Section, np.dtype], name='type', description=''' + type=QuantityType, name='type', description=''' The type of the quantity. Can be one of the following: @@ -1404,8 +1545,8 @@ Quantity.type = DirectQuantity( In the NOMAD CoE meta-info this was basically the ``dTypeStr``. ''') -Quantity.shape = Quantity( - type=Dimension(), shape=['0..*'], name='shape', default=[], description=''' +Quantity.shape = DirectQuantity( + type=Dimension, shape=['0..*'], name='shape', default=[], description=''' The shape of the quantity that defines its dimensionality. A shape is a list, where each item defines a dimension. Each dimension can be: @@ -1417,7 +1558,7 @@ Quantity.shape = Quantity( and an upper bound (int or ``*`` denoting arbitrary large), e.g. ``'0..*'``, ``'1..3'`` ''') Quantity.unit = Quantity( - type=Unit(), name='unit', description=''' + type=Unit, name='unit', description=''' The optional physics unit for this quantity. Units are given in `pint` units. Pint is a Python package that defines units and @@ -1435,10 +1576,15 @@ Quantity.synonym_for = DirectQuantity( the name of the quantity as value. ''') Quantity.derived = DirectQuantity( - type=Callable, default=None, name='derived', description=''' + type=Callable, default=None, name='derived', virtual=True, description=''' Derived quantities are computed from other quantities of the same section. The value of derived needs to be a callable that takes the section and returns a value. ''') +Quantity.virtual = DirectQuantity( + type=bool, default=False, name='virtual', description=''' + Virtual quantities exist in memory, but are not serialized. This is useful for + purely derived quantities, or in situations where serialization is not required. + ''') Package.section_definitions = SubSection( sub_section=Section.m_def, name='section_definitions', repeats=True, @@ -1458,7 +1604,5 @@ Section.__init_cls__() SubSection.__init_cls__() Quantity.__init_cls__() -print('Metainfo initialization took %d ms' % ((time.time() - start_time) * 1000)) - units = UnitRegistry() """ The default pint unit registry that should be used to give units to quantity definitions. """ diff --git a/tests/test_metainfo.py b/tests/test_metainfo.py index 91fbba79c3829574d75d6035c765e3a14b470d57..a64aa798566feb5b577b843a62b66d62a8c7c738 100644 --- a/tests/test_metainfo.py +++ b/tests/test_metainfo.py @@ -69,6 +69,11 @@ class TestM3: assert_section_instance(Quantity.m_def) + def test_definition(self): + assert len(Section.m_def.base_sections) == 1 + assert len(Section.m_def.all_base_sections) == 1 + assert Section.m_def.m_follows(Definition.m_def) + class TestPureReflection: """ Test for using meta-info instances without knowing/using the respective definitions. """