13

Context: I have a model with two dates, I want to use factory.Faker for both of them but the second date should always be greater that the first one.

I tried this:

Model excerpt:

class Event(models.Model):
     execution_start_date = models.DateTimeField()
     execution_end_date = models.DateTimeField()

Factory:

class EventFactory(factory.DjangoModelFactory):
    class Meta:
        model = Event
        strategy = factory.BUILD_STRATEGY

    execution_start_date = factory.Faker('date_time_this_year', tzinfo=pytz.utc)
    @factory.lazy_attribute
    def execution_end_date(self):
        return factory.Faker('date_time_between_dates',
                             datetime_start=self.execution_start_date,
                             datetime_end=now(),
                             tzinfo=pytz.utc)

But when I try to use the factory from the python shell I got this:

In [3]: e = EventFactory()

In [4]: e.execution_end_date
Out[4]: <factory.faker.Faker at 0x1103f51d0>

The only way I managed to make it work was with like this:

@factory.lazy_attribute
def execution_end_date(self):
    # return factory.Faker('date_time_between_dates',
    #                      datetime_start=self.execution_start_date,
    #                      datetime_end=now(),
    #                      tzinfo=pytz.utc)
    faker = factory.Faker._get_faker()
    return faker.date_time_between_dates(datetime_start=self.execution_start_date,
                                         datetime_end=now(),
                                         tzinfo=pytz.utc)

But I honestly think there is a better way to do it.

My dependencies are:

  • Django (1.8.18)
  • factory-boy (2.8.1)
  • Faker (0.7.17)
diegueus9
  • 29,351
  • 16
  • 62
  • 74

2 Answers2

9

When lazy_attribute come into play you already have generated object on your hand. So you can work with, for example, random and timedelta, like this:

@factory.lazy_attribute
def execution_end_date(self):
    max_days = (now() - self.execution_start_date).days
    return self.execution_start_date + timedelta(random.randint(1, max_days))

or some other way to generate random date. There is no point to stick to factory_boy.Faker

EDIT

After my first answer I manage to found a way to do what you want, it's really simple.You just need to call generate() method with empty dict from Faker:

@factory.lazy_attribute
def execution_end_date(self):
    return factory.Faker('date_time_between_dates',
                         datetime_start=self.execution_start_date,
                         datetime_end=now(),
                         tzinfo=pytz.utc).generate({})
Dmitriy Repin
  • 258
  • 1
  • 9
  • I'd like to stick to factory-boy and Faker since the purpose of both libraries is to generate random data following certain rules, I don't see why I would create my own "faker" routine if I've already got one. Besides, I'd like to point out that your own answer includes "or some other way to generate random date" and that'd be Faker. There are a lot of corner cases when working with dates in python, and the best option is to use a library that covers those corner cases (like timezone handling, etc) – diegueus9 Aug 10 '17 at 21:44
  • 1
    @diegueus9 yes, I understand and luckely just a few days ago I found a way to do exactly this. I edit my answer with another solution. – Dmitriy Repin Aug 12 '17 at 10:29
  • 2
    for those wondering, the `{}` is for extra kwargs to something https://github.com/FactoryBoy/factory_boy/blob/bc9b5615558418012279aa5c526cbbf87f1fe5d7/factory/faker.py#L48 – Tim Diels Dec 11 '18 at 11:22
  • These are probably kwargs which override the kwargs to `Faker`, so you'll always want just `{}`. – Tim Diels Dec 12 '18 at 14:16
1

At first I was trying to do the same thing, but according to Factory Boy's documentation for the Faker wrapper, the parameters can be any valid declaration. That means that you're allowed to specify each of the faker's parameter as a SelfAttribute, LazyAttribute, etc. I don't know when this feature was first introduced, but version 3.1.0 (Oct 2020) docs mentions it for the first time.

I believe that the question's example can be rewritten as:

class EventFactory(factory.DjangoModelFactory):
    class Meta:
        model = Event
        strategy = factory.BUILD_STRATEGY

    execution_start_date = factory.Faker('date_time_this_year', tzinfo=pytz.utc)
    execution_end_date = factory.Faker('date_time_between_dates',
                             datetime_start=factory.SelfAttribute('..execution_start_date'),
                             datetime_end='now',
                             tzinfo=pytz.utc)

So basically it's turned around and the faker's parameters are evaluated instead of the LazyAttribute's return value. In my example datetime_start now refers to whatever execution_start_date resolves to and datetime_end takes a literal string 'now', that the Faker library will replace with the current datetime.

jboot
  • 701
  • 9
  • 17