FactoryBoy / factory_boy

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

Cannot use factory.Maybe without a “decider” model field #854

Open allynt opened 3 years ago

allynt commented 3 years ago

Description

Using a factory.Faker as the "decider" for a factory.Maybe instead of the name of a model field causes the following error:

.venv/lib/python3.8/site-packages/factory/base.py:40: in __call__
    return cls.create(**kwargs)
.venv/lib/python3.8/site-packages/factory/base.py:528: in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
.venv/lib/python3.8/site-packages/factory/django.py:117: in _generate
    return super()._generate(strategy, params)
.venv/lib/python3.8/site-packages/factory/base.py:465: in _generate
    return step.build()
.venv/lib/python3.8/site-packages/factory/builder.py:258: in build
    step.resolve(pre)
.venv/lib/python3.8/site-packages/factory/builder.py:199: in resolve
    self.attributes[field_name] = getattr(self.stub, field_name)
.venv/lib/python3.8/site-packages/factory/builder.py:344: in __getattr__
    value = value.evaluate_pre(
.venv/lib/python3.8/site-packages/factory/declarations.py:476: in evaluate_pre
    choice = self.decider.evaluate(instance=instance, step=step, extra={})
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <factory.faker.Faker object at 0x7ff008607670>, instance = <Resolver for <BuildStep for <StepBuilder(<DjangoOptions for OrganizationFactory>, strategy='create')>>>
step = <BuildStep for <StepBuilder(<DjangoOptions for OrganizationFactory>, strategy='create')>>, extra = {}

    def evaluate(self, instance, step, extra):
>       locale = extra.pop('locale')
E       KeyError: 'locale'

.venv/lib/python3.8/site-packages/factory/faker.py:46: KeyError

To Reproduce

Model / Factory code

class Organization(models.Model):
    is_active = models.BooleanField()
    name = models.SlugField(max_length=64, unique=True, blank=False, null=False)
    description = models.TextField(blank=True, null=True)

class OrganizationFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Organization

    is_active = factory.Faker("boolean", chance_of_getting_true=50)
    name = factory.LazyAttributeSequence(lambda o, n: f"organization-{n}")
    description = factory.Maybe(
        factory.Faker("boolean", chance_of_getting_true=50),
        yes_declaration=factory.Faker("text"),
        no_declaration=None,
    )
The issue

I want description to have a value only some of the time - regardless of the value of any other model fields. So although I could pass "is_active" as the first argument to factory.Maybe it doesn't quite fit my use-case.

I have managed to get it working using this ugly code:

from functools import partial
from faker import Faker
fake = Faker()
class OrganizationFactory(factory.django.DjangoModelFactory):
  ...
  description = factory.Maybe(
    factory.LazyFunction(partial(fake.boolean, chance_of_getting_true=50)),
    yes_declaration=factory.Faker("text"),
    no_declaration=None,
)

But that feels like a hack.

Notes

I feel like this was meant to have been fixed by https://github.com/FactoryBoy/factory_boy/pull/828.

rbarrois commented 3 years ago

Thanks for the very thorough bug report! Indeed, this should have been fixed in that issue, but some test cases were missing :(

Apreche commented 2 years ago

I am experiencing this as well, but at least now I know what the problem is.