Skip to content

Commit

Permalink
Merge pull request #8 from olegpidsadnyi/lazyfixture
Browse files Browse the repository at this point in the history
Fixture attributes implemented
  • Loading branch information
olegpidsadnyi committed May 29, 2015
2 parents d578b76 + d53b09a commit e4c2bc2
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 13 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Unreleased
- post_generation extra parameters fixed (olegpidsadnyi)
- fixture partial specialization (olegpidsadnyi)
- fixes readme and example (dduong42)
- lazy fixtures (olegpidsadnyi)
- deferred post-generation evaluation (olegpidsadnyi)
- hooks (olegpidsadnyi)


1.0.2
Expand Down
41 changes: 41 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,47 @@ function call.
assert female_author.name == "Jane Doe"
Fixture attributes
------------------

Sometimes it is necessary to pass an instance of another fixture as an attribute value to the factory.
It is possible to override the generated attribute fixture where desired values can be requested as
fixture dependencies. There is also a lazy wrapper for the fixture that can be used in the parametrization
without defining fixtures in a module.


LazyFixture constructor accepts either existing fixture name or callable with dependencies:

.. code-block:: python
import pytest
from pytest_factoryboy import register, LazyFixture
@pytest.mark.parametrize("book__author", [LazyFixture("another_author")])
def test_lazy_fixture_name(book, another_author):
"""Test that book author is replaced with another author by fixture name."""
assert book.author == another_author
@pytest.mark.parametrize("book__author", [LazyFixture(lambda another_author: another_author)])
def test_lazy_fixture_callable(book, another_author):
"""Test that book author is replaced with another author by callable."""
assert book.author == another_author
# Can also be used in the partial specialization during the registration.
register(AuthorFactory, "another_book", author=LazyFixture("another_author"))
Hooks
-----

pytest-factoryboy exposes several `pytest hooks <http://pytest.org/latest/plugins.html#well-specified-hooks>`_
which might be helpful controlling database transaction, for reporting etc:

* pytest_factoryboy_done(request) - Called after all factory based fixtures and their post-generation actions were evaluated.


License
-------
Expand Down
7 changes: 5 additions & 2 deletions pytest_factoryboy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
__version__ = '1.0.3'

try:
from .fixture import register
from .fixture import register, LazyFixture

__all__ = [register.__name__]
__all__ = [
register.__name__,
LazyFixture.__name__,
]
except ImportError: # pragma: no cover
# avoid import errors when only __version__ is needed (for setup.py)
pass
57 changes: 48 additions & 9 deletions pytest_factoryboy/fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ def get_deps(factory_class, parent_factory_class=None, model_name=None):

def model_fixture(request, factory_name):
"""Model fixture implementation."""

def evaluate(value):
"""Evaluate the declaration (lazy fixtures, etc)."""
return value.evaluate(request) if isinstance(value, LazyFixture) else value

factory_class = request.getfuncargvalue(factory_name)
prefix = "".join((request.fixturename, SEPARATOR))
data = {}
Expand All @@ -125,10 +130,11 @@ def model_fixture(request, factory_name):
data[argname[len(prefix):]] = request.getfuncargvalue(argname)

class Factory(factory_class):

@classmethod
def attributes(cls, create=False, extra=None):
return dict(
(key, value)
(key, evaluate(value))
for key, value in super(Factory, cls).attributes(create=create, extra=extra).items()
if key in data
)
Expand All @@ -138,23 +144,32 @@ def attributes(cls, create=False, extra=None):

postgen_declarations = {}
postgen_declaration_args = {}
related = []
postgen = []
if factory_class._meta.postgen_declarations:
for attr, declaration in sorted(factory_class._meta.postgen_declarations.items()):
postgen_declarations[attr] = declaration
postgen_declaration_args[attr] = declaration.extract(attr, data)
if isinstance(declaration, factory.RelatedFactory):
related.append(attr)
else:
postgen.append(attr)

result = Factory(**data)
if postgen_declarations:
request._fixturedef.cached_result = (result, 0, None)
request._fixturedefs[request.fixturename] = request._fixturedef
factoryboy_request = request.getfuncargvalue("factoryboy_request")

for attr, declaration in postgen_declarations.items():
if isinstance(declaration, factory.RelatedFactory):
def deferred():
for attr in related:
request.getfuncargvalue(prefix + attr)
else:
extra = postgen_declaration_args[attr].extra
declaration.function(result, True, request.getfuncargvalue(prefix + attr), **extra)

