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 newdeconstruct()
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.
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))
- [Answered ]-Instantiate object "at module level" in Django
- [Answered ]-Maintaning isolation between modules in Django monolith
- [Answered ]-Json recognised as String
- [Answered ]-How does Cygwin work for python programming?