factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

Can't pass size to RelatedFactoryList from parent factory's Params

Open mcabrams opened this issue 5 years ago • 6 comments

Description

I can't seem to find a way to pass the size kwarg from a parent factory's params through to a RelatedFactoryList. I've tried every permutation of LazyAttribute to SelfAttribute to lambdas.

Model / Factory code
class BarFactory(factory.DjangoModelFactory)
    class Meta:
        model = Bar

class FooFactory(factory.DjangoModelFactory)
    class Meta:
        model = Foo

    class Params:
        size = 2

    bars = factory.RelatedFactoryList(BarFactory, size=factory.SelfAttribute('..size'))
The issue
FooFactory()
# ->
  File "/usr/local/lib/python3.7/site-packages/factory/base.py", line 46, in __call__
    return cls.create(**kwargs)
  File "/usr/local/lib/python3.7/site-packages/factory/base.py", line 564, in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
  File "/usr/local/lib/python3.7/site-packages/factory/django.py", line 141, in _generate
    return super(DjangoModelFactory, cls)._generate(strategy, params)
  File "/usr/local/lib/python3.7/site-packages/factory/base.py", line 501, in _generate
    return step.build()
  File "/usr/local/lib/python3.7/site-packages/factory/builder.py", line 272, in build
    step.resolve(pre)
  File "/usr/local/lib/python3.7/site-packages/factory/builder.py", line 221, in resolve
    self.attributes[field_name] = getattr(self.stub, field_name)
  File "/usr/local/lib/python3.7/site-packages/factory/builder.py", line 375, in __getattr__
    extra=context,
  File "/usr/local/lib/python3.7/site-packages/factory/declarations.py", line 321, in evaluate
    return self.generate(step, defaults)
  File "/usr/local/lib/python3.7/site-packages/factory/declarations.py", line 411, in generate
    return step.recurse(subfactory, params, force_sequence=force_sequence)
  File "/usr/local/lib/python3.7/site-packages/factory/builder.py", line 233, in recurse
    return builder.build(parent_step=self, force_sequence=force_sequence)
  File "/usr/local/lib/python3.7/site-packages/factory/builder.py", line 299, in build
    context=postgen_context,
  File "/usr/local/lib/python3.7/site-packages/factory/declarations.py", line 694, in call
    else self.size())]
TypeError: 'SelfAttribute' object is not callable

Notes

If there is a different strategy for passing the size of a RelatedFactoryList from it's parent Factory (i.e. being able to specify size of the list when instantiating the parent factory via a kwarg) I'd be happy to know of an alternative approach.

mcabrams avatar Aug 06 '20 00:08 mcabrams

I'm assuming this is due to bars being a ManyToManyField... it seems like in this case I may not be able to use a RelatedFactoryList?

mcabrams avatar Aug 06 '20 00:08 mcabrams

I also ran into this issue with RelatedFactoryList.

I got around this by using the post_generation hook to create my related factory list manually. From what I can tell, there is no support for the size param to be a LazyAttribute or SelfAttribute. There's an inactive but relevant PR about this at #727.

timorthi avatar Aug 21 '20 02:08 timorthi

@timorthi indeed; this could be adressed by #774 — which is significant change, for which I'd like to gather some feedback before going forward ;)

rbarrois avatar Aug 21 '20 07:08 rbarrois

I had the same issue. Here was my quick solution (I know this would cause issues in some cases, but it worked for my usage)

class RelatedFactoryVariableList(RelatedFactoryList):
    """allows overriding ``size`` during factory usage, e.g. ParentFactory(list_factory__size=4)"""
    def call(self, instance, step, context):
        size = context.extra.get('size', self.size)
        assert isinstance(size, int)
        return [super(RelatedFactoryList, self).call(instance, step, context) for i in range(size)]

So for OP's example, you would use it like FooFactory(bars__size=4)

ibushong avatar Sep 19 '20 02:09 ibushong

I had the same issue. Here was my quick solution (I know this would cause issues in some cases, but it worked for my usage)

Nice workaround!

In my case, I needed to remove size from context.extra (by using pop instead of get), otherwise the call to the super constructor would throw TypeError: 'size' is an invalid keyword argument for Bar. So what works for me is:

class RelatedFactoryVariableList(RelatedFactoryList):
    """allows overriding ``size`` during factory usage, e.g. ParentFactory(list_factory__size=4)"""
    def call(self, instance, step, context):
        size = context.extra.pop('size', self.size)
        assert isinstance(size, int)
        return [super(RelatedFactoryList, self).call(instance, step, context) for i in range(size)]

nidico avatar May 27 '22 01:05 nidico