Skip to content

Commit

Permalink
415 Auto-add reference fields in child entities
Browse files Browse the repository at this point in the history
This commit includes changes that add a `Reference` field in the enclosed
child entity pointing to the aggregate, when an explicit reference has
not been provided.
  • Loading branch information
subhashb committed May 12, 2024
1 parent 7d562a8 commit e100bc7
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 140 deletions.
4 changes: 4 additions & 0 deletions src/protean/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,14 @@ def __create_id_field(new_class):
id_field = Auto(identifier=True)

setattr(new_class, "id", id_field)

# Set the name of the field on itself
id_field.__set_name__(new_class, "id")

# Set the name of the attribute on the class
setattr(new_class, _ID_FIELD_NAME, id_field.field_name)

# Add the attribute to _FIELDS for introspection
field_objects = getattr(new_class, _FIELDS)
field_objects["id"] = id_field
setattr(new_class, _FIELDS, field_objects)
53 changes: 39 additions & 14 deletions src/protean/core/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from protean.exceptions import IncorrectUsageError, NotSupportedError, ValidationError
from protean.fields import Auto, HasMany, Reference, ValueObject
from protean.fields.association import Association
from protean.reflection import attributes, declared_fields, fields, id_field
from protean.reflection import attributes, declared_fields, fields, id_field, _FIELDS
from protean.utils import (
DomainObjects,
derive_element_class,
Expand Down Expand Up @@ -111,19 +111,6 @@ class User(BaseEntity):
class Meta:
abstract = True

def __init_subclass__(subclass) -> None:
super().__init_subclass__()

subclass.__set_up_reference_fields()

@classmethod
def __set_up_reference_fields(subclass):
"""Walk through relation fields and setup shadow attributes"""
for _, field in declared_fields(subclass).items():
if isinstance(field, Reference):
shadow_field_name, shadow_field = field.get_shadow_field()
shadow_field.__set_name__(subclass, shadow_field_name)

def __init__(self, *template, **kwargs): # noqa: C901
"""
Initialise the entity object.
Expand Down Expand Up @@ -462,4 +449,42 @@ def entity_factory(element_cls, **kwargs):
}
)

# Set up reference fields
if not element_cls.meta_.abstract:
reference_field = None
for field_obj in declared_fields(element_cls).values():
if isinstance(field_obj, Reference):
# An explicit `Reference` field is already present
reference_field = field_obj
break

if reference_field is None:
# If no explicit Reference field is present, create one
reference_field = Reference(element_cls.meta_.aggregate_cls)

# If aggregate_cls is a string, set field name to inflection.underscore(aggregate_cls)
# Else, if it is a class, extract class name and set field name to inflection.underscore(class_name)
if isinstance(element_cls.meta_.aggregate_cls, str):
field_name = inflection.underscore(element_cls.meta_.aggregate_cls)
else:
field_name = inflection.underscore(
element_cls.meta_.aggregate_cls.__name__
)

setattr(element_cls, field_name, reference_field)

# Set the name of the field on itself
reference_field.__set_name__(element_cls, field_name)

# FIXME Centralize this logic to add fields dynamically to _FIELDS
field_objects = getattr(element_cls, _FIELDS)
field_objects[field_name] = reference_field
setattr(element_cls, _FIELDS, field_objects)

# Set up shadow fields for Reference fields
for _, field in fields(element_cls).items():
if isinstance(field, Reference):
shadow_field_name, shadow_field = field.get_shadow_field()
shadow_field.__set_name__(element_cls, shadow_field_name)

return element_cls
38 changes: 37 additions & 1 deletion tests/entity/elements.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from collections import defaultdict
from enum import Enum

from protean import BaseEntity
from protean import BaseAggregate, BaseEntity
from protean.fields import Auto, HasOne, Integer, String


class Account(BaseAggregate):
account_number = String(max_length=50, required=True)


class AbstractPerson(BaseEntity):
age = Integer(default=5)

Expand All @@ -16,46 +20,65 @@ class ConcretePerson(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)

class Meta:
aggregate_cls = "Account"


class Person(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)

class Meta:
aggregate_cls = "Account"


class PersonAutoSSN(BaseEntity):
ssn = Auto(identifier=True)
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)

class Meta:
aggregate_cls = "Account"


class PersonExplicitID(BaseEntity):
ssn = String(max_length=36, identifier=True)
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)

class Meta:
aggregate_cls = "Account"


class Relative(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)
relative_of = HasOne(Person)

class Meta:
aggregate_cls = "Account"


class Adult(Person):
pass

class Meta:
schema_name = "adults"
aggregate_cls = "Account"


class NotAPerson(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)

class Meta:
aggregate_cls = "Account"


# Entities to test Meta Info overriding # START #
class DbPerson(BaseEntity):
Expand All @@ -65,21 +88,25 @@ class DbPerson(BaseEntity):

class Meta:
schema_name = "pepes"
aggregate_cls = "Account"


class SqlPerson(Person):
class Meta:
schema_name = "people"
aggregate_cls = "Account"


class DifferentDbPerson(Person):
class Meta:
provider = "non-default"
aggregate_cls = "Account"


class SqlDifferentDbPerson(Person):
class Meta:
provider = "non-default-sql"
aggregate_cls = "Account"


class OrderedPerson(BaseEntity):
Expand All @@ -89,23 +116,32 @@ class OrderedPerson(BaseEntity):

class Meta:
order_by = "first_name"
aggregate_cls = "Account"


class OrderedPersonSubclass(Person):
class Meta:
order_by = "last_name"
aggregate_cls = "Account"


class BuildingStatus(Enum):
WIP = "WIP"
DONE = "DONE"


class Area(BaseAggregate):
name = String(max_length=50)


class Building(BaseEntity):
name = String(max_length=50)
floors = Integer()
status = String(choices=BuildingStatus)

class Meta:
aggregate_cls = "Area"

def defaults(self):
if not self.status:
if self.floors == 4:
Expand Down
132 changes: 13 additions & 119 deletions tests/entity/test_entity.py
Original file line number Diff line number Diff line change
@@ -1,125 +1,19 @@
from collections import defaultdict
from enum import Enum

from protean import BaseEntity
from protean.container import Options
from protean.fields import Auto, HasOne, Integer, String
from protean.fields import Auto, Integer, String
from protean.reflection import attributes, declared_fields


class AbstractPerson(BaseEntity):
age = Integer(default=5)

class Meta:
abstract = True


class ConcretePerson(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)


class Person(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)


class PersonAutoSSN(BaseEntity):
ssn = Auto(identifier=True)
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)


class PersonExplicitID(BaseEntity):
ssn = String(max_length=36, identifier=True)
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)


class Relative(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)
relative_of = HasOne(Person)


class Adult(Person):
class Meta:
schema_name = "adults"


class NotAPerson(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)


# Entities to test Meta Info overriding # START #
class DbPerson(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)

class Meta:
schema_name = "pepes"


class SqlPerson(Person):
class Meta:
schema_name = "people"


class DifferentDbPerson(Person):
class Meta:
provider = "non-default"


class SqlDifferentDbPerson(Person):
class Meta:
provider = "non-default-sql"


class OrderedPerson(BaseEntity):
first_name = String(max_length=50, required=True)
last_name = String(max_length=50)
age = Integer(default=21)

class Meta:
order_by = "first_name"


class OrderedPersonSubclass(Person):
class Meta:
order_by = "last_name"


class BuildingStatus(Enum):
WIP = "WIP"
DONE = "DONE"


class Building(BaseEntity):
name = String(max_length=50)
floors = Integer()
status = String(choices=BuildingStatus)

def defaults(self):
if not self.status:
if self.floors == 4:
self.status = BuildingStatus.DONE.value
else:
self.status = BuildingStatus.WIP.value

def clean(self):
errors = defaultdict(list)

if self.floors >= 4 and self.status != BuildingStatus.DONE.value:
errors["status"].append("should be DONE")

return errors
from .elements import (
AbstractPerson,
ConcretePerson,
Person,
PersonAutoSSN,
Relative,
SqlDifferentDbPerson,
SqlPerson,
DbPerson,
DifferentDbPerson,
Adult,
)


class TestEntityMeta:
Expand Down
Loading

0 comments on commit e100bc7

Please sign in to comment.