Extending Custom Model Field - Couldn't reconstruct field: multiple values for "verbose_name"

Hey group! I fear this is a basic python issue that I can’t seem to solve. But, posting here in Django, since it relates to a model field and attribute “verbose_name”, in hopes for faster resolution.

I am using Django-Pint. It basically uses Pint libraries and a Float Field in Models. All I did, was override it’s init() in order to format the precision of the data.

The custom field I’m extending is QuantityField:

class QuantityField(QuantityFieldMixin, models.FloatField):
    form_field_class = QuantityFormField
    to_number_type = float

And the parent class init() that I’m overriding:

class QuantityFieldMixin(object):
    to_number_type: Callable[[Any], NUMBER_TYPE]

    # TODO: Move these stuff into an Protocol or anything
    #       better defining a Mixin
    value_from_object: Callable[[Any], Any]
    name: str
    validate: Callable
    run_validators: Callable

    """A Django Model Field that resolves to a pint Quantity object"""

    def __init__(
        self,
        base_units: str,
        *args,
        unit_choices: Optional[typing.Iterable[str]] = None,
        **kwargs,
    ):

My code in models.py is pretty basic. I’m just taking an object inside the init and setting a format attribute. I’m creating/setting a named argument “precision”

class PrecisionQuantityField(QuantityField):
    def __init__(self, base_units, *args, unit_choices, precision='.2f', **kwargs):
        super(PrecisionQuantityField,self).__init__(base_units, *args, unit_choices, **kwargs)
        self.ureg.default_format = precision

And the field I’m applying it to:

class RecipeItem(models.Model):

    class Meta:
        abstract = True #We don't want our own table.  Inherit these fiels

    amount_weight = PrecisionQuantityField('kilograms', null=True, blank=True, unit_choices=['lb', 'gram', 'oz', 'milligram', 'kilogram'])
    amount_volume = PrecisionQuantityField('liters', null=True, blank=True, unit_choices=['floz', 'ml', 'gallon', 'liter'])

    @property
    def getAmount(self):
        if self.amount_weight is not None:
            return self.amount_weight
        return self.amount_volume


class RecipeFermentable(RecipeItem):

    recipe_notes = models.CharField(max_length=200, null=True, blank=True)
    is_fermentable = models.BooleanField(null=True, blank=True)
    fermentable = models.ForeignKey(Fermentable, on_delete=models.RESTRICT)
    intended_use = models.ForeignKey(AdjunctUsage, on_delete=models.CASCADE)
    recipe = models.ManyToManyField(Recipe, related_name='fermentables')

    @property
    def display_name(self):
        if self.fermentable.supplier:
            return self.fermentable.supplier + ": " + self.fermentable.name
        else:
            return self.fermentable.name

    def __str__(self):
        return self.display_name

The error I’m getting when “makemigrations” is:

TypeError: Couldn't reconstruct field amount_weight on batchthis.RecipeFermentable: __init__() got multiple values for argument 'verbose_name'

RecipeFermentable inherits from the abstract class RecipeItem, which holds the field “amount_weight” using my overriding class. It works fine without my override.

Does anyone know where I went wrong? I made sure I’m not duplicating values in my super().init() call. Or, at least, I’m pretty darn sure.

Thanks!

What is the complete QuantityFieldMixin class? (At a minimum, we need to see the complete __init__ function.)

Fair point. I figured I was making some bad call with my parameters.

class QuantityFieldMixin(object):
    to_number_type: Callable[[Any], NUMBER_TYPE]

    # TODO: Move these stuff into an Protocol or anything
    #       better defining a Mixin
    value_from_object: Callable[[Any], Any]
    name: str
    validate: Callable
    run_validators: Callable

    """A Django Model Field that resolves to a pint Quantity object"""

    def __init__(
        self,
        base_units: str,
        *args,
        unit_choices: Optional[typing.Iterable[str]] = None,
        **kwargs,
    ):
        """
        Create a Quantity field
        :param base_units: Unit description of base unit
        :param unit_choices: If given the possible unit choices with the same
                             dimension like the base_unit
        """
        if not isinstance(base_units, str):
            raise ValueError(
                'QuantityField must be defined with base units, eg: "gram"'
            )

        self.ureg = ureg

        # we do this as a way of raising an exception if some crazy unit was supplied.
        unit = getattr(self.ureg, base_units)  # noqa: F841

        # if we've not hit an exception here, we should be all good
        self.base_units = base_units

        if unit_choices is None:
            self.unit_choices: List[str] = [self.base_units]
        else:
            self.unit_choices = list(unit_choices)
            # The multi widget expects that the base unit is always present as unit
            # choice.
            # Otherwise we would need to handle special cases for no good reason.
            if self.base_units in self.unit_choices:
                self.unit_choices.remove(self.base_units)
            # Base unit has to be the first choice, always as all values are saved as
            # base unit within the database and this would be the first unit shown
            # in the widget
            self.unit_choices = [self.base_units, *self.unit_choices]

        # Check if all unit_choices are valid
        check_matching_unit_dimension(self.ureg, self.base_units, self.unit_choices)

        super(QuantityFieldMixin, self).__init__(*args, **kwargs)

