Skip to content

Commit

Permalink
Merge branch 'entity-move-to-container'
Browse files Browse the repository at this point in the history
  • Loading branch information
subhashb committed Sep 1, 2021
2 parents aa67f94 + 1b49dd6 commit 2ff3444
Show file tree
Hide file tree
Showing 22 changed files with 127 additions and 321 deletions.
2 changes: 1 addition & 1 deletion src/protean/adapters/repository/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def from_entity(cls, entity) -> "MemoryModel":
@classmethod
def to_entity(cls, item: "MemoryModel"):
"""Convert the dictionary record to an entity """
return cls.meta_.entity_cls(item, raise_errors=False)
return cls.meta_.entity_cls(item)


class MemorySession:
Expand Down
2 changes: 1 addition & 1 deletion src/protean/adapters/repository/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ def to_entity(cls, model_obj: "SqlalchemyModel"):
item_dict = {}
for field_name in attributes(cls.meta_.entity_cls):
item_dict[field_name] = getattr(model_obj, field_name, None)
return cls.meta_.entity_cls(item_dict, raise_errors=False)
return cls.meta_.entity_cls(item_dict)


class SADAO(BaseDAO):
Expand Down
11 changes: 9 additions & 2 deletions src/protean/core/aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import logging

from protean.core.entity import BaseEntity
from protean.utils import DomainObjects, derive_element_class
from protean.utils import DomainObjects, derive_element_class, inflection

logger = logging.getLogger("protean.domain.aggregate")

Expand Down Expand Up @@ -43,9 +43,16 @@ def __new__(cls, *args, **kwargs):
raise TypeError("BaseAggregate cannot be instantiated")
return super().__new__(cls)

class Meta:
abstract = True

@classmethod
def _default_options(cls):
return [("provider", "default"), ("model", None)]
return [
("provider", "default"),
("model", None),
("schema_name", inflection.underscore(cls.__name__)),
]