for attr in postgen:
declaration = postgen_declarations[attr]
context = postgen_declaration_args[attr]
context.value = evaluate(request.getfuncargvalue(prefix + attr))
context.extra = dict((key, evaluate(value)) for key, value in context.extra.items())
declaration.call(result, True, context)

factoryboy_request.defer(deferred)
return result


Expand Down Expand Up @@ -182,3 +197,27 @@ def get_caller_module(depth=2):
if module is None:
return get_caller_module(depth=depth) # pragma: no cover
return module


class LazyFixture(object):

"""Lazy fixture."""

def __init__(self, fixture):
"""Lazy pytest fixture wrapper.
:param fixture: Fixture name or callable with dependencies.
"""
self.fixture = fixture

def evaluate(self, request):
"""Evaluate the lazy fixture.
:param request: pytest request object.
:return: evaluated fixture.
"""
if callable(self.fixture):
kwargs = dict((arg, request.getfuncargvalue(arg)) for arg in inspect.getargspec(self.fixture).args)
return self.fixture(**kwargs)
else:
return request.getfuncargvalue(self.fixture)
5 changes: 5 additions & 0 deletions pytest_factoryboy/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""pytest-factoryboy pytest hooks."""


def pytest_factoryboy_done(request):
"""Called after all factory based fixtures and their post-generation actions were evaluated."""
57 changes: 57 additions & 0 deletions pytest_factoryboy/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""pytest-factoryboy plugin."""

import pytest


class Request(object):

"""PyTest FactoryBoy request."""

def __init__(self):
self.deferred = []
self.is_evaluated = False

def defer(self, function):
"""Defer post-generation declaration execution until the end of the test setup.
:param function: Function to be deferred.
:note: Once already evaluated all following defer calls will execute the function directly.
"""
if self.is_evaluated:
function()
else:
self.deferred.append(function)

def evaluate(self):
"""Finalize, run deferred post-generation actions, etc."""
while True:
try:
self.deferred.pop(0)()
except IndexError:
return
finally:
self.is_evaluated = True


@pytest.fixture
def factoryboy_request():
"""PyTest FactoryBoy request fixture."""
return Request()


@pytest.mark.tryfirst
def pytest_runtest_call(item):
"""Before the test item is called."""
try:
request = item._request
except AttributeError:
# pytest-pep8 plugin passes Pep8Item here during tests.
return
request.getfuncargvalue("factoryboy_request").evaluate()
request.config.hook.pytest_factoryboy_done(request=request)


def pytest_addhooks(pluginmanager):
"""Register plugin hooks."""
from pytest_factoryboy import hooks
pluginmanager.addhooks(hooks)
34 changes: 32 additions & 2 deletions tests/test_factory_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from factory import fuzzy
import pytest

from pytest_factoryboy import register
from pytest_factoryboy import register, LazyFixture


class User(object):
Expand Down Expand Up @@ -67,7 +67,8 @@ class Meta:

name = "Charles Dickens"

register_user__is_active = True
register_user__is_active = True # Make sure fixture is generated
register_user__password = "qwerty" # Make sure fixture is generated

@factory.post_generation
def register_user(author, create, username, **kwargs):
Expand Down Expand Up @@ -167,3 +168,32 @@ def test_second_author(author, second_author):
def test_partial(partial_author):
"""Test fixture partial specialization."""
assert partial_author.name == "John Doe"


register(AuthorFactory, "another_author", name=LazyFixture(lambda: "Another Author"))


@pytest.mark.parametrize("book__author", [LazyFixture("another_author")])
def test_lazy_fixture_name(book, another_author):
"""Test that book author is replaced with another author by fixture name."""
assert book.author == another_author
assert book.author.name == "Another Author"


@pytest.mark.parametrize("book__author", [LazyFixture(lambda another_author: another_author)])
def test_lazy_fixture_callable(book, another_author):
"""Test that book author is replaced with another author by callable."""
assert book.author == another_author
assert book.author.name == "Another Author"


@pytest.mark.parametrize(
("author__register_user", "author__register_user__password"),
[
(LazyFixture(lambda: "lazyfixture"), LazyFixture(lambda: "asdasd")),
]
)
def test_lazy_fixture_post_generation(author):
"""Test that post-generation values are replaced with lazy fixtures."""
# assert author.user.username == "lazyfixture"
assert author.user.password == "asdasd"

0 comments on commit e4c2bc2

Please sign in to comment.