Skip to content

Commit

Permalink
Merge pull request #2 from olegpidsadnyi/fix-related-factory
Browse files Browse the repository at this point in the history
Creation of the fixture functions and related factory test fix
  • Loading branch information
olegpidsadnyi committed Apr 19, 2015
2 parents d513492 + 5cfa650 commit 1e59132
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 129 deletions.
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

0 comments on commit 1e59132

Please sign in to comment.