FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.52k stars 398 forks source link

Factory for Django model linking to self with related_name #327

Open maxshilov opened 8 years ago

maxshilov commented 8 years ago

Guys, Please advise how can I make factory with FactoryBoy 2.6.0 and python 2.7 from django model below . Each object may have 'next' object, and if some object is the 'next' for another object, then it knows this object by 'previous' attribute.

class MyModel(models.Model):
    name = models.CharField('Name', max_length=128)
    next = models.OneToOneField('self', related_name='previous', 
                                                     on_delete=models.SET_NULL, 
                                                      null=True, blank=True)

I tried following Factory:

class MyModelFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = 'app.MyModel'

    next = factory.SubFactory('pp.app.tests.factories.MyModelFactory', previous=None)
    previous = factory.RelatedFactory('pp.app.tests.factories.MyModelFactory', 'next')

But, got the "RuntimeError: maximum recursion depth exceeded in cmp" error.

rbarrois commented 8 years ago

Indeed, you need to tell factory_boy when to stop digging deeper ;)

The easiest way would be to specify it on call: MyModelFactory(next__next__next__next=None, previous__previous__previous=None).

maxshilov commented 8 years ago

thank you for a quick reply!

is it possible to specify maximum recursion depth inside the factory declaration?

rbarrois commented 8 years ago

It is not possible natively.

You could write a custom ComplexParameter for this:


from factory import declarations

class RecursiveStopper(declarations.ComplexParameter):
    def __init__(self, default_depth, recursive_field, stopper_value=None):
        self.default_depth = default_depth
        self.recursive_field = recursive_field
        self.stopper_value = stopper_value

    def compute(self, field_name, declarations):
        depth = declarations.get(field_name) or self.default_depth
        # use depth + 1: if depth is 0, we must stop now => set FIELD_NAME=None
        override = '__'.join([self.recursive_field] * (depth + 1))
        return {override: self.stopper_value}

With this, your factory would look like this:

class MyFactory(factory.django.DjangoModelFactory):
    class Params:
        next_depth = RecursiveStopper(10, 'next')
        previous_depth = RecursiveStopper(5, 'previous')

Could you try this and let us know:

If it's useful, we'll add it to the core :)

rbarrois commented 7 years ago

@maxshilov does the proposed API look good to you?

leamingrad commented 6 years ago

The parameter method outlined above doesn't seem to work with newer version of factoryboy (I am on 2.11.1). In particular, if you rename the compute method to as_declarations to fit in with the new way of declaring parameters the declaration dictionary will not contain field_name if you declare it as an argument.

In the example above, running MyFactory(next_depth=20) will still give you a next depth of 10.

I had a go at getting a toy version of this working another way:

import factory

class RecursiveClass:
    def __init__(self, child):
        self.child = child

class RecursiveFactory(factory.Factory):
    class Meta:
        model = RecursiveClass
        exclude = ('child_depth_temp', )

    class Params:
        child_depth = 10

    child_depth_temp=factory.LazyAttribute(lambda o: o.child_depth-1)
    child = factory.Maybe(
        factory.LazyAttribute(lambda o: o.child_depth > 0),
        yes_declaration=factory.SubFactory(
            'tests.test_factories.RecursiveStopperFactory',
            child_depth=factory.SelfAttribute('..child_depth_temp'),
        ),
        no_declaration=None,
    )

This seems to work - RecursiveFactory(20) will give you a 20-deep recursion rather than 10-deep.

@rbarrois: Is there still a way to do this with a parameter or similar? It would be nice to be able to separate the logic for handling recursion depth from the actual factory declaration.