Opened 2 years ago

Closed 2 years ago

#33313 closed Bug (fixed)

Inheriting from multiple abstract models with same field causes name collision when overriding field is direct parent

Reported by: Ben Nace Owned by: nobody
Component: Database layer (models, ORM) Version: 3.2
Severity: Normal Keywords:
Cc: Jarek Glowacki, Carlton Gibson Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Given the following example models:

class ModelActivation(models.Model):
    start_date = models.DateField(null=True, blank=True)
    end_date = models.DateField(null=True, blank=True)
    active = models.BooleanField()

    class Meta:
        abstract = True

class BaseData(ModelActivation):
    entity_state = models.CharField(max_length=100)

    class Meta:
        abstract = True

class RequiredStart(models.Model):
    start_date = models.DateField()

    class Meta:
        abstract = True

class RequiredEnd(models.Model):
    end_date = models.DateField()

    class Meta:
        abstract = True

class RequiredStartEnd(RequiredStart, RequiredEnd):
    class Meta:
        abstract = True


Any of the following when the override for start_date is defined on a direct parent model, results in the error "(models.E006) The field 'start_date' clashes with the field 'start_date' from model 'app.testmodel' (or 'app.testmodel2')"

class TestModel(RequiredStart, BaseData):
    pass

class TestModel2(RequiredStart, ModelActivation):
    pass

However, if the overriding field is pushed up to a grandparent model, rather than a direct parent, it works fine.

class TestModel3(RequiredStartEnd, BaseData):
    pass

class TestModel4(RequiredStartEnd, ModelActivation):
    pass

In my limited debugging, it appears to me that this is because of the way inherited_attributes is tracked in the new method of the ModelBase model metaclass (in django.db.models.base.py). For a grandparent model, not being a direct parent, all items in the dict will be added to inherited_attributes, which includes the fields:

            if base not in parents or not hasattr(base, '_meta'):
                # Things without _meta aren't functional models, so they're
                # uninteresting parents.
                inherited_attributes.update(base.__dict__)
                continue

However, when a field is inherited from a direct parent, it is not added to inherited_attributes, it is not added to field_names, and it does not appear in new_class.dict, so the field from the ancestor higher up in the mro is also added via

                for field in parent_fields:
                    if (field.name not in field_names and
                            field.name not in new_class.__dict__ and
                            field.name not in inherited_attributes):
                        new_field = copy.deepcopy(field)
                        new_class.add_to_class(field.name, new_field)

Change History (3)

comment:1 by Mariusz Felisiak, 2 years ago

Cc: Jarek Glowacki Carlton Gibson added

comment:2 by Ken Whitesell, 2 years ago

Ben, this appears to be fixed in the Django 4.0 pre-release version.

I can recreate it using your example under 3.2.9, but not under 4.0.pre.

comment:3 by Mariusz Felisiak, 2 years ago

Resolution: fixed
Status: newclosed
Note: See TracTickets for help on using tickets.
Back to Top