def aggregate_factory(element_cls, **kwargs):
Expand Down
229 changes: 56 additions & 173 deletions src/protean/core/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
)
from protean.utils.container import (
_FIELDS,
BaseContainer,
OptionsMixin,
fields,
_ID_FIELD_NAME,
id_field,
Expand All @@ -40,175 +42,6 @@
logger = logging.getLogger("protean.domain.entity")


class _EntityMetaclass(type):
"""
This base metaclass processes the class declaration and constructs a meta object that can
be used to introspect the Entity class later. Specifically, it sets up a `meta_` attribute on
the Entity to an instance of Meta, either the default of one that is defined in the
Entity class.
`meta_` is setup with these attributes:
* `declared_fields`: A dictionary that gives a list of any instances of `Field`
included as attributes on either the class or on any of its superclasses
* `id_field`: The Primary identifier attribute of the Entity
"""

def __new__(mcs, name, bases, attrs, **kwargs):
"""Initialize Entity MetaClass and load attributes"""

# Ensure initialization is only performed for subclasses of Entity
# (excluding Entity class itself).
parents = [b for b in bases if isinstance(b, _EntityMetaclass)]
if not parents:
return super().__new__(mcs, name, bases, attrs)

# Remove `abstract` if defined in base classes
for base in bases:
if hasattr(base, "Meta") and hasattr(base.Meta, "abstract"):
delattr(base.Meta, "abstract")

new_class = super().__new__(mcs, name, bases, attrs, **kwargs)

# Gather `Meta` class/object if defined
attr_meta = attrs.pop(
"Meta", None
) # Gather Metadata defined in inner `Meta` class
entity_meta = EntityMeta(name, attr_meta) # Initialize the Metadata container
setattr(
new_class, "meta_", entity_meta
) # Associate the Metadata container with new class

# Load declared fields
new_class._load_fields(attrs)

# Load declared fields from Base class, in case this Entity is subclassing another
new_class._load_base_class_fields(bases, attrs)

# Lookup an already defined ID field or create an `Auto` field
new_class._set_id_field()

# FIXME Temporary change until entity is moved to Container completely
setattr(new_class, _FIELDS, new_class.meta_.declared_fields)

return new_class

def _load_base_class_fields(new_class, bases, attrs):
"""If this class is subclassing another Entity, add that Entity's
fields. Note that we loop over the bases in *reverse*.
This is necessary in order to maintain the correct order of fields.
"""
for base in reversed(bases):
if hasattr(base, "meta_") and hasattr(base.meta_, "declared_fields"):
base_class_fields = {
field_name: field_obj
for (field_name, field_obj) in fields(base).items()
if (
field_name not in attrs
and not isinstance(field_obj, Association)
and not field_obj.identifier
)
}
new_class._load_fields(base_class_fields)

def _load_fields(new_class, attrs):
"""Load field items into Class.
This method sets up the primary attribute of an association.
"""
for attr_name, attr_obj in attrs.items():
if isinstance(attr_obj, (Association, Field, Reference)):
setattr(new_class, attr_name, attr_obj)
new_class.meta_.declared_fields[attr_name] = attr_obj
else:
if isinstance(attr_obj, BaseEntity):
raise IncorrectUsageError(
{
"_entity": [
f"`{attr_name}` of type `{type(attr_obj).__name__}` cannot be part of an entity."
]
}
)

def _set_id_field(new_class):
"""Lookup the id field for this entity and assign"""
# FIXME What does it mean when there are no declared fields?
# Does it translate to an abstract entity?
try:
id_field = next(
field
for _, field in new_class.meta_.declared_fields.items()
if isinstance(field, (Field, Reference)) and field.identifier
)

setattr(new_class, _ID_FIELD_NAME, id_field.field_name)

# If the aggregate/entity has been marked abstract,
# and contains an identifier field, raise exception
if new_class.meta_.abstract and id_field:
raise IncorrectUsageError(
{
"_entity": [
f"Abstract Aggregate `{new_class.__name__}` marked as abstract cannot have"
" identity fields"
]
}
)
except StopIteration:
# If no id field is declared then create one
# If the aggregate/entity is marked abstract,
# avoid creating an identifier field.
if not new_class.meta_.abstract:
new_class._create_id_field()

def _create_id_field(new_class):
"""Create and return a default ID field that is Auto generated"""
id_field = Auto(identifier=True)

setattr(new_class, "id", id_field)
id_field.__set_name__(new_class, "id")

# Ensure ID field is updated properly in Meta attribute
new_class.meta_.declared_fields["id"] = id_field

setattr(new_class, _ID_FIELD_NAME, id_field.field_name)


class EntityMeta:
""" Metadata info for the entity.
Options:
- ``abstract``: Indicates that this is an abstract entity (Ignores all other meta options)
- ``schema_name``: name of the schema (table/index/doc) used for persistence of this entity
defaults to underscore version of the Entity name.
- ``provider``: the name of the datasource associated with this
entity, default value is `default`.
Also acts as a placeholder for generated entity fields like:
:declared_fields: dict
Any instances of `Field` included as attributes on either the class
or on any of its superclasses will be include in this dictionary.
:id_field: protean.core.Field
An instance of the field that will serve as the unique identifier for the entity
FIXME Make `EntityMeta` immutable
"""

def __init__(self, entity_name, meta):
self.abstract = getattr(meta, "abstract", None) or False
self.schema_name = getattr(meta, "schema_name", None) or inflection.underscore(
entity_name
)
self.provider = getattr(meta, "provider", None) or "default"
self.model = getattr(meta, "model", None)

# Initialize Options
self.declared_fields = {}

# Domain Attributes
self.aggregate_cls = getattr(meta, "aggregate_cls", None)


class _FieldsCacheDescriptor:
def __get__(self, instance, cls=None):
if instance is None:
Expand Down Expand Up @@ -263,7 +96,7 @@ def mark_destroyed(self):
fields_cache = _FieldsCacheDescriptor()


class BaseEntity(metaclass=_EntityMetaclass):
class BaseEntity(BaseContainer, OptionsMixin):
"""The Base class for Protean-Compliant Domain Entities.
Provides helper methods to custom define entity attributes, and query attribute names
Expand Down Expand Up @@ -296,7 +129,58 @@ class User(BaseEntity):

element_type = DomainObjects.ENTITY

def __init__(self, *template, raise_errors=True, **kwargs): # noqa: C901
def __init_subclass__(subclass) -> None:
super().__init_subclass__()

subclass.__set_id_field()

@classmethod
def __set_id_field(new_class):
"""Lookup the id field for this entity and assign"""
# FIXME What does it mean when there are no declared fields?
# Does it translate to an abstract entity?
try:
id_field = next(
field
for _, field in fields(new_class).items()
if isinstance(field, (Field, Reference)) and field.identifier
)

setattr(new_class, _ID_FIELD_NAME, id_field.field_name)

# If the aggregate/entity has been marked abstract,
# and contains an identifier field, raise exception
if new_class.meta_.abstract and id_field:
raise IncorrectUsageError(
{
"_entity": [
f"Abstract Aggregate `{new_class.__name__}` marked as abstract cannot have"
" identity fields"
]
}
)
except StopIteration:
# If no id field is declared then create one
# If the aggregate/entity is marked abstract,
# avoid creating an identifier field.
if not new_class.meta_.abstract:
new_class.__create_id_field()

@classmethod
def __create_id_field(new_class):
"""Create and return a default ID field that is Auto generated"""
id_field = Auto(identifier=True)

setattr(new_class, "id", id_field)
id_field.__set_name__(new_class, "id")

setattr(new_class, _ID_FIELD_NAME, id_field.field_name)

field_objects = getattr(new_class, _FIELDS)
field_objects["id"] = id_field
setattr(new_class, _FIELDS, field_objects)

def __init__(self, *template, **kwargs): # noqa: C901
"""
Initialise the entity object.
Expand All @@ -323,7 +207,6 @@ def __init__(self, *template, raise_errors=True, **kwargs): # noqa: C901
)

