metainfo.py 14.2 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
# 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.
14

15
from typing import Type, TypeVar, Union, Tuple, Iterable, List, Any, Dict, cast
16
import sys
17
18


19
20
__module__ = sys.modules[__name__]
MObjectBound = TypeVar('MObjectBound', bound='MObject')
21

22
23
24
25
26
27
28
"""

Discussion:
-----------

"""

29

30
# Reflection
31

32
33
34
35
class Enum(list):
    pass


36
class MObjectMeta(type):
37

38
39
40
41
42
43
    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()
        return cls
44
45


46
Content = Tuple[MObjectBound, Union[List[MObjectBound], MObjectBound], str, MObjectBound]
47
SectionDef = Union[str, 'Section', Type[MObjectBound]]
48
49


50
class MObject(metaclass=MObjectMeta):
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
    """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
92
        self.m_data = dict(**kwargs)
93

94
95
96
97
98
99
        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'

100
101
102
103
104
    @classmethod
    def __init_section_cls__(cls):
        if not hasattr(__module__, 'Quantity') or not hasattr(__module__, 'Section'):
            # no initialization during bootstrapping, will be done maunally
            return
105

106
107
108
109
110
        m_section = getattr(cls, 'm_section', None)
        if m_section is None:
            m_section = Section()
            setattr(cls, 'm_section', m_section)
        m_section.name = cls.__name__
111
        m_section.section_cls = cls
112

113
114
115
        for name, value in cls.__dict__.items():
            if isinstance(value, Quantity):
                value.name = name
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
                # 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
176

177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
    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:
212
        """Creates a subsection and adds it this this section
213

214
215
216
217
        Args:
            section: The section definition of the subsection. It is either the
                definition itself, or the python class representing the section definition.
            **kwargs: Are used to initialize the subsection.
218

219
220
        Returns:
            The created subsection
221

222
        Raises:
223
            KeyError: If the given section is not a subsection of this section.
224
        """
225
        section_def: 'Section' = self.__resolve_section(definition)
226

227
        section_cls = section_def.section_cls
228
        section_instance = section_cls(m_section=section_def, m_parent=self, **kwargs)
229

230
        if section_def.repeats:
231
232
233
234
            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
235
        else:
236
            self.m_data[section_def.name] = section_instance
237

238
        return cast(MObjectBound, section_instance)
239

240
241
242
243
    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]
244

245
246
247
248
249
250
251
252
253
        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."""
254

255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
        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. """
276
        pass
277

278
    def m_to_json(self):
279
        """Returns the data of this section as a json string. """
280
        pass
281

282
    def m_all_contents(self) -> Iterable[Content]:
283
        """Returns an iterable over all sub and sub subs sections. """
284
285
286
        for content in self.m_contents():
            for sub_content in content[0].m_all_contents():
                yield sub_content
287

288
            yield content
289

290
    def m_contents(self) -> Iterable[Content]:
291
        """Returns an iterable over all direct subs sections. """
292
293
294
295
296
        for name, attr in self.m_data.items():
            if isinstance(attr, list):
                for value in attr:
                    if isinstance(value, MObject):
                        yield value, attr, name, self
297

298
299
            elif isinstance(attr, MObject):
                yield value, value, name, self
300

301
302
303
304
305
306
307
    def __repr__(self):
        m_section_name = self.m_section.name
        name = ''
        if 'name' in self.m_data:
            name = self.m_data['name']

        return '%s:%s' % (name, m_section_name)
308
309


310
# M3
311

312
313
314
315
316
class Quantity(MObject):
    m_section: 'Section' = None
    name: 'Quantity' = None
    type: 'Quantity' = None
    shape: 'Quantity' = None
317

318
    __name = property(lambda self: self.m_data['name'])
319

320
    default = property(lambda self: None)
321

322
    def __get__(self, obj, type=None):
323
        return obj.m_data[self.__name]
324

325
    def __set__(self, obj, value):
326
        MObject.__dict__['_MObject__type_check'].__get__(MObject)(self, value)
327
        obj.m_data[self.__name] = value
328

329
    def __delete__(self, obj):
330
        del obj.m_data[self.__name]
331
332


333
334
class Section(MObject):
    m_section: 'Section' = None
335
    section_cls: Type[MObject] = None
336
337
338
339
    name: 'Quantity' = None
    repeats: 'Quantity' = None
    parent: 'Quantity' = None
    extends: 'Quantity' = None
340

341
    __all_instances: List['Section'] = []
342

343
    default = property(lambda self: [] if self.repeats else None)
344

345
346
347
348
    def __init__(self, **kwargs):
        # The mechanism that produces default values, depends on parent. Without setting
        # the parent default manually, an endless recursion will occur.
        kwargs.setdefault('parent', None)
349

350
351
        super().__init__(**kwargs)
        Section.__all_instances.append(self)
352

353
354
355
356
    # TODO cache
    @property
    def attributes(self) -> Dict[str, Union['Section', Quantity]]:
        """ All attribute (sub section and quantity) definitions. """
357

358
359
360
        attributes: Dict[str, Union[Section, Quantity]] = dict(**self.quantities)
        attributes.update(**self.sub_sections)
        return attributes
361

362
363
364
365
    # TODO cache
    @property
    def quantities(self) -> Dict[str, Quantity]:
        """ All quantity definition in the given section definition. """
366

367
368
369
        return {
            quantity.name: quantity
            for quantity in self.m_data.get('Quantity', [])}
370

371
372
373
374
    # TODO cache
    @property
    def sub_sections(self) -> Dict[str, 'Section']:
        """ All sub section definitions for this section definition. """
375

376
377
378
379
        return {
            sub_section.name: sub_section
            for sub_section in Section.__all_instances
            if sub_section.parent == self}
380
381


382
383
Section.m_section = Section(repeats=True, name='Section')
Section.m_section.m_section = Section.m_section
384
Section.m_section.section_cls = Section
385

386
387
388
389
Section.name = Quantity(type=str, name='name')
Section.repeats = Quantity(type=bool, name='repeats')
Section.parent = Quantity(type=Section.m_section, name='parent')
Section.extends = Quantity(type=Section.m_section, shape=['0..*'], name='extends')
390

391
Quantity.m_section = Section(repeats=True, parent=Section.m_section, name='Quantity')
392
Quantity.m_section.section_cls = Quantity
393
394
395
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')
396
397


398
399
400
class Package(MObject):
    m_section = Section()
    name = Quantity(type=str)
401
402


403
Section.m_section.parent = Package.m_section
404
405


406
407
class Definition(MObject):
    m_section = Section(extends=[Section.m_section, Quantity.m_section, Package.m_section])
408

409
    description = Quantity(type=str)