Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Changelog
=========

1.0.2
-----

- refactoring of the fixture function compilation (olegpidsadnyi)
- related factory fix (olegpidsadnyi)

1.0.1
-----

Expand All @@ -9,4 +15,4 @@ Changelog
1.0.0
-----

- initial release
- initial release (olegpidsadnyi)
2 changes: 1 addition & 1 deletion pytest_factoryboy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""pytest-factoryboy public API."""

__version__ = '1.0.1'
__version__ = '1.0.2'

try:
from .fixture import register
Expand Down
208 changes: 86 additions & 122 deletions pytest_factoryboy/fixture.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
"""Factory boy fixture integration."""

import sys
import inspect
from types import CodeType

import factory
import inflection
import pytest
import six


SEPARATOR = "__"


FIXTURE_FUNC_FORMAT = """
def {name}({deps}):
return _fixture_impl(request, **kwargs)
"""


def make_fixture(name, module, func, args=None, **kwargs):
"""Make fixture function and inject arguments.

:param name: Fixture name.
:param module: Python module to contribute the fixture into.
:param func: Fixture implementation function.
:param args: Argument names.
"""
args = [] if args is None else list(args)
if "request" not in args:
args.insert(0, "request")
deps = ", ".join(args)
context = dict(_fixture_impl=func, kwargs=kwargs)
context.update(kwargs)
exec(FIXTURE_FUNC_FORMAT.format(name=name, deps=deps), context)
fixture_func = context[name]
fixture_func.__module__ = module
fixture = pytest.fixture(fixture_func)
setattr(module, name, fixture)
return fixture


def register(factory_class):
"""Register fixtures for the factory class."""
module = get_caller_module()
Expand All @@ -20,23 +47,40 @@ def register(factory_class):

deps = get_deps(factory_class)
for attr, value in factory_class.declarations(factory_class._meta.postgen_declarations).items():
fixture = None
attr_name = SEPARATOR.join((model_name, attr))
add_args = []

if isinstance(value, (factory.SubFactory, factory.RelatedFactory)):
subfactory_class = value.get_factory()
fixture = make_subfactory_fixture(subfactory_class)
add_args = get_deps(subfactory_class, factory_class)
make_fixture(
name=attr_name,
module=module,
func=subfactory_fixture,
args=get_deps(subfactory_class, factory_class),
factory_class=subfactory_class,
)
if isinstance(value, factory.RelatedFactory):
deps.extend(get_deps(subfactory_class, factory_class))
else:
fixture = make_attr_fixture(value)

if fixture is not None:
contribute_to_module(module, attr_name, fixture, add_args=add_args)

contribute_to_module(module, factory_name, make_factory_fixture(factory_class))
contribute_to_module(module, model_name, make_model_fixture(factory_name), add_args=deps)
make_fixture(
name=attr_name,
module=module,
func=attr_fixture,
value=value,
)
make_fixture(
name=factory_name,
module=module,
func=factory_fixture,
factory_class=factory_class,
)

make_fixture(
name=model_name,
module=module,
func=model_fixture,
args=deps,
factory_name=factory_name,
)
return factory_class


Expand All @@ -58,129 +102,49 @@ def get_deps(factory_class, parent_factory_class=None):
]


def make_model_fixture(factory_name):
@pytest.fixture
def model_fixture(request):
factory_class = request.getfuncargvalue(factory_name)
prefix = "".join((request.fixturename, SEPARATOR))
data = {}
def model_fixture(request, factory_name):
factory_class = request.getfuncargvalue(factory_name)
prefix = "".join((request.fixturename, SEPARATOR))
data = {}

for argname in request._fixturedef.argnames:
if argname.startswith(prefix):
data[argname[len(prefix):]] = request.getfuncargvalue(argname)
for argname in request._fixturedef.argnames:
if argname.startswith(prefix):
data[argname[len(prefix):]] = request.getfuncargvalue(argname)

class Factory(factory_class):
pass
class Factory(factory_class):
pass

related_factories = []
for attr, value in Factory._meta.postgen_declarations.items():
if isinstance(value, factory.RelatedFactory):
related_factories.append(value)
Factory._meta.postgen_declarations.pop(attr)
related_factories = []
for attr, value in dict(Factory._meta.postgen_declarations).items():
if isinstance(value, factory.RelatedFactory):
related_factories.append(value)
Factory._meta.postgen_declarations.pop(attr)

result = Factory(**data)
result = Factory(**data)

if related_factories:
request._fixturedef.cached_result = (result, 0, None)
request._fixturedefs[request.fixturename] = request._fixturedef
if related_factories:
request._fixturedef.cached_result = (result, 0, None)
request._fixturedefs[request.fixturename] = request._fixturedef

for related_factory in related_factories:
related_factory_class = related_factory.get_factory()
model_name = get_model_name(related_factory_class)
request.getfuncargvalue(model_name)
return result
for related_factory in related_factories:
related_factory_class = related_factory.get_factory()
model_name = get_model_name(related_factory_class)
request.getfuncargvalue(prefix + model_name)

return model_fixture
return result


def make_factory_fixture(factory_class):
@pytest.fixture
def factory_fixture():
return factory_class
return factory_fixture
def factory_fixture(request, factory_class):
return factory_class


def make_attr_fixture(value):
@pytest.fixture
def attr_fixture():
return value
return attr_fixture
def attr_fixture(request, value):
return value


def make_subfactory_fixture(factory_class):
def subfactory_fixture(request, factory_class):
fixture = inflection.underscore(factory_class._meta.model.__name__)

@pytest.fixture
def subfactory_fixture(request):
return request.getfuncargvalue(fixture)
return subfactory_fixture


def recreate_function(func, module=None, name=None, add_args=[], firstlineno=None):
"""Recreate a function, replacing some info.