self.errors = defaultdict(list)
self.raise_errors = raise_errors

# Set up the storage for instance state
self.state_ = _EntityState()
Expand Down Expand Up @@ -433,7 +316,7 @@ def __init__(self, *template, raise_errors=True, **kwargs): # noqa: C901
self.errors[field].extend(custom_errors[field])

# Raise any errors found during load
if self.errors and self.raise_errors:
if self.errors:
logger.error(f"Error during initialization: {dict(self.errors)}")
raise ValidationError(self.errors)

Expand Down
23 changes: 3 additions & 20 deletions src/protean/core/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)
from protean.exceptions import NotSupportedError
from protean.utils import DomainObjects, derive_element_class
from protean.utils.container import _FIELDS
from protean.utils.container import _FIELDS, Options
from protean.utils.container import Element

logger = logging.getLogger("protean.application.serializer")
Expand Down Expand Up @@ -138,30 +138,13 @@ def _declared_fields(attrs):
# Gather `Meta` class/object if defined
attr_meta = attrs.pop("Meta", None)
meta = attr_meta or getattr(new_class, "Meta", None)
setattr(new_class, "meta_", SerializerMeta(meta, declared_fields))
setattr(new_class, "meta_", Options(meta))

setattr(new_class, _FIELDS, new_class.meta_.declared_fields)
setattr(new_class, _FIELDS, declared_fields)

return new_class


class SerializerMeta:
""" Metadata info for the Serializer.
Also acts as a placeholder for generated entity fields like:
:declared_fields: dict
Any instances of `Field` included as attributes on either the class
or on any of its superclasses will be include in this dictionary.
"""

def __init__(self, meta, declared_fields):
self.aggregate_cls = getattr(meta, "aggregate_cls", None)

# Initialize Options
self.declared_fields = declared_fields if declared_fields else {}


class BaseSerializer(Element, metaclass=_SerializerMetaclass):
"""The Base class for Protean-Compliant Serializers.
Expand Down
Loading

0 comments on commit 2ff3444

Please sign in to comment.