Skip to content

Commit

Permalink
415 Generate identity as first step during entity initialization
Browse files Browse the repository at this point in the history
This commit also contains minor documentation fixes to related methods. It also
removes autogeneration of description in a field. Now description will only
be populated when explicity provided.
  • Loading branch information
subhashb committed May 12, 2024
1 parent 86e77c1 commit 7d562a8
Show file tree
Hide file tree
Showing 10 changed files with 503 additions and 74 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 @@ -49,7 +49,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)
return cls.meta_.entity_cls(**item)


class MemorySession:
Expand Down
55 changes: 40 additions & 15 deletions src/protean/core/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,28 @@ def __init__(self, *template, **kwargs): # noqa: C901
if isinstance(field_obj, Reference)
}

# Load the attributes based on the template
# Track fields that have been loaded
loaded_fields = []

# Pick identifier if provided in template or kwargs
# Generate if not provided
id_field_obj = id_field(self)
id_field_name = id_field_obj.field_name
if kwargs and id_field_name in kwargs:
setattr(self, id_field_name, kwargs.pop(id_field_name))
loaded_fields.append(id_field_name)
elif template:
for dictionary in template:
if id_field_name in dictionary:
setattr(self, id_field_name, dictionary[id_field_name])
loaded_fields.append(id_field_name)
break
else:
if type(id_field_obj) is Auto and not id_field_obj.increment:
setattr(self, id_field_name, generate_identity())
loaded_fields.append(id_field_name)

# Load the attributes based on the template
for dictionary in template:
if not isinstance(dictionary, dict):
raise AssertionError(
Expand All @@ -176,22 +196,23 @@ def __init__(self, *template, **kwargs): # noqa: C901
f"values.",
)
for field_name, val in dictionary.items():
if field_name not in kwargs:
if field_name not in kwargs and field_name not in loaded_fields:
kwargs[field_name] = val

# Now load against the keyword arguments
for field_name, val in kwargs.items():
try:
setattr(self, field_name, val)
except ValidationError as err:
for field_name in err.messages:
self.errors[field_name].extend(err.messages[field_name])
finally:
loaded_fields.append(field_name)

# Also note reference field name if its attribute was loaded
if field_name in reference_attributes:
loaded_fields.append(reference_attributes[field_name])
if field_name not in loaded_fields:
try:
setattr(self, field_name, val)
except ValidationError as err:
for field_name in err.messages:
self.errors[field_name].extend(err.messages[field_name])
finally:
loaded_fields.append(field_name)

# Also note reference field name if its attribute was loaded
if field_name in reference_attributes:
loaded_fields.append(reference_attributes[field_name])

# Load Value Objects from associated fields
# This block will dynamically construct value objects from field values
Expand Down Expand Up @@ -225,9 +246,13 @@ def __init__(self, *template, **kwargs): # noqa: C901
"{}_{}".format(field_name, sub_field_name)
].extend(err.messages[sub_field_name])

# Load Identities
# Load other identities
for field_name, field_obj in declared_fields(self).items():
if type(field_obj) is Auto and not field_obj.increment:
if (
field_name not in loaded_fields
and type(field_obj) is Auto
and not field_obj.increment
):
if not getattr(self, field_obj.field_name, None):
setattr(self, field_obj.field_name, generate_identity())
loaded_fields.append(field_obj.field_name)
Expand Down
76 changes: 61 additions & 15 deletions src/protean/fields/association.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,26 @@


class _ReferenceField(Field):
"""Shadow Attribute Field to back References"""
"""
Represents a reference field that can be used to establish associations between entities.
Args:
reference (str): The reference field as an attribute.
**kwargs: Additional keyword arguments to be passed to the base `Field` class.
"""

def __init__(self, reference, **kwargs):
"""Accept reference field as a an attribute, otherwise is a straightforward field"""
"""Accept reference field as an attribute, otherwise is a straightforward field"""
self.reference = reference
super().__init__(**kwargs)

def __set__(self, instance, value):
"""Override `__set__` to update relation field and keep it in sync with the shadow
attribute's value
Args:
instance: The instance of the class.
value: The value to be set.
"""
value = self._load(value)

Expand All @@ -29,20 +39,43 @@ def __set__(self, instance, value):
self._reset_values(instance)

def __delete__(self, instance):
"""Nullify values and linkages"""
"""Nullify values and linkages
Args:
instance: The instance of the class.
"""
self._reset_values(instance)

def _cast_to_type(self, value):
"""Verify type of value assigned to the shadow field"""
"""Verify the type of value assigned to the shadow field
Args:
value: The value to be assigned.
Returns:
The casted value.
"""
# FIXME Verify that the value being assigned is compatible with the remote field
return value

def as_dict(self, value):
"""Return JSON-compatible value of self"""
"""Return JSON-compatible value of self
Args:
value: The value to be converted to JSON.
Raises:
NotImplementedError: This method needs to be implemented in the derived class.
"""
raise NotImplementedError

def _reset_values(self, instance):
"""Reset all associated values and clean up dictionary items"""
"""Reset all associated values and clean up dictionary items
Args:
instance: The instance of the class.
"""
self.value = None
self.reference.value = None
instance.__dict__.pop(self.field_name, None)
Expand All @@ -52,11 +85,14 @@ def _reset_values(self, instance):