:param func: Function object.
:param module: Module to contribute to.
:param add_args: Additional arguments to add to the function.

:return: Function copy.
"""
def get_code(func):
return func.__code__ if six.PY3 else func.func_code

def set_code(func, code):
if six.PY3:
func.__code__ = code
else:
func.func_code = code

argnames = [
"co_argcount", "co_nlocals", "co_stacksize", "co_flags", "co_code", "co_consts", "co_names",
"co_varnames", "co_filename", "co_name", "co_firstlineno", "co_lnotab", "co_freevars", "co_cellvars",
]
if six.PY3:
argnames.insert(1, "co_kwonlyargcount")

for arg in inspect.getargspec(func).args:
if arg in add_args:
add_args.remove(arg)

args = []
code = get_code(func)
for arg in argnames:
if module is not None and arg == "co_filename":
args.append(module.__file__)
elif name is not None and arg == "co_name":
args.append(name)
elif arg == "co_argcount":
args.append(getattr(code, arg) + len(add_args))
elif arg == "co_varnames":
co_varnames = getattr(code, arg)
args.append(co_varnames[:code.co_argcount] + tuple(add_args) + co_varnames[code.co_argcount:])
elif arg == "co_firstlineno":
args.append(firstlineno if firstlineno else 0)
else:
args.append(getattr(code, arg))

set_code(func, CodeType(*args))
if name is not None:
func.__name__ = name
return func


def contribute_to_module(module, name, func, add_args=[]):
"""Contribute a function to a module.

:param module: Module to contribute to.
:param name: Attribute name.
:param func: Function object.
:param add_args: Additional arguments to add to the function.

:return: New function copy contributed to the module
"""
func = recreate_function(func, module=module, add_args=add_args, name=name)
setattr(module, name, func)
return func
return request.getfuncargvalue(fixture)


def get_caller_module(depth=2):
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
"Programming Language :: Python :: 3"
] + [("Programming Language :: Python :: %s" % x) for x in "2.6 2.7 3.0 3.1 3.2 3.3 3.4".split()],
install_requires=[
"six",
"inflection",
"factory_boy",
],
Expand Down
5 changes: 1 addition & 4 deletions tests/test_factory_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ class Book(object):

"""Book model."""

editions = []

def __init__(self, name=None, price=None, author=None):
self.editions = []
self.name = name
self.price = price
self.author = author
Expand Down Expand Up @@ -79,7 +78,6 @@ def test_factory(book_factory):
assert book_factory == BookFactory


@pytest.mark.xfail
def test_model(book):
"""Test model fixture."""
assert book.name == "Alice in Wonderland"
Expand All @@ -105,7 +103,6 @@ def test_attr(book__name, book__price, author__name, edition__year):
@pytest.mark.parametrize("book__price", [1.0])
@pytest.mark.parametrize("author__name", ["Bill Gates"])
@pytest.mark.parametrize("edition__year", [2000])
@pytest.mark.xfail
def test_parametrized(book):
"""Test model factory fixture."""
assert book.name == "PyTest for Dummies"
Expand Down