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 returnsTrueif it’s persistable by the backend. Usually it’s enough testing is the object is an instance of some class,django.db.models.Modelby 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.parentis the parent object,nameis the field name andchildis 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_persistablecheck. 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,
}