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 returnsTrue
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 andchild
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,
}