class Reference(FieldCacheMixin, Field):
"""
Provide a many-to-one relation by adding an attribute to the local entity
to hold the remote value.
By default ForeignKey will target the `id` column of the remote model but this
behavior can be changed by using the ``via`` argument.
A field representing a reference to another entity. This field is used to establish
the reverse relationship to the remote entity.
Args:
to_cls (str or Entity): The target entity class or its name.
via (str, optional): The linkage attribute between `via` and the designated
`id_field` of the target class.
**kwargs: Additional keyword arguments to be passed to the base `Field` class.
"""

def __init__(self, to_cls, via=None, **kwargs):
Expand Down Expand Up @@ -207,7 +243,17 @@ def as_dict(self, value):


class Association(FieldBase, FieldDescriptorMixin, FieldCacheMixin):
"""Base class for all association classes"""
"""
Represents an association between entities in a domain model.
An association field allows one entity to reference another entity in the domain model.
It provides methods to retrieve associated objects and handle changes in the association.
Args:
to_cls (class): The class of the target entity that this association references.
via (str, optional): The name of the linkage attribute between the associated entities.
If not provided, a default linkage attribute is generated based on the entity names.
"""

def __init__(self, to_cls, via=None, **kwargs):
super().__init__(**kwargs)
Expand Down Expand Up @@ -308,10 +354,10 @@ def has_changed(self):

class HasOne(Association):
"""
Provide a HasOne relation to a remote entity.
Represents a one-to-one association between two entities.
By default, the query will lookup an attribute of the form `<current_entity>_id`
to fetch and populate. This behavior can be changed by using the `via` argument.
This class is used to define a relationship where an instance of one entity
is associated with at most one instance of another entity.
"""

def __set__(self, instance, value):
Expand Down
44 changes: 31 additions & 13 deletions src/protean/fields/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,14 @@ def as_dict(self, value):


class List(Field):
"""Concrete field implementation for the List type."""
"""
A field that represents a list of values.
:param content_type: The type of the items in the list.
:type content_type: Field, optional
:param pickled: Whether the list should be pickled when stored, defaults to False.
:type pickled: bool, optional
"""

default_error_messages = {
"invalid": '"{value}" value must be of list type.',
Expand Down Expand Up @@ -256,12 +263,16 @@ def _cast_to_type(self, value):

def as_dict(self, value):
"""Return JSON-compatible value of self"""
# FIXME Convert value of objects that the list holds?
return value


class Dict(Field):
"""Concrete field implementation for the Dict type."""
"""
A field that represents a dictionary.
:param pickled: Whether to store the dictionary as a pickled object.
:type pickled: bool, optional
"""

default_error_messages = {
"invalid": '"{value}" value must be of dict type.',
Expand All @@ -288,15 +299,18 @@ def as_dict(self, value):


class Auto(Field):
"""Concrete field implementation for the Database Autogenerated types."""
"""
Auto Field represents an automatically generated field value.
def __init__(self, increment=False, **kwargs):
"""Initialize an Auto Field
Values of Auto-fields are generated automatically and cannot be set explicitly.
They cannot be marked as `required` for this reason - Protean does not accept
values supplied for Auto fields.
Values of Auto-fields are generated automatically and cannot be set explicitly.
They cannot be marked as `required` for this reason - Protean does not accept
values supplied for Auto fields.
"""
Args:
increment (bool): Flag indicating whether the field value should be incremented automatically.
"""

def __init__(self, increment=False, **kwargs):
self.increment = increment

super().__init__(**kwargs)
Expand Down Expand Up @@ -341,11 +355,15 @@ def __repr__(self):


class Identifier(Field):
"""Concrete field implementation for Identifiers.
"""
Represents an identifier field in a domain entity.
An identity field is immutable and cannot be changed once set.
An identifier field is used to uniquely identify an entity within a domain.
It can have different types such as UUID, string, or integer, depending on the configuration.
Values can be UUIDs, Integers or Strings.
:param identity_type: The type of the identifier field. If not provided, it will be picked from the domain config.
:type identity_type: str, optional
:raises ValidationError: If the provided identity type is not supported.
"""

def __init__(self, identity_type=None, **kwargs):
Expand Down
14 changes: 13 additions & 1 deletion src/protean/fields/embedded.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,19 @@ def _reset_values(self, instance):


class ValueObject(Field):
"""Field implementation for Value Objects"""
"""
Represents a field that holds a value object.
This field is used to embed a value object within an entity. It provides
functionality to handle the value object's fields and their values.
Args:
value_object_cls (class): The class of the value object to be embedded.
Attributes:
embedded_fields (dict): A dictionary that holds the embedded fields of the value object.
"""

def __init__(self, value_object_cls, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
9 changes: 0 additions & 9 deletions src/protean/fields/mixins.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import inflection

NOT_PROVIDED = object()


Expand Down Expand Up @@ -44,13 +42,6 @@ def __set_name__(self, entity_cls, name):
self.field_name = name
self.attribute_name = self.get_attribute_name()

# Set the description for this field
self.description = (
self.description
if self.description
else inflection.titleize(self.attribute_name).strip()
)

# Record Entity setting up the field
self._entity_cls = entity_cls

Expand Down
2 changes: 1 addition & 1 deletion src/protean/fields/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def __init__(
self.flags = flags
if self.flags and not isinstance(self.regex, str):
raise TypeError(
"If the flags are set, regex must be a regular expression string."
"If flags are set, regex must be a regular expression string."
)

self.regex = re.compile(self.regex, self.flags)
Expand Down
Loading

0 comments on commit 7d562a8

Please sign in to comment.