Source code for elasticai.creator.ir.base.ir_data_meta
from collections.abc import MutableMapping
from types import GenericAlias, MappingProxyType, resolve_bases
from typing import Any, cast
from .required_field import SimpleRequiredField, is_required_field
[docs]
class IrDataMeta(type):
"""
This implementation tries to find a good compromise between
avoiding boiler plate code and compatibility with
static type checkers.
The metaclass provides automatic generation of an init function
and will convert any type annotations with `Attribute` types into corresponding
`MandatoryField`s.
To **not** generate an init function define your class like this
```python
class C(metaclass=IrDataMeta, create_init=False):
name: str
```
We recommend inheriting from the `IrData` class instead as this provides
useful checks for data integrity and an init function that will be detected
by static type checkers.
IMPORTANT: Type annotations for names starting with `_` are excluded from this!
NOTE:
Other options of implementation have been considered and tried:
- dynamically add fields and init function after class creation
- (-)(-) neither fields nor data attribute detected by static type checkers
- (-) __set_name__ callbacks are not triggered
- (+) easier to understand
- (-) __slots__ need to be defined manually by the user, which will certainly lead
to hard to find bugs.
- a decorator that builds a new class taking the decorated class as a prototype
- (+)(-) type hints for fields are picked up
- (-) a lot of boiler plate to make type checkers detect init and other attributes
- (+) if it wasn't for the type checks, this would be easy to understand and maintain
and have the least amount of boiler plate
"""
@property
def required_fields(self) -> MappingProxyType[str, type]:
"""
In `IrData` and its children this will be available
as a class property. We add it here because python3.13 removed
the ability to combine classmethod and property
"""
return self._fields.keys().mapping # type: ignore
[docs]
@classmethod
def __prepare__(cls, name, bases, create_init=False, **kwds):
"""
called before parsing class body and thus before metaclass instantiation,
hence this is a classmethod
"""
namespace = super().__prepare__(name, bases, **kwds)
cls.__add_data_slot(namespace)
if create_init:
cls.__add_init(namespace)
namespace["_fields"] = {}
return namespace
@classmethod
def __add_init(cls, namespace):
def init(self, data):
self.data = data
namespace["__init__"] = init
[docs]
def __new__(
cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwds
) -> "IrDataMeta":
"""
Create and return a new IrDataMeta object.
This is the type for new IrData classes.
Fields defined in the namespace are fields of the class, not fields
of the instantiated object. E.g., the deriving class will feature
an `__init__` method, the owner of this method is the class, i.e., an object
of type `IRDataMeta`. When accessing such a method attribute via the `dot`
from an object with metaclass `IrDataMeta`, the method will be bound
to that specific instance on the fly.
As such, when dynamically creating the mandatory fields below,
this happens once per class definition, not per instantiation
of such a class.
"""
inherited_fields = cls.__get_inherited_fields(bases)
generated_fields = cls.__get_fields_to_be_generated(namespace)
user_defined_fields = cls.__get_user_defined_fields_from_annotations(namespace)
for name in generated_fields:
namespace[name] = SimpleRequiredField()
namespace["_fields"] = inherited_fields | generated_fields | user_defined_fields
return super().__new__(cls, name, bases, namespace)
@staticmethod
def __get_inherited_fields(bases):
inherited_fields = {}
for o in resolve_bases(bases):
if isinstance(o, IrDataMeta):
inherited_fields.update(o._fields) # type: ignore
return inherited_fields
@staticmethod
def __add_data_slot(namespace: MutableMapping[str, object]) -> None:
namespace["__slots__"] = ("data",) + cast(
tuple, namespace.get("__slots__", tuple())
)
@staticmethod
def __get_user_defined_fields_from_annotations(
namespace: dict[str, Any],
) -> dict[str, type]:
annotations = namespace.get("__annotations__", {})
fields = dict()
for name, annotation in annotations.items():
if not name.startswith("_") and name in namespace:
item = namespace[name]
if is_required_field(item):
stored_type = annotation.__args__[0]
fields[name] = stored_type
return cast(dict[str, type], fields)
@staticmethod
def __get_fields_to_be_generated(
namespace: dict[str, Any],
) -> dict[str, type]:
annotations = namespace.get("__annotations__", {})
fields: dict[str, type] = dict()
for name, annotation in annotations.items():
if name.startswith("_") or name in namespace:
continue
fields[name] = IrDataMeta.__get_annotations_type(annotation)
return fields
@staticmethod
def __get_annotations_type(annotation) -> type:
if isinstance(annotation, str):
annotation = eval(annotation, globals(), locals())
if isinstance(annotation, GenericAlias):
_type = annotation.__origin__
else:
_type = annotation
return _type