[Answered ]-In a django custom field, is it invalid to give the constructor a class-type input parameter?

1👍

TL;DR

Use this code snippet to make it work –

class FooManagedField(models.CharField):
    def __init__(self, foo: Foo, *args, **kwargs) -> None:
        self.foo: Foo = foo
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs["foo"] = "Foo"  # set a static value
        return name, path, args, kwargs


Long Answer

From the doc

The counterpoint to writing your __init__() method is writing the
deconstruct() method. It’s used during model migrations to tell Django
how to take an instance of your new field and reduce it to a
serialized form – in particular, what arguments to pass to __init__()
to recreate it.

If you haven’t added any extra options on top of the field you
inherited from, then there’s no need to write a new deconstruct()
method. If, however, you’re changing the arguments passed in
__init__() (like we are in HandField), you’ll need to supplement the values being passed.

Simply put, deconstruct() determines whether Django needs to generate a new migration file whenever the makemigrations command is called.

Here, the foo argument doesn’t take part in the DB migration, and hence, we don’t need to do anything to the deconstruct(...) method. In other words, the following example is enough

class FooManagedField(models.CharField):

    def __init__(self, foo: Foo, *args, **kwargs) -> None:
        self.foo: Foo = foo
        super().__init__(*args, **kwargs)

But, we will get the following error,

TypeError: Couldn’t reconstruct field foo1 on polls.MyModel: init() missing 1 required positional argument: ‘foo’

This is because we defined the foo argument as mandatory/required; and thus we need to supply some value for the same from the deconstruct(...)

Now, we need to override the deconstruct(...) method by providing value for foo to generate migration file –

class FooManagedField(models.CharField):

    def __init__(self, foo: Foo, *args, **kwargs) -> None:
        self.foo: Foo = foo
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs["foo"] = self.foo # Note that `self.foo` is a class object
        return name, path, args, kwargs

Unfortunately, this will again raise an error –

ValueError: Cannot serialize: Foo(1)

and this is because the Foo object is a custom class and Django doesn’t know how to serialize them.

Setting the value with a built-in type (int, str, float, etc) will solve the issue.

class FooManagedField(models.CharField):

    def __init__(self, foo: Foo, *args, **kwargs) -> None:
        self.foo: Foo = foo
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs["foo"] = str(self.foo)
        return name, path, args, kwargs

But, this also has some issues, which is whenever the value of self.foo changes, a new migration file will be generated (not automatically, but upon the makemigrations command).

So, we are setting the value to a static value inside deconstruct(...) method –

class FooManagedField(models.CharField):
    def __init__(self, foo: Foo, *args, **kwargs) -> None:
        self.foo: Foo = foo
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs["foo"] = "Foo"  # set a static value
        return name, path, args, kwargs


Note: As per the documentation, the non_db_attrs attribute should work, but it doesn’t. I’ll make some tests and raise a ticket if required.

👤JPG

0👍

To answer your main question: The limitation you’re encountering in Django is not a hard limitation in the sense that you cannot provide a class as a default value for a field. However, it’s a limitation in the way Django’s migration system handles default values.

If you want to keep the default_foo function as it is, you can modify your model field definition to use a lambda function as the default value to provide the necessary arguments to default_foo.

class FooField(CharField):
    def __init__(self, *args, **kwargs) -> None:
        default = kwargs.get("default", default_foo())
        kwargs["default"] = default_foo_callable(default)
        super().__init__(*args, **kwargs)
    
    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        if kwargs.get("default") == default_foo_callable():
            del kwargs["default"]
        return name, path, args, kwargs

def default_foo_callable(value=0):
    return lambda: default_foo(value)

def default_foo(n):
    return Foo(n)

Modify your model definition to use the callable for the default value:

class MyModel(models.Model):
    n1 = models.IntegerField()
    n2 = models.IntegerField()
    foo1 = FooField(default=default_foo_callable(1))
    foo2 = FooField(default=default_foo_callable(2))

Leave a comment