Advanced usage

lazy

As an special case, if the callable returns an iterable lazy will wrap the returned value within a Gen instance:

>>> from itertools import cycle
>>> from arv.factory.api import Factory
>>> from arv.factory.api import gen
>>> class PetFactory(Factory):
...     defaults = {
...         "name": "Rocky",
...         "kind": gen.lazy(cycle, ["dog", "cat"]),
...     }
...
>>> factory = PetFactory()
>>> factory()
{'kind': 'dog', 'name': 'Rocky'}
>>> factory()
{'kind': 'cat', 'name': 'Rocky'}
>>> factory()
{'kind': 'dog', 'name': 'Rocky'}

Another usage for lazy is overriding default values when creating factories. Metafactories as default values are already lazily evaluated but they receive no arguments. Wrapping them with lazy allow us to override the default values:

>>> class PersonFactory(Factory):
...     defaults = {
...         "name": "Bob",
...         "pet": gen.lazy(PetFactory, name="Toby"),
...     }
...
>>> factory1 = PersonFactory()
>>> factory2 = PersonFactory()
>>> factory1()
{'pet': {'kind': 'dog', 'name': 'Toby'}, 'name': 'Bob'}
>>> factory2()
{'pet': {'kind': 'dog', 'name': 'Toby'}, 'name': 'Bob'}

Defining a custom generator

Let’s say we need a value generator for the fibonacci sequence. All we need is an iterable with the sequence’s values, a python generator function is a good choice:

>>> def fib():
...     a, b = 0, 1
...     while True:
...         yield a
...         a, b = b, a + b
...
>>> g = fib()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
1
>>> g.next()
2

Now we can create a factory:

>>> from arv.factory.api import gen
>>> g = fib()
>>> factory = Factory(n=gen.Gen(g))
>>> factory()
{'n': 0}
>>> factory()
{'n': 1}

Alternatively we can use the mkgen function:

>>> from arv.factory.api import gen
>>> g = fib()
>>> factory = Factory(n=gen.mkgen(g.next))
>>> factory()
{'n': 0}
>>> factory()
{'n': 1}

If we plan to use the generator in many factories it would be better definning a constructor and a metafactory:

>>> def Fib():
...     iterable = fib()
...     return gen.mkgen(fib().next)

Here we create an interable, a python generator, calling the generator function fib, then we call mkgen passing the next method from the iterable. Remember? mkgen creates a value generator wich will call the function it receives as argument each time it’s consumed.

>>> class MyFactory(Factory):
...     defaults = {"n": gen.lazy(Fib)}
...
>>> factory = MyFactory()
>>> factory()
{'n': 0}

Fib is a function and metafactories don’t evaluate functions, only lazy instances, so we need to wrap Fib with lazy in order to get it called at factory creation time.

If we want to avoid having to use lazy explicitly we can do:

>>> FIB = gen.lazy(Fib)
>>> class MyFactory(Factory):
...     defaults = {"n": FIB}
...
>>> factory = MyFactory()
>>> factory()
{'n': 0}

That’s a lot of repetitive work so arv.factory defines a shortcut for this:

>>> Fib = gen.mkconstructor(fib)
>>> class MyFactory(Factory):
...     defaults = {"n": Fib}
...
>>> factory = MyFactory()
>>> factory()
{'n': 0}

In a previous example (lazy) we have seen how to use lazy + cycle. Alternatively we can create a lazy constructor:

>>> Cycle = gen.mkconstructor(cycle, (2, 4, 8))

Defining a persistent factory

In case that there’s not a persitent factory for your backend defining a new one is easy as cake.

A persistent factory must inherit from PersitanceMixin and some factory class and must implement the methods:

  • _get_fields(obj): receives an object and must return a list of pairs (field_name, field_value).
  • _is_persistable(obj): receives a field’s value and returns True if it’s persistable by the backend. Usually it’s enough testing is the object is an instance of some class, django.db.models.Model by the way, or if it has some distinguished attribute or method.
  • _link_to_parent(parent, name, child): defines the relationship between an object and a subobject. parent is the parent object, name is the field name and child is the subobject stored in that field.
  • _save(obj): receives an object and is responsible for persisting the object to the backend. It’s guaranteed that it will be called only for objects that pass the _is_persistable check. It must return the persisted object.

As an example here’s the implementations for DjangoFactory:

class DjangoFactory(PersistanceMixin, Factory):
    """Factory for creating django models.
    """

    def _get_fields(self, obj):
        for f in obj._meta.get_fields():
            yield f.name, getattr(obj, f.name)

    def _is_persistable(self, obj):
        return isinstance(obj, Model)

    def _link_to_parent(self, parent, name, child):
        setattr(parent, name + "_id", child.pk)

    def _save(self, obj):
        obj.save()
        return obj

Using faker

We can use the faker library with arv.factory. Since the faker providers are callables all we need to do is wrapping them with mkgen:

from faker import Factory as FakerFactory
from arv.factory.api import Factory
from arv.factory.api import gen

class PersonFactory(Factory):
    faker = FakerFactory.create()
    defaults = {
        "name": gen.mkgen(faker.first_name),
        "birth_date": gen.mkgen(faker.date),
    }

This will work but there’s a lot of boilerplate. We can do better defining our own fakers factory that eases integration with arv.factory factories:

# helper_module.py
from faker import Factory as _FakerFactory
from arv.factory.api import gen

class FakerFactory(object):
    def __init__(self, *args, **kwargs):
        self._faker = _FakerFactory.create(*args, **kwargs)
    def __getattr__(self, name):
        method = gen.mkgen(getattr(self._faker, name))
        setattr(self, name, method)
        return method

# factories.py
from arv.factory.api import Factory
from helper_module import FakerFactory

class PersonFactory(Factory):
    faker = FakerFactory()
    defaults = {
        "name": faker.first_name,
        "birth_date": faker.date,
    }