Commit 89c1b8f7 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Continued on the reflection interface and checking values against definitions.

parent b4292c1c
......@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict
from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, cast
import sys
......@@ -24,19 +24,15 @@ MObjectBound = TypeVar('MObjectBound', bound='MObject')
Discussion:
-----------
- Is the reflection interface really necessary? The ideas come from a compiled static
language (Java). In Python the non reflective interface can always be dynamically
generated and then reflectively used through pythons build in reflection mechanisms
(e.g. getattr, hasattr, ...)
- Subsection creation would be most fluent with: ``system = System(m_parent=run)``
- Specialized data entry methods would require refl. interface:
- m_add_array_values
- m_add
"""
# Reflection
class Enum(list):
pass
class MObjectMeta(type):
def __new__(self, cls_name, bases, dct):
......@@ -48,14 +44,59 @@ class MObjectMeta(type):
Content = Tuple[MObjectBound, Union[List[MObjectBound], MObjectBound], str, MObjectBound]
SectionDef = Union[str, 'Section', Type[MObjectBound]]
class MObject(metaclass=MObjectMeta):
def __init__(self, **kwargs):
self.m_section: 'Section' = kwargs.pop('m_section', getattr(self.__class__, 'm_section', None)) # TODO test with failure instead of None
"""Base class for all section objects 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
meta-info, since it allows to manipulate sections (and therefore all meta-info data)
without having to know the specific sub-class.
It also carries all the data for each section. All sub-classes only define specific
sections in terms of possible sub-sections and quantities. The data is managed here.
The reflection insterface for reading and manipulating quantity values consists of
Pythons build in ``getattr``, ``setattr``, and ``del``, as well as member functions
:func:`m_add_value`, and :func:`m_add_values`.
Sub-sections and parent sections can be read and manipulated with :data:`m_parent`,
:func:`m_sub_section`, :func:`m_create`.
```
system = run.m_create(System)
assert system.m_parent == run
assert run.m_sub_section(System, system.m_parent_index) == system
```
Attributes:
m_section: The section definition that defines this sections, its possible
sub-sections and quantities.
m_parent: The parent section instance that this section is a sub-section of.
m_parent_index: For repeatable sections, parent keep a list of sub-sections for
each section definition. This is the index of this section in the respective
parent sub-section list.
m_data: The dictionary that holds all data of this section. It keeps the quantity
values and sub-section. It should only be read directly (and never manipulated)
if you are know what you are doing. You should always use the reflection interface
if possible.
"""
def __init__(self, m_section: 'Section' = None, m_parent: 'MObject' = None, **kwargs):
self.m_section: 'Section' = m_section
self.m_parent: 'MObject' = m_parent
self.m_parent_index = -1
self.m_data = dict(**kwargs)
if self.m_section is None:
self.m_section = getattr(self.__class__, 'm_section', None)
else:
assert self.m_section == getattr(self.__class__, 'm_section', self.m_section), \
'Section class and section definition must match'
@classmethod
def __init_section_cls__(cls):
if not hasattr(__module__, 'Quantity') or not hasattr(__module__, 'Section'):
......@@ -67,13 +108,107 @@ class MObject(metaclass=MObjectMeta):
m_section = Section()
setattr(cls, 'm_section', m_section)
m_section.name = cls.__name__
m_section.section_cls = cls
for name, value in cls.__dict__.items():
if isinstance(value, Quantity):
value.name = name
m_section.m_add('Quantity', value)
# manual manipulation of m_data due to bootstrapping
m_section.m_data.setdefault('Quantity', []).append(value)
@staticmethod
def __type_check(definition: 'Quantity', value: Any, check_item: bool = False):
"""Checks if the value fits the given quantity in type and shape; raises
ValueError if not."""
def check_value(value):
if isinstance(definition.type, Enum):
if value not in definition.type:
raise ValueError('Not one of the enum values.')
elif isinstance(definition.type, type):
if not isinstance(value, definition.type):
raise ValueError('Value has wrong type.')
elif isinstance(definition.type, Section):
if not isinstance(value, MObject) or value.m_section != definition.type:
raise ValueError('The value is not a section of wrong section definition')
else:
raise Exception('Invalid quantity type: %s' % str(definition.type))
shape = None
try:
shape = definition.shape
except KeyError:
pass
if shape is None or len(shape) == 0 or check_item:
check_value(value)
elif len(shape) == 1:
if not isinstance(value, list):
raise ValueError('Wrong shape')
for item in value:
check_value(item)
else:
# TODO
raise Exception('Higher shapes not implemented')
# TODO check dimension
def __resolve_section(self, definition: SectionDef) -> 'Section':
"""Resolves and checks the given section definition. """
if isinstance(definition, str):
section = self.m_section.sub_sections[definition]
else:
if isinstance(definition, type):
section = getattr(definition, 'm_section')
else:
section = definition
if section.name not in self.m_section.sub_sections:
raise KeyError('Not a sub section.')
return section
def m_create(self, section: Union['Section', Type[MObjectBound]], **kwargs) -> MObjectBound:
def m_sub_section(self, definition: SectionDef, parent_index: int = -1) -> MObjectBound:
"""Returns the sub section for the given section definition and possible
parent_index (for repeatable sections).
Args:
definition: The definition of the section.
parent_index: The index of the desired section. This can be omitted for non
repeatable sections. If omitted for repeatable sections a exception
will be raised, if more then one sub-section exists. Likewise, if the given
index is out of range.
Raises:
KeyError: If the definition is not for a sub section
IndexError: If the given index is wrong, or if an index is given for a non
repeatable section
"""
section_def = self.__resolve_section(definition)
m_data_value = self.m_data[section_def.name]
if isinstance(m_data_value, list):
m_data_values = m_data_value
if parent_index == -1:
if len(m_data_values) == 1:
return m_data_values[0]
else:
raise IndexError()
else:
return m_data_values[parent_index]
else:
if parent_index != -1:
raise IndexError('Not a repeatable sub section.')
else:
return m_data_value
def m_create(self, definition: SectionDef, **kwargs) -> MObjectBound:
"""Creates a subsection and adds it this this section
Args:
......@@ -85,44 +220,67 @@ class MObject(metaclass=MObjectMeta):
The created subsection
Raises:
ValueError: If the given section is not a subsection of this section, or
this given definition is not a section at all.
KeyError: If the given section is not a subsection of this section.
"""
section_def: 'Section' = None
if isinstance(section, type) and hasattr(section, 'm_section'):
section_def = section.m_section
elif isinstance(section, Section):
section_def = section
else:
raise ValueError('Not a section definition')
if section_def.parent != self.m_section:
raise ValueError('Not a subsection')
section_def: 'Section' = self.__resolve_section(definition)
section_cls = section_def.section_cls
section_instance = section_cls(**kwargs)
section_instance = section_cls(m_section=section_def, m_parent=self, **kwargs)
if section_def.repeats:
self.m_data.setdefault(section_def.name, []).append(section_instance)
m_data_sections = self.m_data.setdefault(section_def.name, [])
section_index = len(m_data_sections)
m_data_sections.append(section_instance)
section_instance.m_parent_index = section_index
else:
self.m_data[section_def.name] = section_instance
return section_instance
return cast(MObjectBound, section_instance)
def m_add(self, name: str, value: Any):
values = self.m_data.setdefault(name, [])
values.append(value)
def __resolve_quantity(self, definition: Union[str, 'Quantity']) -> 'Quantity':
"""Resolves and checks the given quantity definition. """
if isinstance(definition, str):
quantity = self.m_section.quantities[definition]
def m_add_array_values(self, definition: Union[str, 'Quantity'], values, offset: int):
pass
else:
if definition.m_parent != self.m_section:
raise KeyError('Quantity is not a quantity of this section.')
quantity = definition
return quantity
def m_add(self, definition: Union[str, 'Quantity'], value: Any):
"""Adds the given value to the given quantity."""
def m_to_dict(self):
quantity = self.__resolve_quantity(definition)
MObject.__type_check(quantity, value, check_item=True)
m_data_values = self.m_data.setdefault(quantity.name, [])
m_data_values.append(value)
def m_add_values(self, definition: Union[str, 'Quantity'], values: Iterable[Any]):
"""Adds the given values to the given quantity."""
quantity = self.__resolve_quantity(definition)
for value in values:
MObject.__type_check(quantity, value, check_item=True)
m_data_values = self.m_data.setdefault(quantity.name, [])
for value in values:
m_data_values.append(value)
def m_to_dict(self) -> Dict[str, Any]:
"""Returns the data of this section as a json serializeable dictionary. """
pass
def m_to_json(self):
"""Returns the data of this section as a json string. """
pass
def m_all_contents(self) -> Iterable[Content]:
"""Returns an iterable over all sub and sub subs sections. """
for content in self.m_contents():
for sub_content in content[0].m_all_contents():
yield sub_content
......@@ -130,6 +288,7 @@ class MObject(metaclass=MObjectMeta):
yield content
def m_contents(self) -> Iterable[Content]:
"""Returns an iterable over all direct subs sections. """
for name, attr in self.m_data.items():
if isinstance(attr, list):
for value in attr:
......@@ -148,10 +307,6 @@ class MObject(metaclass=MObjectMeta):
return '%s:%s' % (name, m_section_name)
class Enum(list):
pass
# M3
class Quantity(MObject):
......@@ -168,6 +323,7 @@ class Quantity(MObject):
return obj.m_data[self.__name]
def __set__(self, obj, value):
MObject.__dict__['_MObject__type_check'].__get__(MObject)(self, value)
obj.m_data[self.__name] = value
def __delete__(self, obj):
......@@ -176,6 +332,7 @@ class Quantity(MObject):
class Section(MObject):
m_section: 'Section' = None
section_cls: Type[MObject] = None
name: 'Quantity' = None
repeats: 'Quantity' = None
parent: 'Quantity' = None
......@@ -184,7 +341,6 @@ class Section(MObject):
__all_instances: List['Section'] = []
default = property(lambda self: [] if self.repeats else None)
section_cls = property(lambda self: self.__class__)
def __init__(self, **kwargs):
# The mechanism that produces default values, depends on parent. Without setting
......@@ -225,6 +381,7 @@ class Section(MObject):
Section.m_section = Section(repeats=True, name='Section')
Section.m_section.m_section = Section.m_section
Section.m_section.section_cls = Section
Section.name = Quantity(type=str, name='name')
Section.repeats = Quantity(type=bool, name='repeats')
......@@ -232,6 +389,7 @@ Section.parent = Quantity(type=Section.m_section, name='parent')
Section.extends = Quantity(type=Section.m_section, shape=['0..*'], name='extends')
Quantity.m_section = Section(repeats=True, parent=Section.m_section, name='Quantity')
Quantity.m_section.section_cls = Quantity
Quantity.name = Quantity(type=str, name='name')
Quantity.type = Quantity(type=Union[type, Enum, Section], name='type')
Quantity.shape = Quantity(type=Union[str, int], shape=['0..*'], name='shape')
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment