FactoryBoy / factory_boy

A test fixtures replacement for Python

Home Page:https://factoryboy.readthedocs.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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')