Using descriptors as model attributes for custom field types?

Hi, I have a custom model field class whose Python representation is an instance of a custom Python class (sort of like the HandField example in the docs here). The custom class used by the custom field type really only exists to keep track of some internal state - it’s a wrapper for values of a type that users actually care about.

Right now, the way users manipulate the model attributes associated with the custom field is via a property getter/setter on the instance of the custom class, like so:

class ThingUserCaresAbout():
    def __init__(self, x: int) -> None:
        self.x = x

class ValueWrapperUserDoesntCareAbout:
    def __init__(self, initial_value: Optional[ThingUserCaresAbout] = None) -> None:
        self._value = initial_value
        # also does some internal setup stuff

    @property
    def value(self) -> Optional[ThingUserCaresAbout]:
        # do some logic related to the state here
        return self._value

    @value.setter
    def set_value(self, new_value: Optional[ThingUserCaresAbout]) -> None:
        # do some logic related to the state here
        self._value = new_value

The custom field and interaction with the model looks something like this:

class SomeCustomField(Field):
    # (Implementation of to_python, from_db_value, get_db_prep_value, etc.
    # to serialize and deserialize DB and Python state into and out of
    # ValueWrapperUserDoesntCareAbout objects that wrap ThingUserCaresAbout objects)

class ExampleModel(Model):
    something = SomeCustomField()

example_instance = ExampleModel()
example_instance.something  # => ValueWrapperUserDoesntCareAbout object
example_instance.something.value  # => None
example_instance.something.value = ThingUserCaresAbout(5)

I don’t like the way that that this exposes the ValueWrapperUserDoesntCareAbout object and requires the use of the value property getter and setter.

I’m wondering if it’s possible to have the custom model field to use Python’s data descriptors as the model attributes, instead of exposing this ValueWrapperUserDoesntCareAbout class. I’m thinking something like:

  • The state that’s currently stored in ValueWrapperUserDoesntCareAbout could be stored in a _some_custom_field_data dict on the model instance
    • Keys would be the field names of the SomeCustomField fields on the model, and values would contain the state that’s currently stored in the ValueWrapperUserDoesntCareAbout class
  • The model attributes themselves (like the something attribute of an ExampleModel instance above) would be instances of a new descriptor class
    • This descriptor would have __get__ and __set__ methods that read or manipulate the state of the _some_custom_field_data dict on the model instance and return or store a ThingUserCaresAbout instance.

Based on what I’ve seen of contribute_to_class I think it should be possible to at least set up the descriptors and the data storage, but this is deeper in the weeds of the ORM internals than I’m used to, and it’s certainly not part of the publicly documented Django custom field API. I haven’t found any third-party examples that do anything like this, either.

So, before I pursue this further:

  • Has anyone here done something like this before? Are there existing custom field types that do this kind of thing that I can refer to?
  • Is there existing Django functionality that this could break?
  • When Django calls methods on the custom field class such as get_db_prep_value, will it pass the value returned by the descriptor’s __get__, or will it somehow pass the descriptor instance itself?
  • Same question for from_db_value and analogous custom field methods - when Django sets the model instance attribute to the deserialized value from the database, will it call the descriptor’s __set__, or will it do something else?