Full code found here: https://github.com/CarliJoy/django-pint/blob/main/src/quantityfield/fields.py

I suspect the problem is in the deconstruct method. The args value returned may contain a positional argument, which is then used as the verbose argument of field (since it is the first argument of Field.__init__(), but because kwargs value also contains a verbose entry, this leads to the duplicate.

I supect the positional argument to be the base_unit used in fields declaration (which is declared as positional argument), but I do not know why it would be returned in the args value of your call to super_deconstruct.

Maybe you could add a print of the values returned by super_deconstruct call to confirm/infirm my assuptions

I don’t think it’s related to the issue but, according to the documentation (How to create custom model fields | Django documentation | Django), deconstruct shouldn’t return the unit_choices in kwargs if it’s equal to the default value (i.e. if it’s equal to None)

Interesting. The code works perfectly fine without my override. No issues or errors. But, when I create my override “PrecisionQuantityField”, the error happens.

It looks like you’re right. The value “verbose_name” in kwargs is getting assigned the ‘base_units’ value during the return of deconstruct().

It’s strange that my super() is doing that, but take out my child class, and it works fine.

> /Users/aaronpaxson/PycharmProjects/stonesrivermeadery-webroot/venv/lib/python3.9/site-packages/quantityfield/fields.py(121)deconstruct()
-> return name, path, args, kwargs
(Pdb) name
'amount_weight'
(Pdb) path
'batchthis.models.PrecisionQuantityField'
(Pdb) args
self = <batchthis.models.PrecisionQuantityField: amount_weight>
(Pdb) kwargs
{'verbose_name': ['lb', 'gram', 'oz', 'milligram', 'kilogram'], 'blank': True, 'null': True, 'base_units': 'kilograms', 'unit_choices': ['kilograms']}
(Pdb) 

And, from super_deconstruct():

(Pdb) super_deconstruct()
('amount_weight', 'batchthis.models.PrecisionQuantityField', [], {'verbose_name': ['lb', 'gram', 'oz', 'milligram', 'kilogram'], 'blank': True, 'null': True})

I suppose I can override deconstruct() in my class, and delete the ‘verbose’ key. Feels kinda like duct-tape flimsy. What’s the better way to make my super().init() call? I’m basically just using the exact same parameters as the parent class.

I, of course, can read up more on the custom model fields. I just am trying to determine why my call fails, but it works without it.

FYI, here is my final code. I’d love to know how to solve without deleting the ‘verbose_name’ key. But working nonetheless. Thanks for the guidance @antoinehumbert !

class PrecisionQuantityField(QuantityField):
    def __init__(self, base_units, *args, unit_choices, precision='.2f', **kwargs):
        super(PrecisionQuantityField,self).__init__(base_units, *args, unit_choices, **kwargs)
        self.ureg.default_format = precision

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        kwargs['precision'] = self.ureg.default_format
        del kwargs['verbose_name'] #FIXME - Getting duplicate value.  Forcing removal
        return name, path, args, kwargs

That’s actually an appropriate solution.

There are multiple cases where you want to add parameters to the constructor of an object, but that you must remove those parameters before calling super. I’m most familiar with this in forms. We have many forms requiring additional parameters that are set at the class instance level during __init__, but are removed from kwargs before calling super().__init__(...)

(Note, I have not read this issue in enough detail to know if there’s a different way to handle this - only to say that deleting elements from kwargs is not an unusual situation.)

I think I got the reason why it does not work. When calling super() here,

The unit_choices is passed as a positional argument, so it is resolved in the *args of the mixin, not as the unit_choices argument (which is a keyword only argument). Hence, the first positional args finishes in verbose_name and unit_choices is not set on your instances.

Finally, calling super() the following way should solve the initial issue:

super(PrecisionQuantityField,self).__init__(base_units, *args, unit_choices=unit_choices, **kwargs)

As a side note, the PrecisionQuantityField should still implement deconstruct method to handle the precision parameter

I did include the deconstruct() in my child class and handling the kwarg “precision”.

Thanks for the help! This was exactly what I needed!