Package: fallback-property
RequirementsPython 3.6+What is it?fallback_propertytransforms a function into a property and uses the
decorated function as fallback if no value was assigned to the property itself.
A special descriptor (fallback_property.FallbackDescriptor)
is used internally.Django (or similar frameworks)fallback_propertyis useful if you have a function that aggregates
values from related objects, which could already be fetched using an annotated
queryset.
The decorator will favor the precalculated value over calling the actual method.It is especially helpful, if you optimize your application and want to
replace legacy or performance critical properties with precalulated values
using.annotate().How to use it?Simply define a function and use the decoratorfallback_propertyfrom fallback_property import fallback_property

class Foo:

    @fallback_property()
    def fallback_func(self):
        return 7ArgumentsThefallback_property()has two optional arguments.cached: bool = TrueIf the property is accessed multiple times, call the fallback function only once.logging: bool = FalseLog a warning if there was a fallback to the decorated, original method.Usage Example (Django)Suppose we have the following modelsfrom django.db import models


class Pipeline(model.Model):
    @property
    def total_length(self):
        return sum(self.parts.values_list('length', flat=True))


class Parts(models.Model):
    length = models.PositiveIntegerField()
    pipeline = models.ForeignKey(Pipeline, related_name='parts')Callingpipline.total_lengthwill always trigger another query and is
even more expensive when dealing with multiple objects. This can be
optimized by using.annotate()andfallback_property()from django.db import models, QuerySet
from django.db.functions import Coalesce
from django.db.models import Sum
from fallback_property import fallback_property


class PipelineQuerySet(QuerySet):

    def with_total_length(self):
        return self.annotate(
            total_length=Coalesce(
                Sum('parts__length', output_field=models.IntegerField()),
                0,
            )
        )


class Pipeline(model.Model):

    @fallback_property(logging=True)
    def total_length(self):
        return sum(self.parts.values_list('length', flat=True))You can now access thetotal_lengthwithout triggering another query or
get a warning, when the fallback function is usedpipeline = Pipeline.objects.with_total_length().first()
print(pipeline.total_length)Important: The annotated value and the property must have the same name.Related objectsWhen dealing with related objects in Django be aware that the ORM imposes certain limitations:In the following example one might expect to get an instance ofUser, but instead the
value of the primary key is returned:from django.db import models, QuerySet
from django.db.functions import Coalesce
from django.db.models import F
from fallback_property import fallback_property


class PartQuerySet(QuerySet):

    def with_owner(self):
        return self.annotate(
            owner=Coalesce(
                F('_owner'),
                F('pipeline__owner'),
                None,
            )
        )


class Pipeline(model.Model):
    owner = models.ForeignKey(User)


class Parts(models.Model):
    _owner = models.ForeignKey(User, blank=True, null=True, on_delete=models.SET_NULL)
    length = models.PositiveIntegerField()
    pipeline = models.ForeignKey(Pipeline, related_name='parts')

    objects = PartQuerySet()

    @fallback_property()
    def owner(self):
        return self._owner or self.pipline.owner


>>> print(Part.objects.with_owner().first().owner)
>>> 1DevelopmentThis project is usingpoetryto manage all
dev dependencies.Clone this repository and runpoetry install
poetry run pip install djangoto create a virtual environment with all dependencies.You can now run the test suite usingpoetry run pytestThis repository follows theangular commit conventions.
You can register a pre-commit hook to validate your commit messages by usinghusky. The configurations are already in place if
you have nodejs installed. Just runnpm installand the pre-commit hook will be registered.