Fields do not exist in this model errors with OneToOneField in Django 5
Gwildor opened this issue · comments
Description
After upgrading from Django 4.2 to Django 5, some of our tests are failing. These are using a OneToOneField between two models. Creating one instance through a factory with an instance to the other model fails because the related name is not accepted by the Django model manager.
The workaround is very simple (see below), but I think this is a bug in this library as this was working fine under Django 4.2. We're using factory boy 3.3.
To Reproduce
Share how the bug happened:
Model / Factory code
class Shop(models.Model):
pass
class Event(models.Model):
default_shop = models.OneToOneField(
"shop.Shop",
related_name="default_event",
on_delete=models.SET_NULL,
null=True,
blank=True,
)
class EventFactory(factory.django.DjangoModelFactory):
class Meta:
model = Event
skip_postgeneration_save = True
class ShopFactory(factory.django.DjangoModelFactory):
class Meta:
model = Shop
The issue
Before, we were able to first create an Event, and then create a Shop and immediately set the default_event
of the Shop instance to the Event instance. With Django 5, this now fails in the factory, while still working in a Django shell. So it seems like an issue with factory boy not supporting Django 5 properly here.
@pytest.mark.django_db
class Test:
@pytest.fixture
def shop(self):
event = EventFactory.create()
shop = ShopFactory.create(default_event=event)
$ pytest
> shop = ShopFactory.create(default_event=event)
apps/foo/tests/test_foo.py:335:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/factory/base.py:528: in create
return cls._generate(enums.CREATE_STRATEGY, kwargs)
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/factory/django.py:121: in _generate
return super()._generate(strategy, params)
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/factory/base.py:465: in _generate
return step.build()
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/factory/builder.py:274: in build
instance = self.factory_meta.instantiate(
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/factory/base.py:317: in instantiate
return self.factory._create(model, *args, **kwargs)
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/factory/django.py:174: in _create
return manager.create(*args, **kwargs)
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/django/db/models/manager.py:87: in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <QuerySet []>
kwargs = {'default_event': <Event: Event>, ...}
reverse_one_to_one_fields = frozenset({'default_event'})
def create(self, **kwargs):
"""
Create a new object with the given kwargs, saving it to the database
and returning the created object.
"""
reverse_one_to_one_fields = frozenset(kwargs).intersection(
self.model._meta._reverse_one_to_one_field_names
)
if reverse_one_to_one_fields:
> raise ValueError(
"The following fields do not exist in this model: %s"
% ", ".join(reverse_one_to_one_fields)
)
E ValueError: The following fields do not exist in this model: default_event
../../.cache/pypoetry/virtualenvs/foo-KcrdI-pR-py3.10/lib/python3.10/site-packages/django/db/models/query.py:670: ValueError
Notes
The workaround is very easy, just assign the relation after the object instance is created:
shop = ShopFactory.create()
shop.default_event = event
Any chance of introducing Django 5 support? FactoryBoy doesn't work well with relationships in Django 5.
That’s because the relationship is backward. default_event
is a related manager, not a field. It does not need to be populated by FactoryBoy. Django is right in reporting an error, it was just silently ignored with Django<5
The expected calls should be:
shop = ShopFactory()
EventFactory.create(default_shop=shop)
I don’t see a problem with FactoryBoy here?
problem here is that it fails when you use SubFactory
:
class UserFactory(DjangoModelFactory):
class Meta:
model = User
address= factory.SubFactory('path.to.AddressFactory')