Skip to content

Commit

Permalink
Fix issues that prevented model creation
Browse files Browse the repository at this point in the history
  • Loading branch information
jacklinke committed May 25, 2024
1 parent 94be135 commit fc84af0
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 113 deletions.
2 changes: 1 addition & 1 deletion src/django_segments/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@


# There is likely no reason ever to change the model base, but it is provided as an setting here for completeness.
SEGMENT_MODEL_BASE = getattr(settings, "SEGMENT_MODEL_BASE", ModelBase)
DJANGO_SEGMENTS_MODEL_BASE = getattr(settings, "DJANGO_SEGMENTS_MODEL_BASE", ModelBase)

# Define the allowed PostgreSQL range field types
POSTGRES_RANGE_FIELDS = getattr(
Expand Down
142 changes: 128 additions & 14 deletions src/django_segments/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import logging

from django.apps import apps
from django.core.exceptions import FieldDoesNotExist
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.db.models.signals import class_prepared
from django.utils.translation import gettext as _

from django_segments.app_settings import DJANGO_SEGMENTS_MODEL_BASE as ModelBase
from django_segments.app_settings import POSTGRES_RANGE_FIELDS
from django_segments.exceptions import IncorrectSegmentRangeError
from django_segments.exceptions import IncorrectSpanRangeError
Expand Down Expand Up @@ -37,6 +40,8 @@ def __init__(self, range_field1: models.Field, range_field2: models.Field) -> No
self.range_field1 = range_field1
self.range_field2 = range_field2

logger.debug("RangeTypesMatchHelper __init__(): %s, %s", type(self.range_field1), type(self.range_field2))

def validate_range_types_match(self) -> None:
"""Ensure that the range types match."""
range_type1 = self.range_field1.get_internal_type()
Expand Down Expand Up @@ -64,15 +69,27 @@ def __init__(
self.range_field_attr = range_field_attr
self.error_class = error_class

logger.debug(
"RangeValidationHelper __init__(): %s, %s, %s, %s",
self.model,
self.range_field_name_attr,
self.range_field_attr,
self.error_class,
)

self.range_field_name = getattr(self.model, self.range_field_name_attr, None)
self.range_field = getattr(self.model, self.range_field_attr, None)
range_field_deferred_attr = getattr(self.model, self.range_field_attr, None)

self.range_field = getattr(range_field_deferred_attr, "field", None) if range_field_deferred_attr else None
logger.debug("RangeValidationHelper __init__(): %s, %s", self.range_field_name, self.range_field)

def get_validated_range_field(self) -> models.Field:
"""Return the validated range field."""
return self.validate_range_field()

def validate_range_field(self) -> models.Field:
"""Ensure one and only one of range_field_name or range_field is defined."""
logger.debug("RangeValidationHelper validate_range_field(): %s, %s", self.range_field_name, self.range_field)

# Ensure that one and only one of range_field_name or range_field is defined
if self.range_field_name is None and self.range_field is None:
Expand Down Expand Up @@ -135,6 +152,18 @@ def get_validated_current_range_field(self) -> models.Field:
return self.get_validated_range_field()


class SegmentRangeValidationHelper(RangeValidationHelper):
"""Helper class for validating segment models."""

def __init__(self, model: type[models.Model]) -> None:
"""Initialize the helper with the model."""
super().__init__(model, "segment_range_field_name", "segment_range", IncorrectSegmentRangeError)

def get_validated_segment_range_field(self) -> models.Field:
"""Return the validated segment range field."""
return self.get_validated_range_field()


class BoundaryHelper:
"""Helper class used by AbstractSpan and AbstractSegment to set the boundaries of the range field."""

Expand Down Expand Up @@ -187,18 +216,6 @@ def validate_value_type(self, value):
raise ValueError(f"Unsupported field type: {self.model.field_type}")


class SegmentRangeValidationHelper(RangeValidationHelper):
"""Helper class for validating segment models."""

def __init__(self, model: type[models.Model]) -> None:
"""Initialize the helper with the model."""
super().__init__(model, "segment_range_field_name", "segment_range", IncorrectSegmentRangeError)

def get_validated_segment_range_field(self) -> models.Field:
"""Return the validated segment range field."""
return self.get_validated_range_field()


class SegmentSpanValidationHelper:
"""Helper class for validating that models have valid foreign key to a model that inherits from AbstractSpan.
Expand All @@ -209,7 +226,15 @@ def __init__(self, model: type[models.Model]) -> None:
"""Initialize the helper with the model."""
self.model = model
self.segment_span_field_name = getattr(self.model, "segment_span_field_name", None)
self.segment_span = getattr(self.model, "segment_span", None)

segment_span_deferred_attr = getattr(self.model, "segment_span", None)
self.segment_span = getattr(segment_span_deferred_attr, "field", None) if segment_span_deferred_attr else None
logger.debug(
"SegmentSpanValidationHelper __init__(): %s, %s, %s",
self.model,
self.segment_span_field_name,
self.segment_span,
)

def get_validated_segment_span_field(self) -> models.Field:
"""Return the validated segment span field."""
Expand Down Expand Up @@ -239,3 +264,92 @@ def get_segment_span_field_instance(self) -> models.Field:
raise ImproperlyConfigured(
f"segment_span_field_name '{self.segment_span_field_name}' does not exist on {self.model.__name__}"
) from e


class AbstractSpanMetaclass(ModelBase): # pylint: disable=R0903
"""Metaclass for AbstractSpan."""

def __new__(cls, name, bases, attrs, **kwargs):
"""Validates subclass of AbstractSpan & sets initial_range_field and current_range_field for the model."""
logger.debug("Creating new span model: %s", name)

model = super().__new__(cls, name, bases, attrs, **kwargs) # pylint: disable=E1121

for base in bases:
if base.__name__ == "AbstractSpan":
# Ensure that the model is not abstract
concrete_validation_helper = ConcreteModelValidationHelper(model)
concrete_validation_helper.check_model_is_concrete()

# Ensure that the initial_range field is valid
span_initial_range_validation_helper = SpanInitialRangeValidationHelper(model)
initial_range_field = span_initial_range_validation_helper.get_validated_initial_range_field()
model.initial_range_field = initial_range_field

# Ensure that the current_range field is valid
span_current_range_validation_helper = SpanCurrentRangeValidationHelper(model)
current_range_field = span_current_range_validation_helper.get_validated_current_range_field()
model.current_range_field = current_range_field

# Ensure that the initial_range field and current_range field have the same type
logger.debug(
"AbstractSpanMetaclass Passing to RangeTypesMatchHelper: %s, %s",
type(initial_range_field),
type(current_range_field),
)
range_types_match_helper = RangeTypesMatchHelper(initial_range_field, current_range_field)
range_types_match_helper.validate_range_types_match()

return model


class AbstractSegmentMetaclass(ModelBase): # pylint: disable=R0903
"""Metaclass for AbstractSegment."""

def __new__(cls, name, bases, attrs, **kwargs):
"""Validates subclass of AbstractSegment and sets segment_range_field for the concrete model."""
logger.debug("Creating new segment model: %s", name)

model = super().__new__(cls, name, bases, attrs, **kwargs) # pylint: disable=E1121

def late_binding(sender, **kwargs): # pylint: disable=W0613
"""Late binding to ensure that the segment_range_field is set after the model is prepared.
If we try to access the segment_range_field in the related Span model before the models are prepared,
we will get an AttributeError.
"""
if sender is model:
for base in bases:
if base.__name__ == "AbstractSegment":
# Ensure that the model is not abstract
concrete_validation_helper = ConcreteModelValidationHelper(model)
concrete_validation_helper.check_model_is_concrete()

# Ensure that the segment_range field is valid
segment_validation_helper = SegmentRangeValidationHelper(model)
segment_range_field = segment_validation_helper.get_validated_segment_range_field()
model.segment_range_field = segment_range_field # pylint: disable=W0201

# Ensure that the segment_span field is valid
segment_span_validation_helper = SegmentSpanValidationHelper(model)
segment_span_field = segment_span_validation_helper.get_validated_segment_span_field()
model.segment_span_field = segment_span_field # pylint: disable=W0201

# Ensure that the segment_range field and span's initial_range field have the same type
related_model = segment_span_field.related_model
segment_span_initial_range_field = getattr(related_model, "initial_range_field", None)

if not segment_span_initial_range_field:
raise ImproperlyConfigured(
f"{related_model.__name__} must have an 'initial_range_field' attribute"
)

range_types_match_helper = RangeTypesMatchHelper(
segment_range_field,
segment_span_initial_range_field,
)
range_types_match_helper.validate_range_types_match()

class_prepared.connect(late_binding, sender=model)

return model
56 changes: 3 additions & 53 deletions src/django_segments/models/segment.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import logging

from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.translation import gettext as _

from django_segments.app_settings import ON_DELETE_FOR_PREVIOUS
from django_segments.app_settings import SEGMENT_MODEL_BASE as ModelBase
from django_segments.base import BoundaryHelper
from django_segments.base import ConcreteModelValidationHelper
from django_segments.base import RangeTypesMatchHelper
from django_segments.exceptions import IncorrectSegmentRangeError
from django_segments.exceptions import IncorrectSubclassError
from django_segments.exceptions import SegmentRangeValidationHelper
from django_segments.exceptions import SegmentSpanValidationHelper
from django_segments.models.base import AbstractSegmentMetaclass
from django_segments.models.base import BoundaryHelper


logger = logging.getLogger(__name__)


class AbstractSegment(ModelBase):
class AbstractSegment(models.Model, metaclass=AbstractSegmentMetaclass):
"""Abstract class from which all segment models should inherit.
All concrete subclasses of AbstractSegment must define either `segment_range_field_name` (a string representing the
Expand Down Expand Up @@ -69,49 +62,6 @@ class Meta: # pylint: disable=C0115 disable=R0903
models.Index(fields=["deleted_at"]),
]

def __new__(cls, name, bases, attrs, **kwargs):
"""Validates subclass of AbstractSegment and sets segment_range_field for the concrete model."""
try:
model = super().__new__(cls, name, bases, attrs, **kwargs) # pylint: disable=E1121

for base in bases:
if base.__name__ == "AbstractSegment":
# Ensure that the model is not abstract
concrete_validation_helper = ConcreteModelValidationHelper(model)
concrete_validation_helper.check_model_is_concrete()

# Ensure that the segment_range field is valid
segment_validation_helper = SegmentRangeValidationHelper(model)
segment_range_field = segment_validation_helper.get_validated_segment_range_field()
model.segment_range_field = segment_range_field

# Ensure that the segment_span field is valid
segment_span_validation_helper = SegmentSpanValidationHelper(model)
segment_span_field = segment_span_validation_helper.get_validated_segment_span_field()
model.segment_span_field = segment_span_field

# Ensure that the segment_range field and span's initial_range field have the same type
segment_span_initial_range_field = getattr(segment_span_field, "initial_range", None)
range_types_match_helper = RangeTypesMatchHelper(
segment_range_field,
segment_span_initial_range_field,
)
range_types_match_helper.validate_range_types_match()

return model
except IncorrectSubclassError as e:
logger.error("Incorrect subclass usage in %s: %s", name, str(e))
raise e
except IncorrectSegmentRangeError as e:
logger.error("Incorrect segment usage in %s: %s", name, str(e))
raise e
except ImproperlyConfigured as e:
logger.error("Improperly configured in %s: %s", name, str(e))
raise e
except Exception as e:
logger.error("Error in %s: %s", name, str(e))
raise e

def set_lower_boundary(self, value):
"""Set the lower boundary of the range field."""
boundary_helper = BoundaryHelper(
Expand Down
48 changes: 3 additions & 45 deletions src/django_segments/models/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,16 @@
from django.utils.translation import gettext as _

from django_segments.app_settings import ALLOW_GAPS
from django_segments.app_settings import SEGMENT_MODEL_BASE as ModelBase
from django_segments.app_settings import SOFT_DELETE
from django_segments.app_settings import STICKY_BOUNDARIES
from django_segments.base import BoundaryHelper
from django_segments.base import ConcreteModelValidationHelper
from django_segments.base import RangeTypesMatchHelper
from django_segments.base import SpanCurrentRangeValidationHelper
from django_segments.base import SpanInitialRangeValidationHelper
from django_segments.exceptions import IncorrectSpanRangeError
from django_segments.exceptions import IncorrectSubclassError
from django_segments.models.base import AbstractSegmentMetaclass
from django_segments.models.base import BoundaryHelper


logger = logging.getLogger(__name__)


class AbstractSpan(ModelBase):
class AbstractSpan(models.Model, metaclass=AbstractSegmentMetaclass):
"""Abstract class from which all span models should inherit.
All concrete subclasses of AbstractSpan must define either `initial_range_field_name` (a string representing the
Expand Down Expand Up @@ -71,42 +65,6 @@ def get_config_dict(self) -> dict[str, bool]:
"soft_delete": self.Config.soft_delete,
}

def __new__(cls, name, bases, attrs, **kwargs):
"""Validates subclass of AbstractSpan & sets initial_range_field and current_range_field for the model."""
try:
model = super().__new__(cls, name, bases, attrs, **kwargs) # pylint: disable=E1121

for base in bases:
if base.__name__ == "AbstractSpan":
# Ensure that the model is not abstract
concrete_validation_helper = ConcreteModelValidationHelper(model)
concrete_validation_helper.check_model_is_concrete()

# Ensure that the initial_range field is valid
span_initial_range_validation_helper = SpanInitialRangeValidationHelper(model)
initial_range_field = span_initial_range_validation_helper.get_validated_initial_range_field()
model.initial_range_field = initial_range_field

# Ensure that the current_range field is valid
span_current_range_validation_helper = SpanCurrentRangeValidationHelper(model)
current_range_field = span_current_range_validation_helper.get_validated_current_range_field()
model.current_range_field = current_range_field

# Ensure that the initial_range field and current_range field have the same type
range_types_match_helper = RangeTypesMatchHelper(initial_range_field, current_range_field)
range_types_match_helper.validate_range_types_match()

return model
except IncorrectSubclassError as e:
logger.error("Incorrect subclass usage in %s: %s", name, str(e))
raise e
except IncorrectSpanRangeError as e:
logger.error("Incorrect span usage in %s: %s", name, str(e))
raise e
except Exception as e:
logger.error("Error in %s: %s", name, str(e))
raise e

def get_segment_class(self):
"""Get the segment class from the instance, useful when creating new segments dynamically.
Expand Down

0 comments on commit fc84af0

Please sign in to comment.