Kurt Peek
Kurt Peek

Reputation: 57771

Factory boy yielding a "maximum recursion depth exceeded" error

I'd like to defined two models, Company and Package. Each Package has only one Company, but a Company can have several Packages. However, each company can have only one default_package (which can be null). I've set this up as follows:

class Company(models.Model):
    default_package = models.OneToOneField(
        'dashboard.Package',
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
        related_name='default_for_%(class)s')


class Package(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)

where dashboard is the app label.

In order to simplify tests with these models, I've created test factories for them using factory_boy as follows:

import factory
from .models import Company, Package

class CompanyFactory(factory.Factory):
    class Meta:
        model = Company 

    default_package = factory.SubFactory('dashboard.test_factories.PackageFactory')


class PackageFactory(factory.Factory):
    class Meta:
        model = Package

    company = factory.SubFactory(CompanyFactory)

Now I'm trying two tests:

class DefaultPackageTest(TestCase):
    def test_1(self):
        company = Company.objects.create()

    def test_2(self):
        company = CompanyFactory()

The first one simply creates a Company, whereas the second one tries to do the same thing using the CompanyFactory.

Strangely, however, the first test passes, whereas the second one fails:

  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 233, in recurse
    return builder.build(parent_step=self, force_sequence=force_sequence)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 272, in build
    step.resolve(pre)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 221, in resolve
    self.attributes[field_name] = getattr(self.stub, field_name)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 355, in __getattr__
    declaration = self.__declarations[name]
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/factory/builder.py", line 121, in __getitem__
    context=self.contexts[key],
RecursionError: maximum recursion depth exceeded

----------------------------------------------------------------------
Ran 2 tests in 0.012s

FAILED (errors=1)
Destroying test database for alias 'default'...

Any idea why this is not working? I believe I have followed the docs (http://factoryboy.readthedocs.io/en/latest/reference.html#circular-imports) by providing a full path to the CompanyFactory's sub-factory.

Update

It seems that this use case is addressed by Factory Boy's post-generation hooks. What seems promising is the RelatedFactory, for which the following example is given:

class CityFactory(factory.Factory):
    class Meta:
        model = City

    capital_of = None
    name = "Toronto"

class CountryFactory(factory.Factory):
    class Meta:
        model = Country

    lang = 'fr'
    capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris")

which is tested in the Python REPL as follows:

>>> france = CountryFactory()
>>> City.objects.get(capital_of=france)
<City: Paris>

I'm having difficulty, however, applying this example to my situation. (It doesn't help that there is no text explanation or code of the City and Country models in the docs). It seems like capital_city is analogous to default_package in my case, so I tried turning it into a RelatedFactory like so,

default_package = factory.RelatedFactory('dashboard.test_factories.PackageFactory')

but I still get the same error.

Upvotes: 5

Views: 1424

Answers (2)

linaa
linaa

Reputation: 339

Quoting from the specific documentation of RelatedFactory, you require a RelatedFactory as the Package model requires a company to be built first:

A RelatedFactory behaves mostly like a SubFactory, with the main difference that the related Factory will be generated after the base Factory.

and will need to precise the following parameter:

factory_related_name

If set, the object generated by the factory declaring the RelatedFactory is passed as keyword argument to the related factory.

In your case, if we put those two together:

class PackageFactory(factory.Factory):
    class Meta:
        model = Package

    company = factory.SubFactory('dashboard.test_factories.CompanyFactory')


class CompanyFactory(factory.Factory):
    class Meta:
        model = Company

    # Putting the pieces together
    default_package = factory.RelatedFactory(
        PackageFactory,
        factory_related_name='company' 
    )

It is equivalent to Gustavo's solution with the @factory.post_generation decorator, but follows Factory Boy's declarative mindset more closely in my opinion.

Upvotes: 0

Gustavo Santiago
Gustavo Santiago

Reputation: 111

So, I achieved that by implementing a post_generation function:

    class PackageFactory(factory.Factory):
        class Meta:
            model = Package

        company = factory.SubFactory('dashboard.test_factories.CompanyFactory')

    class CompanyFactory(factory.Factory):
        class Meta:
            model = Company 

        @factory.post_generation
        def default_package(self, create, _, **__):
            PackageFactory(company=self)

The company=self kwarg stops the recursion and the factory is properly created with the desired default package attribute.

Upvotes: 4

Related Questions