RegisterFields in Django

A common (in my opinion) complex pattern in Django is storing a field that identifies what logic should be used. This tends to happen when you have logic that’s similar about how it’s called and what it does, but how they function is significantly different.

An example of this may be a notification system where it can send an SMS or an email. They both require message content and a recipient, but the mechanisms for the actual sending along with generating the complete payload are dramatically different. With Django Cairn this pattern may be necessary to fetch content from different sources. A blog with an RSS feed has a different API than a YouTube channel, but both have lists of content and some way to find the new content. This post seeks to explain this concept and provide some sample code to use in your project.

In an ideal world, we would have a model field that when accessed returns an instance of a custom class1.

from django.db import models


class SourceBackend:
    """This is an interface for defining how each Source backend should work"""
    key: str

    def __init__(self, source: Source):
        self.source = source

    def update(self):
        """
        Every subclass should define an update class.

        An abstract base class is probably the better choice here.
        """
        raise NotImplementedError


class RSSBackend(SourceBackend):
    key = "rss"

    def update(self):
        # Do something based on source.url and RSS
        pass

class YouTubeBackend(SourceBackend):
    key = "youtube"

    def update(self):
        # Do something based on source.url and YouTube's API
        pass

# Define a mapping to allow look ups for each backend
backend_register = Register()
backend_register[RSSBackend.key] = RSSBackend
backend_register[YouTubeBackend.key] = YouTubeBackend

class Source(models.Model):
    """This is an example model for Django Cairn for fetching data"""
    url = models.URLField()
    backend_key, backend = RegisterField.with_property(register=backend_register, max_length=64)

    
# This is what we're hoping to achieve:
source = Source.objects.first()
assert source.backend_key == "rss"
assert isinstance(source.backend, RSSBackend)
source.backend.update()

You probably noticed the RegisterField class that’s not defined. Good catch. That’s what we’ll be talking about for the most part, but let me finish setting up our scenario.

The class SourceBackend is an interface that defines how it should be called. It’s fairly straightforward only containing the __init__ and update methods. Effectively, as long as you can get it a Source instance the backend will be able to fetch the data for it. Since each content source could require an entirely different set of models to store a new post/video/[content], update needs to stay generic. The key attribute will eventually be used to identify this class in the database and go into backend_key. We’ll talk about it more later though, but you may already see how everything will come together.

The RSSBackend through the magic of handwaving is super simple. I didn’t define the update method here because it’s irrelevant to what we’re doing. We can make the assumption it’ll do whatever it’s supposed to do when it’s called. You can see that RSSBackend.key is set to "rss" and we’re using inheritance to define the __init__ method.

There is another custom class called Register. This is effectively a dictionary with a helper function. I’ll share the code for that later. For now, we add our backend classes to a specific instance of Register and pass that to RegisterField.

This brings us to the sample code at the end of the block that demos what we’re hoping to achieve. The goal is to have a Source instance that has a field called backend_key whose value will match the relevant SourceBackend subclass’ key attribute. In other words, if isinstance(source.backend, RSSBackend) then source.backend_key == "rss". Hopefully, you can see the power of this approach. The class SourceBackend could define many methods rather than just two. You also avoid having to litter your project with various hook mappings to access the functionality you want.

Implementation details

Let’s talk about Register and RegisterField. These are both utility classes that go hand-in-hand. Register is straightforward; it has a single helper function from_value:

class Register(dict):
    def from_value(self, value):
        for key, v in self.items():
            if v == value:
                return key
        raise ValueError("Value not found: %r" % value)

If you have a large amout of elements you may want to consider changing Register’s implementation to inherit from collections.UserDict and maintain a separate value to pair data structure. However, if you are going to have less than 100 elements, the above is fine.

The concept of a register is fairly common. It’s a lookup to determine what value (in our case functionality) to use based on a key. The register needs to be populated at the project level. You should not add items to the register dynamically.

Let’s assume this is our project structure.

myproject
├── config
│   ├── settings.py
│   ├── urls.py
│   ├── asgi.py
│   └── wsgi.py
│
├── content
│   ├── backends
│   │   ├── rss.py      # RSSBackend is defined here
│   │   └── youtube.py  # YouTubeBackend is defined here
│   │
│   ├── apps.py
│   ├── backend.py      # SourceBackend, backend_register are defined here
│   ├── register.py     # Register, RegisterField are defined here
│   ├── models.py
│   ├── views.py
│   └── urls.css
│
├── README.md
└── manage.py

