Factories¶
Basic¶
So the most basic factories that you can write are just functions which return models.
from sqlalchemy_model_factory import register_at
@register_at('foo')
def new_foo():
return Foo()
def test_foo(mf):
foo = mf.foo.new()
assert isinstance(foo, Foo)
Nested¶
Note, you can also create nested models through relationships and whatnot; and that will all work normally.
from sqlalchemy_model_factory import register_at
class Foo(Base):
...
bar = relationship('Bar')
class Bar(Base):
...
baz = relationship('Baz')
@register_at('foo')
def new_foo():
...
General Use Functions¶
In some cases, you’ll have a a function already handy that returns the equivalent signature of a model (or you just want a function that returns the signature).
In this case, your function will act as the originally defined function when called normally,
however when invoked in the context of the ModelFactory
, it returns the specified model
instance.
from sqlalchemy_model_factory import for_model, register_at
@register_at('foo')
@for_model(Foo)
def new_foo():
return {'id': 1, 'name': 'bar'}
def test_foo(mf):
foo = mf.foo.new()
assert foo.id == 1
assert foo.name == 'bar'
raw_foo = new_foo()
assert raw_foo == {'id': 1, 'name': 'bar'}
Sources of Uniqueness¶
Suppose you’ve got a column defined with a constraint, like name = Column(types.Integer(), unique=True).
Suddenly you’ll need to parametrize your factory to accept a name
param. However if you
actually don’t care about the specific name values, you have a few options.
Autoincrement¶
Automatically incrementing number values is one option. Your factory will be automatically
supplied with an autoincrement
parameter, known to not collide with previously
generated values.
from sqlalchemy_model_factory import autoincrement, register_at
@register_at('foo')
@autoincrement
def new_foo(autoincrement=1):
return Foo(name=f'name{autoincrement}')
def test_foo(mf):
assert mf.foo.new().name == 'name1'
assert mf.foo.new().name == 'name2'
assert mf.foo.new().name == 'name3'
Fluency¶
You’ve been working along and writing factories and you finally find yourself in a situation like this.
@register_at('foo')
@autoincrement
def new_foo(name='name', height=2, width=3, depth=3, category='foo', autoincrement=1):
...
And in the event your test requires a number of identical parameters across multiple calls, you might end up with test code that looks like.
def test_foo(mf):
width_4 = mf.foo.new(height=3, category='bar', width=4)
width_5 = mf.foo.new(height=3, category='bar', width=5)
width_6 = mf.foo.new(height=3, category='bar', width=6)
width_7 = mf.foo.new(height=3, category='bar', width=7)
...
The above (as a dirt simple example, that might be easily solved in different ways) has got most of its information duplicated unnecessarily.
The “fluent” decorator¶
A simple solution to this general problem category is the fluent
decorator. Which adapts a
given callable to be able to be called in a fluent style.
def test_foo(mf):
bar_type_foo = mf.foo.new(3).category('bar')
width_4 = bar_type_foo.width(4).bind()
width_5 = bar_type_foo.width(5).bind()
width_6 = bar_type_foo.width(6).bind()
width_7 = bar_type_foo.width(7).bind()
Now in this particular case, you could have just done a for-loop over the original set of calls,
or maybe functools.partial
could have sufficed, but the fluent pattern is more generally
useful than just in cases like this.
Also from the callee’s perspective, there’s not necessarily any requirement that all the args have
their parameter names supplied, so you might end up reading foo.new('a', 3, 4, 5, 'ro')
,
which is arguably far less readable.
To note, the bind
call at the end of each expression above is necessary to let the fluent
calls know that its done being called (because as you might notice, we didn’t call all the available
methods we could have called). But this also serves as a convenient point at which to add custom
behaviors. (for example you could supply .bind(call_after=print) to have it print out the final
result of the function; see the api portion of the docs for the full set of options.)
Class-style factories¶
From the perspective of the model factory, all factory “functions” are just callables, so you can
always manually mimic something like the above fluent
decorator in a class so that you can
implement your own custom behavior for each option.
@register_at('foo')
class NewFoo:
def __init__(self, **kwargs):
self.kwargs
def name(self, name):
self.__class__(**self.kwargs, name=name)
def width(self, width):
self.__class__(**self.kwargs, width=width)
def bind(self):
return Foo(**self.kwargs)
Albeit, with the above, very naive implementation, your test code would end up looking like
def test_foo(mf):
bar_foo = mf.foo.new().name('bar')
width_4 = bar_fo.width(4).bind()
width_5 = bar_fo.width(5).bind()