The challenge now becomes how to register the various source backends into the backend_register instance. We could define a decorator similar to how the Django administration site can register ModelAdmin classes or how signal receivers are defined. However, let’s start by keeping it simple and registering the backends in myproject.content.apps.ContentAppConfig.ready():

# myproject/content/backend.py

from .register import Register


backend_register: [str, SourceBackend] = Register()

class SourceBackend:
    """This is an interface for defining how each Source backend should work"""
    key: str

    def __init__(self, source):
        self.source = source

    def update(self):
        """
        Every subclass should define an update class.

        An abstract base class is probably the better choice here.
        """
        raise NotImplementedError
# myproject/content/apps.py

from .backend import backend_register
from .backends.rss import RSSBackend
from .backends.youtube import YouTubeBackend


class ContentAppConfig(AppConfig):
    name = "myproject.content"

    def ready(self):
        backend_register.update({
            RSSBackend.key: RSSBackend,
            YouTubeBackend.key: YouTubeBackend,
        })

Cool, our backend_register now will contain records for each of our backend integrations whenever we access it.

RegisterField explanation

The next step is to use this register in a field class to be used on a model. Here’s all the code. The explanations are in the comments. My hope is that everything is clear enough except for with_property. If something is confusing, let me know.

# myproject/content/register.py

from django.db import models


class Register(dict):
    ...


class RegisterField(models.CharField):
    """Simple key-value serialization and storage in the database."""

    # A RegisterField must have an option selected. If you wish to have a
    # no-op option, then you'll need to create a sentinel value.
    empty_strings_allowed = False

    def __init__(self, /, *args, register: Register, **kwargs):
        # Require a new keyword argument named register.
        # Store it on the instance for later use.
        self.register = register
        super().__init__(*args, **kwargs)

    @classmethod
    def with_property(cls, *args, register: Register, **kwargs):
        """
        Construct the field and create a wrapping property.
        
        This allows us to define the register key as the true CharField
        but also provide the register value as a property on the model.
        
        There may be another way to do this, but I found this works well
        enough.
        
        Usage:
        
            class MyModel(models.Model):
                key, instance = RegisterField.with_property(register=SomeRegister, max_length=64)

        """
        field = cls(*args, register=register, **kwargs)

        @property
        def prop(self):
            key = getattr(self, field.name)
            return register[key] if key else None

        @prop.setter
        def prop(self, value):
            setattr(self, field.name, register.from_value(value))

        return field, prop

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        # Remove dynamic choices to avoid constantly generating
        # new migrations.
        kwargs.pop("choices", None)
        return name, path, args, kwargs

    def formfield(self, **kwargs):
        # Get the choices immediately before getting the form field
        # This allows the dynamic registration of elements in the register.
        self.choices = self.register.items()
        return super().formfield(**kwargs)

Assuming the __init__, deconstruct and formfield all make sense, let’s move onto with_property. This class method is designed to replace the typical usage of my_field = models.CharField(). The end result is adding a register key field and a register value property to your model instance.

For example, if we have:


class Source(models.Model):
    backend_key, backend = RegisterField.with_property(register=backend_register, max_length=64)

When we fetch a Source instance, we’ll be able to get both the register key (“rss” or “youtube”) as instance.backend_key and the SourceBackend instance as instance.backend.

Expressed in code the following would be true:


instance = Source.objects.first()
assert instance.backend_key in ["rss", "youtube"]
assert (
    isinstance(instance.backend, RSSBackend)
    or isinstance(instance.backend, YouTubeBackend)
)

The really neat thing about this is that if we want to change the backend of an instance we can do so through either backend_key or backend:


instance.backend_key = "rss"
instance.backend = RSSBackend()

This is because of the inline prop. The value when you access it is looked up from the register each time. This implies that if you change backend_key behind it, it will still return the SourceBackend instance reflecting that change. Then because we have:


@prop.setter
def prop(self, value):
    setattr(self, field.name, register.from_value(value))

We can change backend_key by assigning a different SourceBackend instance to instance.backend. It will look up the key from the given SourceBackend instance and set backend_key to that key.

So what does this all mean?

In short, it means we can do this:

source = Source.objects.first()
source.backend.update()
  1. A big credit to Ryan Hiebert who implemented logic that I iterated on to get to this version.