summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml11
-rw-r--r--LICENSE2
-rw-r--r--Makefile36
-rw-r--r--README.rst160
-rw-r--r--dev_requirements.txt9
-rw-r--r--docs/changelog.rst138
-rw-r--r--docs/conf.py8
-rw-r--r--docs/examples.rst18
-rw-r--r--docs/fuzzy.rst39
-rw-r--r--docs/ideas.rst5
-rw-r--r--docs/introduction.rst41
-rw-r--r--docs/orms.rst223
-rw-r--r--docs/recipes.rst173
-rw-r--r--docs/reference.rst335
-rw-r--r--examples/Makefile9
-rw-r--r--examples/flask_alchemy/demoapp.py55
-rw-r--r--examples/flask_alchemy/demoapp_factories.py26
-rw-r--r--examples/flask_alchemy/requirements.txt2
-rw-r--r--examples/flask_alchemy/test_demoapp.py35
-rw-r--r--examples/requirements.txt1
-rw-r--r--factory/__init__.py19
-rw-r--r--factory/alchemy.py34
-rw-r--r--factory/base.py406
-rw-r--r--factory/compat.py10
-rw-r--r--factory/containers.py71
-rw-r--r--factory/declarations.py30
-rw-r--r--factory/django.py166
-rw-r--r--factory/faker.py101
-rw-r--r--factory/fuzzy.py53
-rw-r--r--factory/helpers.py7
-rw-r--r--factory/mogo.py13
-rw-r--r--factory/mongoengine.py14
-rw-r--r--factory/utils.py34
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py10
-rw-r--r--tests/__init__.py7
-rw-r--r--tests/alter_time.py2
-rw-r--r--tests/compat.py2
-rw-r--r--tests/cyclic/bar.py5
-rw-r--r--tests/cyclic/foo.py5
-rw-r--r--tests/cyclic/self_ref.py37
-rw-r--r--tests/djapp/models.py47
-rw-r--r--tests/djapp/settings.py6
-rw-r--r--tests/test_alchemy.py73
-rw-r--r--tests/test_base.py266
-rw-r--r--tests/test_containers.py105
-rw-r--r--tests/test_declarations.py17
-rw-r--r--tests/test_django.py391
-rw-r--r--tests/test_faker.py135
-rw-r--r--tests/test_fuzzy.py83
-rw-r--r--tests/test_helpers.py2
-rw-r--r--tests/test_mongoengine.py21
-rw-r--r--tests/test_using.py551
-rw-r--r--tests/test_utils.py16
-rw-r--r--tests/testdata/__init__.py2
-rw-r--r--tests/tools.py2
-rw-r--r--tests/utils.py2
-rw-r--r--tox.ini17
60 files changed, 3018 insertions, 1074 deletions
diff --git a/.gitignore b/.gitignore
index b4d25fc..5437c43 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
# Build-related files
docs/_build/
+auto_dev_requirements*.txt
.coverage
.tox
*.egg-info
diff --git a/.travis.yml b/.travis.yml
index 2bfb978..ff805b0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,16 @@
language: python
python:
- - "2.6"
- "2.7"
- - "3.2"
- - "3.3"
+ - "3.4"
+ - "3.5"
- "pypy"
script:
- - python setup.py test
+ - SKIP_MONGOENGINE=1 python setup.py test
install:
- - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi
- - pip install Django sqlalchemy --use-mirrors
- - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi
+ - make install-deps
notifications:
email: false
diff --git a/LICENSE b/LICENSE
index 620dc61..d009218 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,5 @@
Copyright (c) 2010 Mark Sandstrom
-Copyright (c) 2011-2013 Raphaël Barrois
+Copyright (c) 2011-2015 Raphaël Barrois
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
index bb0428b..da8ac88 100644
--- a/Makefile
+++ b/Makefile
@@ -1,30 +1,60 @@
PACKAGE=factory
TESTS_DIR=tests
DOC_DIR=docs
+EXAMPLES_DIR=examples
# Use current python binary instead of system default.
COVERAGE = python $(shell which coverage)
+# Dependencies
+DJANGO ?= 1.9
+NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
+
+ALCHEMY ?= 1.0
+NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
+
+MONGOENGINE ?= 0.10
+NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
+
+REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt
+EXAMPLES_REQ_FILES = $(shell find $(EXAMPLES_DIR) -name requirements.txt)
+
all: default
default:
+install-deps: $(REQ_FILE)
+ pip install --upgrade pip setuptools
+ pip install --upgrade -r $<
+ pip freeze
+
+$(REQ_FILE): dev_requirements.txt requirements.txt $(EXAMPLES_REQ_FILES)
+ grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@
+ echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@
+ echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@
+ echo "mongoengine>=$(MONGOENGINE),<$(NEXT_MONGOENGINE)" >> $@
+
+
clean:
find . -type f -name '*.pyc' -delete
find . -type f -path '*/__pycache__/*' -delete
find . -type d -empty -delete
+ @rm -f auto_dev_requirements_*
@rm -rf tmp_test/
-test:
+test: install-deps example-test
python -W default setup.py test
+example-test:
+ $(MAKE) -C $(EXAMPLES_DIR) test
+
pylint:
pylint --rcfile=.pylintrc --report=no $(PACKAGE)/
-coverage:
+coverage: install-deps
$(COVERAGE) erase
$(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test
$(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py"
@@ -34,4 +64,4 @@ doc:
$(MAKE) -C $(DOC_DIR) html
-.PHONY: all default clean coverage doc pylint test
+.PHONY: all default clean coverage doc install-deps pylint test
diff --git a/README.rst b/README.rst
index 0371b28..8914c62 100644
--- a/README.rst
+++ b/README.rst
@@ -1,25 +1,78 @@
factory_boy
===========
+.. image:: https://pypip.in/version/factory_boy/badge.svg
+ :target: http://factoryboy.readthedocs.org/en/latest/changelog.html
+ :alt: Latest Version
+
+.. image:: https://pypip.in/py_versions/factory_boy/badge.svg
+ :target: https://pypi.python.org/pypi/factory_boy/
+ :alt: Supported Python versions
+
+.. image:: https://pypip.in/wheel/factory_boy/badge.svg
+ :target: https://pypi.python.org/pypi/factory_boy/
+ :alt: Wheel status
+
.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master
:target: http://travis-ci.org/rbarrois/factory_boy/
factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_.
-Its features include:
+As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures
+with easy-to-use factories for complex object.
-- Straightforward syntax
-- Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects)
-- Powerful helpers for common cases (sequences, sub-factories, reverse dependencies, circular factories, ...)
+Instead of building an exhaustive test setup with every possible combination of corner cases,
+``factory_boy`` allows you to use objects customized for the current test,
+while only declaring the test-specific fields:
+
+.. code-block:: python
+
+ class FooTests(unittest.TestCase):
+
+ def test_with_factory_boy(self):
+ # We need a 200€, paid order, shipping to australia, for a VIP customer
+ order = OrderFactory(
+ amount=200,
+ status='PAID',
+ customer__is_vip=True,
+ address__country='AU',
+ )
+ # Run the tests here
+
+ def test_without_factory_boy(self):
+ address = Address(
+ street="42 fubar street",
+ zipcode="42Z42",
+ city="Sydney",
+ country="AU",
+ )
+ customer = Customer(
+ first_name="John",
+ last_name="Doe",
+ phone="+1234",
+ email="john.doe@example.org",
+ active=True,
+ is_vip=True,
+ address=address,
+ )
+ # etc.
+
+factory_boy is designed to work well with various ORMs (Django, Mogo, SQLAlchemy),
+and can easily be extended for other libraries.
+
+Its main features include:
+
+- Straightforward declarative syntax
+- Chaining factory calls while retaining the global context
+- Support for multiple build strategies (saved/unsaved instances, stubbed objects)
- Multiple factories per class support, including inheritance
-- Support for various ORMs (currently Django, Mogo, SQLAlchemy)
Links
-----
* Documentation: http://factoryboy.readthedocs.org/
-* Official repository: https://github.com/rbarrois/factory_boy
+* Repository: https://github.com/rbarrois/factory_boy
* Package: https://pypi.python.org/pypi/factory_boy/
factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library.
@@ -53,7 +106,8 @@ Usage
Defining factories
""""""""""""""""""
-Factories declare a set of attributes used to instantiate an object. The class of the object must be defined in the FACTORY_FOR attribute:
+Factories declare a set of attributes used to instantiate an object.
+The class of the object must be defined in the ``model`` field of a ``class Meta:`` attribute:
.. code-block:: python
@@ -61,7 +115,8 @@ Factories declare a set of attributes used to instantiate an object. The class o
from . import models
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
first_name = 'John'
last_name = 'Doe'
@@ -69,7 +124,8 @@ Factories declare a set of attributes used to instantiate an object. The class o
# Another, different, factory for the same object
class AdminFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
first_name = 'Admin'
last_name = 'User'
@@ -79,7 +135,7 @@ Factories declare a set of attributes used to instantiate an object. The class o
Using factories
"""""""""""""""
-factory_boy supports several different build strategies: build, create, attributes and stub:
+factory_boy supports several different build strategies: build, create, and stub:
.. code-block:: python
@@ -89,8 +145,8 @@ factory_boy supports several different build strategies: build, create, attribut
# Returns a saved User instance
user = UserFactory.create()
- # Returns a dict of attributes that can be used to build a User instance
- attributes = UserFactory.attributes()
+ # Returns a stub object (just a bunch of attributes)
+ obj = UserFactory.stub()
You can use the Factory class as a shortcut for the default build strategy:
@@ -111,6 +167,42 @@ No matter which strategy is used, it's possible to override the defined attribut
"Joe"
+It is also possible to create a bunch of objects in a single call:
+
+.. code-block:: pycon
+
+ >>> users = UserFactory.build_batch(10, first_name="Joe")
+ >>> len(users)
+ 10
+ >>> [user.first_name for user in users]
+ ["Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe"]
+
+
+Realistic, random values
+""""""""""""""""""""""""
+
+Demos look better with random yet realistic values; and those realistic values can also help discover bugs.
+For this, factory_boy relies on the excellent `fake-factory <https://pypi.python.org/pypi/fake-factory>`_ library:
+
+.. code-block:: python
+
+ class RandomUserFactory(factory.Factory):
+ class Meta:
+ model = models.User
+
+ first_name = factory.Faker('first_name')
+ last_name = factory.Faker('last_name')
+
+.. code-block:: pycon
+
+ >>> UserFactory()
+ <User: Lucy Murray>
+
+
+.. note:: Use of fully randomized data in tests is quickly a problem for reproducing broken builds.
+ To that purpose, factory_boy provides helpers to handle the random seeds it uses.
+
+
Lazy Attributes
"""""""""""""""
@@ -123,7 +215,9 @@ These "lazy" attributes can be added as follows:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
+
first_name = 'Joe'
last_name = 'Blow'
email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower())
@@ -142,7 +236,9 @@ Unique values in a specific format (for example, e-mail addresses) can be genera
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
+
email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n))
>>> UserFactory().email
@@ -160,7 +256,9 @@ This is handled by the ``SubFactory`` helper:
.. code-block:: python
class PostFactory(factory.Factory):
- FACTORY_FOR = models.Post
+ class Meta:
+ model = models.Post
+
author = factory.SubFactory(UserFactory)
@@ -190,7 +288,7 @@ Debugging factory_boy
Debugging factory_boy can be rather complex due to the long chains of calls.
Detailed logging is available through the ``factory`` logger.
-A helper, :meth:`factory.debug()`, is available to ease debugging:
+A helper, `factory.debug()`, is available to ease debugging:
.. code-block:: python
@@ -221,12 +319,12 @@ This will yield messages similar to those (artificial indentation):
ORM Support
"""""""""""
-factory_boy has specific support for a few ORMs, through specific :class:`~factory.Factory` subclasses:
+factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses:
-* Django, with :class:`~factory.django.DjangoModelFactory`
-* Mogo, with :class:`~factory.mogo.MogoFactory`
-* MongoEngine, with :class:`~factory.mongoengine.MongoEngineFactory`
-* SQLAlchemy, with :class:`~factory.alchemy.SQLAlchemyModelFactory`
+* Django, with ``factory.django.DjangoModelFactory``
+* Mogo, with ``factory.mogo.MogoFactory``
+* MongoEngine, with ``factory.mongoengine.MongoEngineFactory``
+* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory``
Contributing
------------
@@ -239,20 +337,28 @@ All pull request should pass the test suite, which can be launched simply with:
.. code-block:: sh
- $ python setup.py test
+ $ make test
-.. note::
- Running test requires the unittest2 (standard in Python 2.7+) and mock libraries.
+In order to test coverage, please use:
+.. code-block:: sh
-In order to test coverage, please use:
+ $ make coverage
+
+
+To test with a specific framework version, you may use:
.. code-block:: sh
- $ pip install coverage
- $ coverage erase; coverage run --branch setup.py test; coverage report
+ $ make DJANGO=1.7 test
+
+Valid options are:
+
+* ``DJANGO`` for ``Django``
+* ``MONGOENGINE`` for ``mongoengine``
+* ``ALCHEMY`` for ``SQLAlchemy``
Contents, indices and tables
diff --git a/dev_requirements.txt b/dev_requirements.txt
index bdc23d0..c78aa9d 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -1,6 +1,13 @@
+-r requirements.txt
+-r examples/requirements.txt
+
coverage
Django
Pillow
-sqlalchemy
+SQLAlchemy
mongoengine
mock
+wheel
+
+Sphinx
+sphinx_rtd_theme
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 47d1139..fa542f4 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,17 +1,149 @@
ChangeLog
=========
+.. _v2.6.0:
+
+2.6.0 (2015-10-20)
+------------------
+
+*New:*
+
+ - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`)
+ - Add support for random-yet-realistic values through `fake-factory <https://pypi.python.org/pypi/fake-factory>`_,
+ through the :class:`factory.Faker` class.
+ - :class:`factory.Iterator` no longer begins iteration of its argument at import time,
+ thus allowing to pass in a lazy iterator such as a Django queryset
+ (i.e ``factory.Iterator(models.MyThingy.objects.all())``).
+ - Simplify imports for ORM layers, now available through a simple ``factory`` import,
+ at ``factory.alchemy.SQLAlchemyModelFactory`` / ``factory.django.DjangoModelFactory`` / ``factory.mongoengine.MongoEngineFactory``.
+
+*Bugfix:*
+
+ - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models.
+ - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching
+ - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required
+ - :issue:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago.
+
+.. _v2.5.2:
+
+2.5.2 (2015-04-21)
+------------------
+
+*Bugfix:*
+
+ - Add support for Django 1.7/1.8
+ - Add support for mongoengine>=0.9.0 / pymongo>=2.1
+
+.. _v2.5.1:
+
+2.5.1 (2015-03-27)
+------------------
+
+*Bugfix:*
+
+ - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`)
+ - Allow passing declarations (e.g :class:`~factory.Sequence`) as parameters to :class:`~factory.django.FileField`
+ and :class:`~factory.django.ImageField`.
+
+.. _v2.5.0:
+
+2.5.0 (2015-03-26)
+------------------
+
+*New:*
+
+ - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`).
+ - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`).
+ - Support non-default databases at the factory level (see :issue:`171`)
+ - Make :class:`factory.django.FileField` and :class:`factory.django.ImageField` non-post_generation, i.e normal fields also available in ``save()`` (see :issue:`141`).
+
+*Bugfix:*
+
+ - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`).
+ - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`).
+
+
+*Deprecation:*
+
+ - Remove deprecated features from :ref:`v2.4.0`
+ - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy;
+ this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. See https://github.com/rbarrois/factory_boy/commit/13d310f for technical details.
+
+.. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature.
+ This could trigger some bugs when tests expected a non-zero sequence reference.
+
+Upgrading
+"""""""""
+
+.. warning:: Version 2.5.0 removes features that were marked as deprecated in :ref:`v2.4.0 <v2.4.0>`.
+
+All ``FACTORY_*``-style attributes are now declared in a ``class Meta:`` section:
+
+.. code-block:: python
+
+ # Old-style, deprecated
+ class MyFactory(factory.Factory):
+ FACTORY_FOR = models.MyModel
+ FACTORY_HIDDEN_ARGS = ['a', 'b', 'c']
+
+ # New-style
+ class MyFactory(factory.Factory):
+ class Meta:
+ model = models.MyModel
+ exclude = ['a', 'b', 'c']
+
+A simple shell command to upgrade the code would be:
+
+.. code-block:: sh
+
+ # sed -i: inplace update
+ # grep -l: only file names, not matching lines
+ sed -i 's/FACTORY_FOR =/class Meta:\n model =/' $(grep -l FACTORY_FOR $(find . -name '*.py'))
+
+This takes care of all ``FACTORY_FOR`` occurences; the files containing other attributes to rename can be found with ``grep -R FACTORY .``
+
+
+.. _v2.4.1:
+
+2.4.1 (2014-06-23)
+------------------
+
+*Bugfix:*
+
+ - Fix overriding deeply inherited attributes (set in one factory, overridden in a subclass, used in a sub-sub-class).
.. _v2.4.0:
-2.4.0 (master)
---------------
+2.4.0 (2014-06-21)
+------------------
*New:*
- Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov <https://github.com/ilya-pirogov>`_ (:issue:`120`)
- - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov <https://github.com>`_ (:issue:`122`)
+ - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov <https://github.com/ilya-pirogov>`_ (:issue:`122`)
- Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`)
+ - Declare target model and other non-declaration fields in a ``class Meta`` section.
+
+*Deprecation:*
+
+ - Use of ``FACTORY_FOR`` and other ``FACTORY`` class-level attributes is deprecated and will be removed in 2.5.
+ Those attributes should now declared within the :class:`class Meta <factory.FactoryOptions>` attribute:
+
+ For :class:`factory.Factory`:
+
+ * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model`
+ * Rename :attr:`~factory.Factory.ABSTRACT_FACTORY` to :attr:`~factory.FactoryOptions.abstract`
+ * Rename :attr:`~factory.Factory.FACTORY_STRATEGY` to :attr:`~factory.FactoryOptions.strategy`
+ * Rename :attr:`~factory.Factory.FACTORY_ARG_PARAMETERS` to :attr:`~factory.FactoryOptions.inline_args`
+ * Rename :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` to :attr:`~factory.FactoryOptions.exclude`
+
+ For :class:`factory.django.DjangoModelFactory`:
+
+ * Rename :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` to :attr:`~factory.django.DjangoOptions.django_get_or_create`
+
+ For :class:`factory.alchemy.SQLAlchemyModelFactory`:
+
+ * Rename :attr:`~factory.alchemy.SQLAlchemyModelFactory.FACTORY_SESSION` to :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session`
.. _v2.3.1:
diff --git a/docs/conf.py b/docs/conf.py
index 4f76d45..d5b86f4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -51,7 +51,7 @@ master_doc = 'index'
# General information about the project.
project = u'Factory Boy'
-copyright = u'2011-2013, Raphaël Barrois, Mark Sandstrom'
+copyright = u'2011-2015, Raphaël Barrois, Mark Sandstrom'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -114,7 +114,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'default'
+html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -247,7 +247,7 @@ intersphinx_mapping = {
'http://docs.djangoproject.com/en/dev/_objects/',
),
'sqlalchemy': (
- 'http://docs.sqlalchemy.org/en/rel_0_8/',
- 'http://docs.sqlalchemy.org/en/rel_0_8/objects.inv',
+ 'http://docs.sqlalchemy.org/en/rel_0_9/',
+ 'http://docs.sqlalchemy.org/en/rel_0_9/objects.inv',
),
}
diff --git a/docs/examples.rst b/docs/examples.rst
index aab990a..e7f6057 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -56,14 +56,16 @@ And now, we'll define the related factories:
class AccountFactory(factory.Factory):
- FACTORY_FOR = objects.Account
+ class Meta:
+ model = objects.Account
username = factory.Sequence(lambda n: 'john%s' % n)
email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username)
class ProfileFactory(factory.Factory):
- FACTORY_FOR = objects.Profile
+ class Meta:
+ model = objects.Profile
account = factory.SubFactory(AccountFactory)
gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE])
@@ -112,12 +114,9 @@ We can now use our factories, for tests:
def test_get_profile_stats(self):
profiles = []
- for _ in xrange(4):
- profiles.append(factories.ProfileFactory())
- for _ in xrange(2):
- profiles.append(factories.FemaleProfileFactory())
- for _ in xrange(2):
- profiles.append(factories.ProfileFactory(planet='Tatooine'))
+ profiles.extend(factories.ProfileFactory.create_batch(4))
+ profiles.extend(factories.FemaleProfileFactory.create_batch(2))
+ profiles.extend(factories.ProfileFactory.create_batch(2, planet="Tatooine"))
stats = business_logic.profile_stats(profiles)
self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets)
@@ -131,8 +130,7 @@ Or for fixtures:
from . import factories
def make_objects():
- for _ in xrange(50):
- factories.ProfileFactory()
+ factories.ProfileFactory.create_batch(size=50)
# Let's create a few, known objects.
factories.ProfileFactory(
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
index 1480419..6b06608 100644
--- a/docs/fuzzy.rst
+++ b/docs/fuzzy.rst
@@ -8,6 +8,8 @@ Some tests may be interested in testing with fuzzy, random values.
This is handled by the :mod:`factory.fuzzy` module, which provides a few
random declarations.
+.. note:: Use ``import factory.fuzzy`` to load this module.
+
FuzzyAttribute
--------------
@@ -62,8 +64,11 @@ FuzzyChoice
The :class:`FuzzyChoice` fuzzer yields random choices from the given
iterable.
- .. note:: The passed in :attr:`choices` will be converted into a list at
- declaration time.
+ .. note:: The passed in :attr:`choices` will be converted into a list upon
+ first use, not at declaration time.
+
+ This allows passing in, for instance, a Django queryset that will
+ only hit the database during the database, not at import time.
.. attribute:: choices
@@ -338,3 +343,33 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its
The method responsible for generating random values.
*Must* be overridden in subclasses.
+
+
+Managing randomness
+-------------------
+
+Using :mod:`random` in factories allows to "fuzz" a program efficiently.
+However, it's sometimes required to *reproduce* a failing test.
+
+:mod:`factory.fuzzy` uses a separate instance of :class:`random.Random`,
+and provides a few helpers for this:
+
+.. method:: get_random_state()
+
+ Call :meth:`get_random_state` to retrieve the random generator's current
+ state.
+
+.. method:: set_random_state(state)
+
+ Use :meth:`set_random_state` to set a custom state into the random generator
+ (fetched from :meth:`get_random_state` in a previous run, for instance)
+
+.. method:: reseed_random(seed)
+
+ The :meth:`reseed_random` function allows to load a chosen seed into the random generator.
+
+
+Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD**
+use :obj:`factory.fuzzy._random` as a randomness source; this ensures that
+data they generate can be regenerated using the simple state from
+:meth:`get_random_state`.
diff --git a/docs/ideas.rst b/docs/ideas.rst
index 914e640..6e3962d 100644
--- a/docs/ideas.rst
+++ b/docs/ideas.rst
@@ -4,5 +4,6 @@ Ideas
This is a list of future features that may be incorporated into factory_boy:
-* **A 'options' attribute**: instead of adding more class-level constants, use a django-style ``class Meta`` Factory attribute with all options there
-
+* When a :class:`Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere
+* Define a proper set of rules for the support of third-party ORMs
+* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``)
diff --git a/docs/introduction.rst b/docs/introduction.rst
index 86e2046..d00154d 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -18,10 +18,11 @@ Basic usage
-----------
-Factories declare a set of attributes used to instantiate an object, whose class is defined in the FACTORY_FOR attribute:
+Factories declare a set of attributes used to instantiate an object, whose class is defined in the ``class Meta``'s ``model`` attribute:
- Subclass ``factory.Factory`` (or a more suitable subclass)
-- Set its ``FACTORY_FOR`` attribute to the target class
+- Add a ``class Meta:`` block
+- Set its ``model`` attribute to the target class
- Add defaults for keyword args to pass to the associated class' ``__init__`` method
@@ -31,7 +32,8 @@ Factories declare a set of attributes used to instantiate an object, whose class
from . import base
class UserFactory(factory.Factory):
- FACTORY_FOR = base.User
+ class Meta:
+ model = base.User
firstname = "John"
lastname = "Doe"
@@ -56,7 +58,8 @@ A given class may be associated to many :class:`~factory.Factory` subclasses:
.. code-block:: python
class EnglishUserFactory(factory.Factory):
- FACTORY_FOR = base.User
+ class Meta:
+ model = base.User
firstname = "John"
lastname = "Doe"
@@ -64,7 +67,8 @@ A given class may be associated to many :class:`~factory.Factory` subclasses:
class FrenchUserFactory(factory.Factory):
- FACTORY_FOR = base.User
+ class Meta:
+ model = base.User
firstname = "Jean"
lastname = "Dupont"
@@ -88,7 +92,8 @@ This is achieved with the :class:`~factory.Sequence` declaration:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
username = factory.Sequence(lambda n: 'user%d' % n)
@@ -104,7 +109,8 @@ This is achieved with the :class:`~factory.Sequence` declaration:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
@factory.sequence
def username(n):
@@ -121,7 +127,8 @@ taking the object being built and returning the value for the field:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
username = factory.Sequence(lambda n: 'user%d' % n)
email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username)
@@ -146,7 +153,8 @@ taking the object being built and returning the value for the field:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
username = factory.Sequence(lambda n: 'user%d' % n)
@@ -168,7 +176,8 @@ and update them with its own declarations:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = base.User
+ class Meta:
+ model = base.User
firstname = "John"
lastname = "Doe"
@@ -209,13 +218,14 @@ Non-kwarg arguments
Some classes take a few, non-kwarg arguments first.
-This is handled by the :data:`~factory.Factory.FACTORY_ARG_PARAMETERS` attribute:
+This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute:
.. code-block:: python
class MyFactory(factory.Factory):
- FACTORY_FOR = MyClass
- FACTORY_ARG_PARAMETERS = ('x', 'y')
+ class Meta:
+ model = MyClass
+ inline_args = ('x', 'y')
x = 1
y = 2
@@ -251,7 +261,8 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the
.. code-block:: python
class MyFactory(factory.Factory):
- FACTORY_FOR = MyClass
+ class Meta:
+ model = MyClass
.. code-block:: pycon
@@ -265,6 +276,6 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the
<MyClass: X (saved)>
-The default strategy can be changed by setting the class-level :attr:`~factory.Factory.FACTORY_STRATEGY` attribute.
+The default strategy can be changed by setting the ``class Meta`` :attr:`~factory.FactoryOptions.strategy` attribute.
diff --git a/docs/orms.rst b/docs/orms.rst
index c893cac..af20917 100644
--- a/docs/orms.rst
+++ b/docs/orms.rst
@@ -15,7 +15,7 @@ Django
The first versions of factory_boy were designed specifically for Django,
-but the library has now evolved to be framework-independant.
+but the library has now evolved to be framework-independent.
Most features should thus feel quite familiar to Django users.
@@ -32,15 +32,36 @@ All factories for a Django :class:`~django.db.models.Model` should use the
This class provides the following features:
- * The :attr:`~factory.Factory.FACTORY_FOR` attribute also supports the ``'app.Model'``
+ * The :attr:`~factory.FactoryOptions.model` attribute also supports the ``'app.Model'``
syntax
* :func:`~factory.Factory.create()` uses :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>`
- * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value
* When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration`
attributes, the base object will be :meth:`saved <django.db.models.Model.save>`
once all post-generation hooks have run.
- .. attribute:: FACTORY_DJANGO_GET_OR_CREATE
+
+.. note:: With Django versions 1.8.0 to 1.8.3, it was no longer possible to call ``.build()``
+ on a factory if this factory used a :class:`~factory.SubFactory` pointing
+ to another model: Django refused to set a :class:`~djang.db.models.ForeignKey`
+ to an unsaved :class:`~django.db.models.Model` instance.
+
+ See https://code.djangoproject.com/ticket/10811 and https://code.djangoproject.com/ticket/25160 for details.
+
+
+.. class:: DjangoOptions(factory.base.FactoryOptions)
+
+ The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters:
+
+ .. attribute:: database
+
+ .. versionadded:: 2.5.0
+
+ All queries to the related model will be routed to the given database.
+ It defaults to ``'default'``.
+
+ .. attribute:: django_get_or_create
+
+ .. versionadded:: 2.4.0
Fields whose name are passed in this list will be used to perform a
:meth:`Model.objects.get_or_create() <django.db.models.query.QuerySet.get_or_create>`
@@ -49,8 +70,9 @@ All factories for a Django :class:`~django.db.models.Model` should use the
.. code-block:: python
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = 'myapp.User' # Equivalent to ``FACTORY_FOR = myapp.models.User``
- FACTORY_DJANGO_GET_OR_CREATE = ('username',)
+ class Meta:
+ model = 'myapp.User' # Equivalent to ``model = myapp.models.User``
+ django_get_or_create = ('username',)
username = 'john'
@@ -80,11 +102,13 @@ All factories for a Django :class:`~django.db.models.Model` should use the
.. code-block:: python
class MyAbstractModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.MyAbstractModel
- ABSTRACT_FACTORY = True
+ class Meta:
+ model = models.MyAbstractModel
+ abstract = True
class MyConcreteModelFactory(MyAbstractModelFactory):
- FACTORY_FOR = models.MyConcreteModel
+ class Meta:
+ model = models.MyConcreteModel
Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model.
@@ -102,7 +126,7 @@ Extra fields
:param str from_path: Use data from the file located at ``from_path``,
and keep its filename
:param file from_file: Use the contents of the provided file object; use its filename
- if available
+ if available, unless ``filename`` is also provided.
:param bytes data: Use the provided bytes as file contents
:param str filename: The filename for the FileField
@@ -112,7 +136,8 @@ Extra fields
.. code-block:: python
class MyFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.MyModel
+ class Meta:
+ model = models.MyModel
the_file = factory.django.FileField(filename='the_file.dat')
@@ -149,7 +174,8 @@ Extra fields
.. code-block:: python
class MyFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.MyModel
+ class Meta:
+ model = models.MyModel
the_image = factory.django.ImageField(color='blue')
@@ -188,7 +214,8 @@ To work around this problem, use the :meth:`mute_signals()` decorator/context ma
@factory.django.mute_signals(signals.pre_save, signals.post_save)
class FooFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.Foo
+ class Meta:
+ model = models.Foo
# ...
@@ -241,11 +268,39 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin
* :func:`~factory.Factory.create()` builds an instance through ``__init__`` then
saves it.
- .. note:: If the :attr:`associated class <factory.Factory.FACTORY_FOR>` is a :class:`mongoengine.EmbeddedDocument`,
+ .. note:: If the :attr:`associated class <factory.FactoryOptions.model` is a :class:`mongoengine.EmbeddedDocument`,
the :meth:`~MongoEngineFactory.create` function won't "save" it, since this wouldn't make sense.
This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document.
+A minimalist example:
+
+.. code-block:: python
+
+ import mongoengine
+
+ class Address(mongoengine.EmbeddedDocument):
+ street = mongoengine.StringField()
+
+ class Person(mongoengine.Document):
+ name = mongoengine.StringField()
+ address = mongoengine.EmbeddedDocumentField(Address)
+
+ import factory
+
+ class AddressFactory(factory.mongoengine.MongoEngineFactory):
+ class Meta:
+ model = Address
+
+ street = factory.Sequence(lambda n: 'street%d' % n)
+
+ class PersonFactory(factory.mongoengine.MongoEngineFactory):
+ class Meta:
+ model = Person
+
+ name = factory.Sequence(lambda n: 'name%d' % n)
+ address = factory.SubFactory(AddressFactory)
+
SQLAlchemy
----------
@@ -255,7 +310,7 @@ SQLAlchemy
Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class.
-To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_SESSION" class attribute.
+To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:`Meta.sqlalchemy_session <SQLAlchemyOptions.sqlalchemy_session>` attribute.
.. _SQLAlchemy: http://www.sqlalchemy.org/
@@ -266,13 +321,23 @@ To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_S
This class provides the following features:
* :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add`
- * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value
- .. attribute:: FACTORY_SESSION
- Fields whose SQLAlchemy session object are passed will be used to communicate with the database
+.. class:: SQLAlchemyOptions(factory.base.FactoryOptions)
+
+ In addition to the usual parameters available in :class:`class Meta <factory.base.FactoryOptions>`,
+ a :class:`SQLAlchemyModelFactory` also supports the following settings:
+
+ .. attribute:: sqlalchemy_session
-A (very) simple exemple:
+ SQLAlchemy session to use to communicate with the database when creating
+ an object through this :class:`SQLAlchemyModelFactory`.
+
+ .. attribute:: force_flush
+
+ Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`.
+
+A (very) simple example:
.. code-block:: python
@@ -280,9 +345,8 @@ A (very) simple exemple:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker
- session = scoped_session(sessionmaker())
engine = create_engine('sqlite://')
- session.configure(bind=engine)
+ session = scoped_session(sessionmaker(bind=engine))
Base = declarative_base()
@@ -295,10 +359,12 @@ A (very) simple exemple:
Base.metadata.create_all(engine)
+ import factory
- class UserFactory(SQLAlchemyModelFactory):
- FACTORY_FOR = User
- FACTORY_SESSION = session # the SQLAlchemy session object
+ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
+ class Meta:
+ model = User
+ sqlalchemy_session = session # the SQLAlchemy session object
id = factory.Sequence(lambda n: n)
name = factory.Sequence(lambda n: u'User %d' % n)
@@ -311,3 +377,112 @@ A (very) simple exemple:
<User: User 1>
>>> session.query(User).all()
[<User: User 1>]
+
+
+Managing sessions
+"""""""""""""""""
+
+Since `SQLAlchemy`_ is a general purpose library,
+there is no "global" session management system.
+
+The most common pattern when working with unit tests and ``factory_boy``
+is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`:
+
+* The test runner configures some project-wide :class:`~sqlalchemy.orm.scoping.scoped_session`
+* Each :class:`~SQLAlchemyModelFactory` subclass uses this
+ :class:`~sqlalchemy.orm.scoping.scoped_session` as its :attr:`~SQLAlchemyOptions.sqlalchemy_session`
+* The :meth:`~unittest.TestCase.tearDown` method of tests calls
+ :meth:`Session.remove <sqlalchemy.orm.scoping.scoped_session.remove>`
+ to reset the session.
+
+.. note:: See the excellent :ref:`SQLAlchemy guide on scoped_session <sqlalchemy:unitofwork_contextual>`
+ for details of :class:`~sqlalchemy.orm.scoping.scoped_session`'s usage.
+
+ The basic idea is that declarative parts of the code (including factories)
+ need a simple way to access the "current session",
+ but that session will only be created and configured at a later point.
+
+ The :class:`~sqlalchemy.orm.scoping.scoped_session` handles this,
+ by virtue of only creating the session when a query is sent to the database.
+
+
+Here is an example layout:
+
+- A global (test-only?) file holds the :class:`~sqlalchemy.orm.scoping.scoped_session`:
+
+.. code-block:: python
+
+ # myprojet/test/common.py
+
+ from sqlalchemy import orm
+ Session = orm.scoped_session(orm.sessionmaker())
+
+
+- All factory access it:
+
+.. code-block:: python
+
+ # myproject/factories.py
+
+ import factory
+ import factory.alchemy
+
+ from . import models
+ from .test import common
+
+ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
+ class Meta:
+ model = models.User
+
+ # Use the not-so-global scoped_session
+ # Warning: DO NOT USE common.Session()!
+ sqlalchemy_session = common.Session
+
+ name = factory.Sequence(lambda n: "User %d" % n)
+
+
+- The test runner configures the :class:`~sqlalchemy.orm.scoping.scoped_session` when it starts:
+
+.. code-block:: python
+
+ # myproject/test/runtests.py
+
+ import sqlalchemy
+
+ from . import common
+
+ def runtests():
+ engine = sqlalchemy.create_engine('sqlite://')
+
+ # It's a scoped_session, and now is the time to configure it.
+ common.Session.configure(bind=engine)
+
+ run_the_tests
+
+
+- :class:`test cases <unittest.TestCase>` use this ``scoped_session``,
+ and clear it after each test (for isolation):
+
+.. code-block:: python
+
+ # myproject/test/test_stuff.py
+
+ import unittest
+
+ from . import common
+
+ class MyTest(unittest.TestCase):
+
+ def setUp(self):
+ # Prepare a new, clean session
+ self.session = common.Session()
+
+ def test_something(self):
+ u = factories.UserFactory()
+ self.assertEqual([u], self.session.query(User).all())
+
+ def tearDown(self):
+ # Rollback the session => no changes to the database
+ self.session.rollback()
+ # Remove it, so that the next test gets a new Session()
+ common.Session.remove()
diff --git a/docs/recipes.rst b/docs/recipes.rst
index 7a6bf23..df86bac 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -26,12 +26,36 @@ use the :class:`~factory.SubFactory` declaration:
from . import models
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
first_name = factory.Sequence(lambda n: "Agent %03d" % n)
group = factory.SubFactory(GroupFactory)
+Choosing from a populated table
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If the target of the :class:`~django.db.models.ForeignKey` should be
+chosen from a pre-populated table
+(e.g :class:`django.contrib.contenttypes.models.ContentType`),
+simply use a :class:`factory.Iterator` on the chosen queryset:
+
+.. code-block:: python
+
+ import factory, factory.django
+ from . import models
+
+ class UserFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.User
+
+ language = factory.Iterator(models.Language.objects.all())
+
+Here, ``models.Language.objects.all()`` won't be evaluated until the
+first call to ``UserFactory``; thus avoiding DB queries at import time.
+
+
Reverse dependencies (reverse ForeignKey)
-----------------------------------------
@@ -53,7 +77,8 @@ use a :class:`~factory.RelatedFactory` declaration:
# factories.py
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE)
@@ -75,7 +100,8 @@ factory_boy allows to define attributes of such profiles dynamically when creati
.. code-block:: python
class ProfileFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = my_models.Profile
+ class Meta:
+ model = my_models.Profile
title = 'Dr'
# We pass in profile=None to prevent UserFactory from creating another profile
@@ -83,7 +109,8 @@ factory_boy allows to define attributes of such profiles dynamically when creati
user = factory.SubFactory('app.factories.UserFactory', profile=None)
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = auth_models.User
+ class Meta:
+ model = auth_models.User
username = factory.Sequence(lambda n: "user_%d" % n)
@@ -121,8 +148,8 @@ factory_boy related factories.
method.
-Simple ManyToMany
------------------
+Simple Many-to-many relationship
+--------------------------------
Building the adequate link between two models depends heavily on the use case;
factory_boy doesn't provide a "all in one tools" as for :class:`~factory.SubFactory`
@@ -140,17 +167,19 @@ hook:
class User(models.Model):
name = models.CharField()
- groups = models.ManyToMany(Group)
+ groups = models.ManyToManyField(Group)
# factories.py
class GroupFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.Group
+ class Meta:
+ model = models.Group
name = factory.Sequence(lambda n: "Group #%s" % n)
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
name = "John Doe"
@@ -175,8 +204,8 @@ the ``groups`` declaration will add passed in groups to the set of groups for th
user.
-ManyToMany with a 'through'
----------------------------
+Many-to-many relation with a 'through'
+--------------------------------------
If only one link is required, this can be simply performed with a :class:`RelatedFactory`.
@@ -190,7 +219,7 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations:
class Group(models.Model):
name = models.CharField()
- members = models.ManyToMany(User, through='GroupLevel')
+ members = models.ManyToManyField(User, through='GroupLevel')
class GroupLevel(models.Model):
user = models.ForeignKey(User)
@@ -200,17 +229,20 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations:
# factories.py
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
name = "John Doe"
class GroupFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.Group
+ class Meta:
+ model = models.Group
name = "Admins"
class GroupLevelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.GroupLevel
+ class Meta:
+ model = models.GroupLevel
user = factory.SubFactory(UserFactory)
group = factory.SubFactory(GroupFactory)
@@ -273,20 +305,23 @@ Here, we want:
# factories.py
class CountryFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.Country
+ class Meta:
+ model = models.Country
name = factory.Iterator(["France", "Italy", "Spain"])
lang = factory.Iterator(['fr', 'it', 'es'])
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
name = "John"
lang = factory.SelfAttribute('country.lang')
country = factory.SubFactory(CountryFactory)
class CompanyFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.Company
+ class Meta:
+ model = models.Company
name = "ACME, Inc."
country = factory.SubFactory(CountryFactory)
@@ -302,14 +337,110 @@ default :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>`
.. code-block:: python
class UserFactory(factory.DjangoModelFactory):
- FACTORY_FOR = UserenaSignup
+ class Meta:
+ model = UserenaSignup
+
username = "l7d8s"
email = "my_name@example.com"
password = "my_password"
@classmethod
- def _create(cls, target_class, *args, **kwargs):
+ def _create(cls, model_class, *args, **kwargs):
"""Override the default ``_create`` with our custom call."""
- manager = cls._get_manager(target_class)
+ manager = cls._get_manager(model_class)
# The default would use ``manager.create(*args, **kwargs)``
return manager.create_user(*args, **kwargs)
+
+
+Forcing the sequence counter
+----------------------------
+
+A common pattern with factory_boy is to use a :class:`factory.Sequence` declaration
+to provide varying values to attributes declared as unique.
+
+However, it is sometimes useful to force a given value to the counter, for instance
+to ensure that tests are properly reproductible.
+
+factory_boy provides a few hooks for this:
+
+
+Forcing the value on a per-call basis
+ In order to force the counter for a specific :class:`~factory.Factory` instantiation,
+ just pass the value in the ``__sequence=42`` parameter:
+
+ .. code-block:: python
+
+ class AccountFactory(factory.Factory):
+ class Meta:
+ model = Account
+ uid = factory.Sequence(lambda n: n)
+ name = "Test"
+
+ .. code-block:: pycon
+
+ >>> obj1 = AccountFactory(name="John Doe", __sequence=10)
+ >>> obj1.uid # Taken from the __sequence counter
+ 10
+ >>> obj2 = AccountFactory(name="Jane Doe")
+ >>> obj2.uid # The base sequence counter hasn't changed
+ 1
+
+
+Resetting the counter globally
+ If all calls for a factory must start from a deterministic number,
+ use :meth:`factory.Factory.reset_sequence`; this will reset the counter
+ to its initial value (as defined by :meth:`factory.Factory._setup_next_sequence`).
+
+ .. code-block:: pycon
+
+ >>> AccountFactory().uid
+ 1
+ >>> AccountFactory().uid
+ 2
+ >>> AccountFactory.reset_sequence()
+ >>> AccountFactory().uid # Reset to the initial value
+ 1
+ >>> AccountFactory().uid
+ 2
+
+ It is also possible to reset the counter to a specific value:
+
+ .. code-block:: pycon
+
+ >>> AccountFactory.reset_sequence(10)
+ >>> AccountFactory().uid
+ 10
+ >>> AccountFactory().uid
+ 11
+
+ This recipe is most useful in a :class:`~unittest.TestCase`'s
+ :meth:`~unittest.TestCase.setUp` method.
+
+
+Forcing the initial value for all projects
+ The sequence counter of a :class:`~factory.Factory` can also be set
+ automatically upon the first call through the
+ :meth:`~factory.Factory._setup_next_sequence` method; this helps when the
+ objects's attributes mustn't conflict with pre-existing data.
+
+ A typical example is to ensure that running a Python script twice will create
+ non-conflicting objects, by setting up the counter to "max used value plus one":
+
+ .. code-block:: python
+
+ class AccountFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.Account
+
+ @classmethod
+ def _setup_next_sequence(cls):
+ try:
+ return models.Accounts.objects.latest('uid').uid + 1
+ except models.Account.DoesNotExist:
+ return 1
+
+ .. code-block:: pycon
+
+ >>> Account.objects.create(uid=42, name="Blah")
+ >>> AccountFactory.create() # Sets up the account number based on the latest uid
+ <Account uid=43, name=Test>
diff --git a/docs/reference.rst b/docs/reference.rst
index 53584a0..b5ccd16 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -11,37 +11,54 @@ For internals and customization points, please refer to the :doc:`internals` sec
The :class:`Factory` class
--------------------------
-.. class:: Factory
+.. class:: FactoryOptions
+
+ .. versionadded:: 2.4.0
+
+ A :class:`Factory`'s behaviour can be tuned through a few settings.
- The :class:`Factory` class is the base of factory_boy features.
+ For convenience, they are declared in a single ``class Meta`` attribute:
- It accepts a few specific attributes (must be specified on class declaration):
+ .. code-block:: python
+
+ class MyFactory(factory.Factory):
+ class Meta:
+ model = MyObject
+ abstract = False
- .. attribute:: FACTORY_FOR
+ .. attribute:: model
This optional attribute describes the class of objects to generate.
If unset, it will be inherited from parent :class:`Factory` subclasses.
- .. attribute:: ABSTRACT_FACTORY
+ .. versionadded:: 2.4.0
+
+ .. attribute:: abstract
This attribute indicates that the :class:`Factory` subclass should not
be used to generate objects, but instead provides some extra defaults.
It will be automatically set to ``True`` if neither the :class:`Factory`
- subclass nor its parents define the :attr:`~Factory.FACTORY_FOR` attribute.
+ subclass nor its parents define the :attr:`~FactoryOptions.model` attribute.
- .. attribute:: FACTORY_ARG_PARAMETERS
+ .. warning:: This flag is reset to ``False`` when a :class:`Factory` subclasses
+ another one if a :attr:`~FactoryOptions.model` is set.
+
+ .. versionadded:: 2.4.0
+
+ .. attribute:: inline_args
Some factories require non-keyword arguments to their :meth:`~object.__init__`.
- They should be listed, in order, in the :attr:`FACTORY_ARG_PARAMETERS`
+ They should be listed, in order, in the :attr:`inline_args`
attribute:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
- FACTORY_ARG_PARAMETERS = ('login', 'email')
+ class Meta:
+ model = User
+ inline_args = ('login', 'email')
login = 'john'
email = factory.LazyAttribute(lambda o: '%s@example.com' % o.login)
@@ -53,22 +70,25 @@ The :class:`Factory` class
<User: john>
>>> User('john', 'john@example.com', firstname="John") # actual call
- .. attribute:: FACTORY_HIDDEN_ARGS
+ .. versionadded:: 2.4.0
+
+ .. attribute:: exclude
While writing a :class:`Factory` for some object, it may be useful to
have general fields helping defining others, but that should not be
- passed to the target class; for instance, a field named 'now' that would
+ passed to the model class; for instance, a field named 'now' that would
hold a reference time used by other objects.
- Factory fields whose name are listed in :attr:`FACTORY_HIDDEN_ARGS` will
+ Factory fields whose name are listed in :attr:`exclude` will
be removed from the set of args/kwargs passed to the underlying class;
they can be any valid factory_boy declaration:
.. code-block:: python
class OrderFactory(factory.Factory):
- FACTORY_FOR = Order
- FACTORY_HIDDEN_ARGS = ('now',)
+ class Meta:
+ model = Order
+ exclude = ('now',)
now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow())
started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1))
@@ -83,6 +103,60 @@ The :class:`Factory` class
>>> OrderFactory(now=datetime.datetime(2013, 4, 1, 10))
<Order: started 2013-04-01 09:00:00, paid 2013-04-01 09:10:00>
+ .. versionadded:: 2.4.0
+
+
+ .. attribute:: rename
+
+ Sometimes, a model expect a field with a name already used by one
+ of :class:`Factory`'s methods.
+
+ In this case, the :attr:`rename` attributes allows to define renaming
+ rules: the keys of the :attr:`rename` dict are those used in the
+ :class:`Factory` declarations, and their values the new name:
+
+ .. code-block:: python
+
+ class ImageFactory(factory.Factory):
+ # The model expects "attributes"
+ form_attributes = ['thumbnail', 'black-and-white']
+
+ class Meta:
+ model = Image
+ rename = {'form_attributes': 'attributes'}
+
+ .. versionadded: 2.6.0
+
+
+ .. attribute:: strategy
+
+ Use this attribute to change the strategy used by a :class:`Factory`.
+ The default is :data:`CREATE_STRATEGY`.
+
+
+
+.. class:: Factory
+
+
+ **Class-level attributes:**
+
+ .. attribute:: _meta
+
+ .. versionadded:: 2.4.0
+
+ The :class:`FactoryOptions` instance attached to a :class:`Factory` class is available
+ as a :attr:`_meta` attribute.
+
+ .. attribute:: _options_class
+
+ .. versionadded:: 2.4.0
+
+ If a :class:`Factory` subclass needs to define additional, extra options, it has to
+ provide a custom :class:`FactoryOptions` subclass.
+
+ A pointer to that custom class should be provided as :attr:`_options_class` so that
+ the :class:`Factory`-building metaclass can use it instead.
+
**Base functions:**
@@ -162,7 +236,7 @@ The :class:`Factory` class
The :meth:`_adjust_kwargs` extension point allows for late fields tuning.
It is called once keyword arguments have been resolved and post-generation
- items removed, but before the :attr:`FACTORY_ARG_PARAMETERS` extraction
+ items removed, but before the :attr:`~FactoryOptions.inline_args` extraction
phase.
.. code-block:: python
@@ -177,7 +251,6 @@ The :class:`Factory` class
.. OHAI_VIM**
-
.. classmethod:: _setup_next_sequence(cls)
This method will compute the first value to use for the sequence counter
@@ -189,19 +262,19 @@ The :class:`Factory` class
Subclasses may fetch the next free ID from the database, for instance.
- .. classmethod:: _build(cls, target_class, *args, **kwargs)
+ .. classmethod:: _build(cls, model_class, *args, **kwargs)
.. OHAI_VIM*
This class method is called whenever a new instance needs to be built.
- It receives the target class (provided to :attr:`FACTORY_FOR`), and
+ It receives the model class (provided to :attr:`~FactoryOptions.model`), and
the positional and keyword arguments to use for the class once all has
been computed.
Subclasses may override this for custom APIs.
- .. classmethod:: _create(cls, target_class, *args, **kwargs)
+ .. classmethod:: _create(cls, model_class, *args, **kwargs)
.. OHAI_VIM*
@@ -214,10 +287,11 @@ The :class:`Factory` class
.. code-block:: python
class BaseBackendFactory(factory.Factory):
- ABSTRACT_FACTORY = True # Optional
+ class Meta:
+ abstract = True # Optional
- def _create(cls, target_class, *args, **kwargs):
- obj = target_class(*args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ obj = model_class(*args, **kwargs)
obj.save()
return obj
@@ -254,7 +328,7 @@ The :class:`Factory` class
>>> SomeFactory._next_sequence
4
- Since subclasses of a non-:attr:`abstract <factory.Factory.ABSTRACT_FACTORY>`
+ Since subclasses of a non-:attr:`abstract <factory.FactoryOptions.abstract>`
:class:`~factory.Factory` share the same sequence counter, special care needs
to be taken when resetting the counter of such a subclass.
@@ -293,7 +367,7 @@ factory_boy supports two main strategies for generating instances, plus stubs.
but not persisted to any datastore.
It is usually a simple call to the :meth:`~object.__init__` method of the
- :attr:`~Factory.FACTORY_FOR` class.
+ :attr:`~FactoryOptions.model` class.
.. data:: CREATE_STRATEGY
@@ -316,7 +390,7 @@ factory_boy supports two main strategies for generating instances, plus stubs.
when using the ``create`` strategy.
That policy will be used if the
- :attr:`associated class <Factory.FACTORY_FOR>` has an ``objects``
+ :attr:`associated class <FactoryOptions.model>` has an ``objects``
attribute *and* the :meth:`~Factory._create` classmethod of the
:class:`Factory` wasn't overridden.
@@ -337,7 +411,7 @@ factory_boy supports two main strategies for generating instances, plus stubs.
.. data:: STUB_STRATEGY
The 'stub' strategy is an exception in the factory_boy world: it doesn't return
- an instance of the :attr:`~Factory.FACTORY_FOR` class, and actually doesn't
+ an instance of the :attr:`~FactoryOptions.model` class, and actually doesn't
require one to be present.
Instead, it returns an instance of :class:`StubObject` whose attributes have been
@@ -359,7 +433,7 @@ factory_boy supports two main strategies for generating instances, plus stubs.
.. class:: StubFactory(Factory)
- An :attr:`abstract <Factory.ABSTRACT_FACTORY>` :class:`Factory`,
+ An :attr:`abstract <FactoryOptions.abstract>` :class:`Factory`,
with a default strategy set to :data:`STUB_STRATEGY`.
@@ -400,6 +474,83 @@ factory_boy supports two main strategies for generating instances, plus stubs.
Declarations
------------
+
+Faker
+"""""
+
+.. class:: Faker(provider, locale=None, **kwargs)
+
+ .. OHAIVIM**
+
+ In order to easily define realistic-looking factories,
+ use the :class:`Faker` attribute declaration.
+
+ This is a wrapper around `fake-factory <https://pypi.python.org/pypi/fake-factory>`_;
+ its argument is the name of a ``fake-factory`` provider:
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+ class Meta:
+ model = User
+
+ name = factory.Faker('name')
+
+ .. code-block:: pycon
+
+ >>> user = UserFactory()
+ >>> user.name
+ 'Lucy Cechtelar'
+
+
+ .. attribute:: locale
+
+ If a custom locale is required for one specific field,
+ use the ``locale`` parameter:
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+ class Meta:
+ model = User
+
+ name = factory.Faker('name', locale='fr_FR')
+
+ .. code-block:: pycon
+
+ >>> user = UserFactory()
+ >>> user.name
+ 'Jean Valjean'
+
+
+ .. classmethod:: override_default_locale(cls, locale)
+
+ If the locale needs to be overridden for a whole test,
+ use :meth:`~factory.Faker.override_default_locale`:
+
+ .. code-block:: pycon
+
+ >>> with factory.Faker.override_default_locale('de_DE'):
+ ... UserFactory()
+ <User: Johannes Brahms>
+
+ .. classmethod:: add_provider(cls, locale=None)
+
+ Some projects may need to fake fields beyond those provided by ``fake-factory``;
+ in such cases, use :meth:`factory.Faker.add_provider` to declare additional providers
+ for those fields:
+
+ .. code-block:: python
+
+ factory.Faker.add_provider(SmileyProvider)
+
+ class FaceFactory(factory.Factory):
+ class Meta:
+ model = Face
+
+ smiley = factory.Faker('smiley')
+
+
LazyAttribute
"""""""""""""
@@ -414,7 +565,8 @@ accept the object being built as sole argument, and return a value.
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
username = 'john'
email = factory.LazyAttribute(lambda o: '%s@example.com' % o.username)
@@ -449,7 +601,8 @@ return value of the method:
.. code-block:: python
class UserFactory(factory.Factory)
- FACTORY_FOR = User
+ class Meta:
+ model = User
name = u"Jean"
@@ -481,13 +634,14 @@ This declaration takes a single argument, a function accepting a single paramete
.. note:: An extra kwarg argument, ``type``, may be provided.
- This feature is deprecated in 1.3.0 and will be removed in 2.0.0.
+ This feature was deprecated in 1.3.0 and will be removed in 2.0.0.
.. code-block:: python
class UserFactory(factory.Factory)
- FACTORY_FOR = User
+ class Meta:
+ model = User
phone = factory.Sequence(lambda n: '123-555-%04d' % n)
@@ -512,7 +666,8 @@ be the sequence counter - this might be confusing:
.. code-block:: python
class UserFactory(factory.Factory)
- FACTORY_FOR = User
+ class Meta:
+ model = User
@factory.sequence
def phone(n):
@@ -537,7 +692,8 @@ The sequence counter is shared across all :class:`Sequence` attributes of the
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
phone = factory.Sequence(lambda n: '%04d' % n)
office = factory.Sequence(lambda n: 'A23-B%03d' % n)
@@ -561,7 +717,8 @@ sequence counter is shared:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
phone = factory.Sequence(lambda n: '123-555-%04d' % n)
@@ -596,7 +753,8 @@ class-level value.
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
uid = factory.Sequence(int)
@@ -631,7 +789,8 @@ It takes a single argument, a function whose two parameters are, in order:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
login = 'john'
email = factory.LazyAttributeSequence(lambda o, n: '%s@s%d.example.com' % (o.login, n))
@@ -655,7 +814,8 @@ handles more complex cases:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
login = 'john'
@@ -692,7 +852,8 @@ The :class:`SubFactory` attribute should be called with:
.. code-block:: python
class FooFactory(factory.Factory):
- FACTORY_FOR = Foo
+ class Meta:
+ model = Foo
bar = factory.SubFactory(BarFactory) # Not BarFactory()
@@ -705,7 +866,8 @@ Definition
# A standard factory
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
# Various fields
first_name = 'John'
@@ -714,7 +876,8 @@ Definition
# A factory for an object with a 'User' field
class CompanyFactory(factory.Factory):
- FACTORY_FOR = Company
+ class Meta:
+ model = Company
name = factory.Sequence(lambda n: 'FactoryBoyz' + 'z' * n)
@@ -794,13 +957,15 @@ This issue can be handled by passing the absolute import path to the target
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
username = 'john'
main_group = factory.SubFactory('users.factories.GroupFactory')
class GroupFactory(factory.Factory):
- FACTORY_FOR = Group
+ class Meta:
+ model = Group
name = "MyGroup"
owner = factory.SubFactory(UserFactory)
@@ -828,7 +993,8 @@ That declaration takes a single argument, a dot-delimited path to the attribute
.. code-block:: python
class UserFactory(factory.Factory)
- FACTORY_FOR = User
+ class Meta:
+ model = User
birthdate = factory.Sequence(lambda n: datetime.date(2000, 1, 1) + datetime.timedelta(days=n))
birthmonth = factory.SelfAttribute('birthdate.month')
@@ -854,13 +1020,15 @@ gains an "upward" semantic through the double-dot notation, as used in Python im
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
language = 'en'
class CompanyFactory(factory.Factory):
- FACTORY_FOR = Company
+ class Meta:
+ model = Company
country = factory.SubFactory(CountryFactory)
owner = factory.SubFactory(UserFactory, language=factory.SelfAttribute('..country.language'))
@@ -888,7 +1056,8 @@ through the :attr:`~containers.LazyStub.factory_parent` attribute of the passed-
.. code-block:: python
class CompanyFactory(factory.Factory):
- FACTORY_FOR = Company
+ class Meta:
+ model = Company
country = factory.SubFactory(CountryFactory)
owner = factory.SubFactory(UserFactory,
language=factory.LazyAttribute(lambda user: user.factory_parent.country.language),
@@ -966,7 +1135,8 @@ adequate value.
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
# CATEGORY_CHOICES is a list of (key, title) tuples
category = factory.Iterator(User.CATEGORY_CHOICES, getter=lambda c: c[0])
@@ -987,7 +1157,8 @@ use the :func:`iterator` decorator:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
@factory.iterator
def name():
@@ -1030,7 +1201,8 @@ with the :class:`Dict` and :class:`List` attributes:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
is_superuser = False
roles = factory.Dict({
@@ -1066,7 +1238,8 @@ with the :class:`Dict` and :class:`List` attributes:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
flags = factory.List([
'user',
@@ -1113,10 +1286,11 @@ For instance, a :class:`PostGeneration` hook is declared as ``post``:
.. code-block:: python
class SomeFactory(factory.Factory):
- FACTORY_FOR = SomeObject
+ class Meta:
+ model = SomeObject
@post_generation
- def post(self, create, extracted, **kwargs):
+ def post(obj, create, extracted, **kwargs):
obj.set_origin(create)
.. OHAI_VIM**
@@ -1128,7 +1302,7 @@ When calling the factory, some arguments will be extracted for this method:
- Any argument starting with ``post__XYZ`` will be extracted, its ``post__`` prefix
removed, and added to the kwargs passed to the post-generation hook.
-Extracted arguments won't be passed to the :attr:`~Factory.FACTORY_FOR` class.
+Extracted arguments won't be passed to the :attr:`~FactoryOptions.model` class.
Thus, in the following call:
@@ -1142,7 +1316,7 @@ Thus, in the following call:
)
The ``post`` hook will receive ``1`` as ``extracted`` and ``{'y': 3, 'z__t': 42}``
-as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory.FACTORY_FOR``.
+as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory._meta.model``.
RelatedFactory
@@ -1184,7 +1358,8 @@ RelatedFactory
.. code-block:: python
class FooFactory(factory.Factory):
- FACTORY_FOR = Foo
+ class Meta:
+ model = Foo
bar = factory.RelatedFactory(BarFactory) # Not BarFactory()
@@ -1192,13 +1367,15 @@ RelatedFactory
.. code-block:: python
class CityFactory(factory.Factory):
- FACTORY_FOR = City
+ class Meta:
+ model = City
capital_of = None
name = "Toronto"
class CountryFactory(factory.Factory):
- FACTORY_FOR = Country
+ class Meta:
+ model = Country
lang = 'fr'
capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris")
@@ -1235,12 +1412,29 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables
1
+.. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated.
+ This means that calls to :class:`factory.SelfAttribute` cannot go higher than this :class:`RelatedFactory`:
+
+ .. code-block:: python
+
+ class CountryFactory(factory.Factory):
+ class Meta:
+ model = Country
+
+ lang = 'fr'
+ capital_city = factory.RelatedFactory(CityFactory, 'capital_of',
+ # factory.SelfAttribute('..lang') will crash, since the context of
+ # ``CountryFactory`` has already been evaluated.
+ main_lang=factory.SelfAttribute('capital_of.lang'),
+ )
+
+
PostGeneration
""""""""""""""
.. class:: PostGeneration(callable)
-The :class:`PostGeneration` declaration performs actions once the target object
+The :class:`PostGeneration` declaration performs actions once the model object
has been generated.
Its sole argument is a callable, that will be called once the base object has
@@ -1260,7 +1454,8 @@ as ``callable(obj, create, extracted, **kwargs)``, where:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
login = 'john'
make_mbox = factory.PostGeneration(
@@ -1280,7 +1475,8 @@ A decorator is also provided, decorating a single method accepting the same
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
login = 'john'
@@ -1316,7 +1512,7 @@ PostGenerationMethodCall
.. attribute:: method_name
- The name of the method to call on the :attr:`~Factory.FACTORY_FOR` object
+ The name of the method to call on the :attr:`~FactoryOptions.model` object
.. attribute:: args
@@ -1340,7 +1536,8 @@ attribute like below:
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
username = 'user'
password = factory.PostGenerationMethodCall('set_password',
@@ -1390,7 +1587,8 @@ factory during instantiation.
.. code-block:: python
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
username = 'user'
password = factory.PostGenerationMethodCall('set_password',
@@ -1404,7 +1602,8 @@ example, if we declared the ``password`` attribute like the following,
.. code-block:: python
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
username = 'user'
password = factory.PostGenerationMethodCall('set_password', '', 'sha1')
@@ -1467,7 +1666,8 @@ Lightweight factory declaration
# This is equivalent to:
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ class Meta:
+ model = User
login = 'john'
email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login)
@@ -1486,7 +1686,8 @@ Lightweight factory declaration
# This is equivalent to:
class UserFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class Meta:
+ model = models.User
login = 'john'
email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login)
diff --git a/examples/Makefile b/examples/Makefile
new file mode 100644
index 0000000..6064a9b
--- /dev/null
+++ b/examples/Makefile
@@ -0,0 +1,9 @@
+EXAMPLES = flask_alchemy
+
+TEST_TARGETS = $(addprefix runtest-,$(EXAMPLES))
+
+test: $(TEST_TARGETS)
+
+
+$(TEST_TARGETS): runtest-%:
+ cd $* && PYTHONPATH=../.. python -m unittest
diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py
new file mode 100644
index 0000000..4ab42b0
--- /dev/null
+++ b/examples/flask_alchemy/demoapp.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+from flask import Flask
+from flask.ext.sqlalchemy import SQLAlchemy
+
+app = Flask(__name__)
+app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
+db = SQLAlchemy(app)
+
+
+class User(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ username = db.Column(db.String(80), unique=True)
+ email = db.Column(db.String(120), unique=True)
+
+ def __init__(self, username, email):
+ self.username = username
+ self.email = email
+
+ def __repr__(self):
+ return '<User %r>' % self.username
+
+
+class UserLog(db.Model):
+ id = db.Column(db.Integer, primary_key=True)
+ message = db.Column(db.String(1000))
+
+ user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
+ user = db.relationship('User', backref=db.backref('logs', lazy='dynamic'))
+
+ def __init__(self, message, user):
+ self.message = message
+ self.user = user
+
+ def __repr__(self):
+ return '<Log for %r: %s>' % (self.user, self.message)
diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py
new file mode 100644
index 0000000..f32f8c3
--- /dev/null
+++ b/examples/flask_alchemy/demoapp_factories.py
@@ -0,0 +1,26 @@
+import factory
+import factory.fuzzy
+
+import demoapp
+
+
+class BaseFactory(factory.alchemy.SQLAlchemyModelFactory):
+ class Meta:
+ abstract = True
+ sqlalchemy_session = demoapp.db.session
+
+
+class UserFactory(BaseFactory):
+ class Meta:
+ model = demoapp.User
+
+ username = factory.fuzzy.FuzzyText()
+ email = factory.fuzzy.FuzzyText()
+
+
+class UserLogFactory(BaseFactory):
+ class Meta:
+ model = demoapp.UserLog
+
+ message = factory.fuzzy.FuzzyText()
+ user = factory.SubFactory(UserFactory)
diff --git a/examples/flask_alchemy/requirements.txt b/examples/flask_alchemy/requirements.txt
new file mode 100644
index 0000000..fb675a9
--- /dev/null
+++ b/examples/flask_alchemy/requirements.txt
@@ -0,0 +1,2 @@
+Flask
+Flask-SQLAlchemy
diff --git a/examples/flask_alchemy/test_demoapp.py b/examples/flask_alchemy/test_demoapp.py
new file mode 100644
index 0000000..b485a92
--- /dev/null
+++ b/examples/flask_alchemy/test_demoapp.py
@@ -0,0 +1,35 @@
+import os
+import unittest
+import tempfile
+
+import demoapp
+import demoapp_factories
+
+class DemoAppTestCase(unittest.TestCase):
+
+ def setUp(self):
+ demoapp.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
+ demoapp.app.config['TESTING'] = True
+ self.app = demoapp.app.test_client()
+ self.db = demoapp.db
+ self.db.create_all()
+
+ def tearDown(self):
+ self.db.drop_all()
+
+ def test_user_factory(self):
+ user = demoapp_factories.UserFactory()
+ self.db.session.commit()
+ self.assertIsNotNone(user.id)
+ self.assertEqual(1, len(demoapp.User.query.all()))
+
+ def test_userlog_factory(self):
+ userlog = demoapp_factories.UserLogFactory()
+ self.db.session.commit()
+ self.assertIsNotNone(userlog.id)
+ self.assertIsNotNone(userlog.user.id)
+ self.assertEqual(1, len(demoapp.User.query.all()))
+ self.assertEqual(1, len(demoapp.UserLog.query.all()))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/examples/requirements.txt b/examples/requirements.txt
new file mode 100644
index 0000000..5e11ca5
--- /dev/null
+++ b/examples/requirements.txt
@@ -0,0 +1 @@
+-r flask_alchemy/requirements.txt
diff --git a/factory/__init__.py b/factory/__init__.py
index aa550e8..4a4a09f 100644
--- a/factory/__init__.py
+++ b/factory/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-__version__ = '2.3.1'
+__version__ = '2.6.0'
__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
@@ -32,15 +32,15 @@ from .base import (
ListFactory,
StubFactory,
+ FactoryError,
+
BUILD_STRATEGY,
CREATE_STRATEGY,
STUB_STRATEGY,
use_strategy,
)
-# Backward compatibility; this should be removed soon.
-from .mogo import MogoFactory
-from .django import DjangoModelFactory
+from .faker import Faker
from .declarations import (
LazyAttribute,
@@ -81,3 +81,12 @@ from .helpers import (
post_generation,
)
+# Backward compatibility; this should be removed soon.
+from . import alchemy
+from . import django
+from . import mogo
+from . import mongoengine
+
+MogoFactory = mogo.MogoFactory
+DjangoModelFactory = django.DjangoModelFactory
+
diff --git a/factory/alchemy.py b/factory/alchemy.py
index cec15c9..a9aab23 100644
--- a/factory/alchemy.py
+++ b/factory/alchemy.py
@@ -19,33 +19,31 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import unicode_literals
-from sqlalchemy.sql.functions import max
from . import base
+class SQLAlchemyOptions(base.FactoryOptions):
+ def _build_default_options(self):
+ return super(SQLAlchemyOptions, self)._build_default_options() + [
+ base.OptionDefault('sqlalchemy_session', None, inherit=True),
+ base.OptionDefault('force_flush', False, inherit=True),
+ ]
+
+
class SQLAlchemyModelFactory(base.Factory):
"""Factory for SQLAlchemy models. """
- ABSTRACT_FACTORY = True
- FACTORY_SESSION = None
-
- @classmethod
- def _setup_next_sequence(cls, *args, **kwargs):
- """Compute the next available PK, based on the 'pk' database field."""
- session = cls.FACTORY_SESSION
- model = cls.FACTORY_FOR
- pk = getattr(model, model.__mapper__.primary_key[0].name)
- max_pk = session.query(max(pk)).one()[0]
- if isinstance(max_pk, int):
- return max_pk + 1 if max_pk else 1
- else:
- return 1
+ _options_class = SQLAlchemyOptions
+ class Meta:
+ abstract = True
@classmethod
- def _create(cls, target_class, *args, **kwargs):
+ def _create(cls, model_class, *args, **kwargs):
"""Create an instance of the model, and save it to the database."""
- session = cls.FACTORY_SESSION
- obj = target_class(*args, **kwargs)
+ session = cls._meta.sqlalchemy_session
+ obj = model_class(*args, **kwargs)
session.add(obj)
+ if cls._meta.force_flush:
+ session.flush()
return obj
diff --git a/factory/base.py b/factory/base.py
index 3c6571c..0f2af59 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -23,6 +23,7 @@
import logging
from . import containers
+from . import declarations
from . import utils
logger = logging.getLogger('factory.generate')
@@ -33,22 +34,13 @@ CREATE_STRATEGY = 'create'
STUB_STRATEGY = 'stub'
-# Special declarations
-FACTORY_CLASS_DECLARATION = 'FACTORY_FOR'
-
-# Factory class attributes
-CLASS_ATTRIBUTE_DECLARATIONS = '_declarations'
-CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations'
-CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class'
-CLASS_ATTRIBUTE_IS_ABSTRACT = '_abstract_factory'
-
class FactoryError(Exception):
"""Any exception raised by factory_boy."""
class AssociatedClassError(FactoryError):
- """Exception for Factory subclasses lacking FACTORY_FOR."""
+ """Exception for Factory subclasses lacking Meta.model."""
class UnknownStrategy(FactoryError):
@@ -66,6 +58,14 @@ def get_factory_bases(bases):
return [b for b in bases if issubclass(b, BaseFactory)]
+def resolve_attribute(name, bases, default=None):
+ """Find the first definition of an attribute according to MRO order."""
+ for base in bases:
+ if hasattr(base, name):
+ return getattr(base, name)
+ return default
+
+
class FactoryMetaClass(type):
"""Factory metaclass for handling ordered declarations."""
@@ -75,142 +75,193 @@ class FactoryMetaClass(type):
Returns an instance of the associated class.
"""
- if cls.FACTORY_STRATEGY == BUILD_STRATEGY:
+ if cls._meta.strategy == BUILD_STRATEGY:
return cls.build(**kwargs)
- elif cls.FACTORY_STRATEGY == CREATE_STRATEGY:
+ elif cls._meta.strategy == CREATE_STRATEGY:
return cls.create(**kwargs)
- elif cls.FACTORY_STRATEGY == STUB_STRATEGY:
+ elif cls._meta.strategy == STUB_STRATEGY:
return cls.stub(**kwargs)
else:
- raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(
- cls.FACTORY_STRATEGY))
+ raise UnknownStrategy('Unknown Meta.strategy: {0}'.format(
+ cls._meta.strategy))
- @classmethod
- def _discover_associated_class(mcs, class_name, attrs, inherited=None):
- """Try to find the class associated with this factory.
+ def __new__(mcs, class_name, bases, attrs):
+ """Record attributes as a pattern for later instance construction.
- In order, the following tests will be performed:
- - Lookup the FACTORY_CLASS_DECLARATION attribute
- - If an inherited associated class was provided, use it.
+ This is called when a new Factory subclass is defined; it will collect
+ attribute declaration from the class definition.
Args:
- class_name (str): the name of the factory class being created
- attrs (dict): the dict of attributes from the factory class
+ class_name (str): the name of the class being created
+ bases (list of class): the parents of the class being created
+ attrs (str => obj dict): the attributes as defined in the class
definition
- inherited (class): the optional associated class inherited from a
- parent factory
Returns:
- class: the class to associate with this factory
+ A new class
"""
- if FACTORY_CLASS_DECLARATION in attrs:
- return attrs[FACTORY_CLASS_DECLARATION]
+ parent_factories = get_factory_bases(bases)
+ if parent_factories:
+ base_factory = parent_factories[0]
+ else:
+ base_factory = None
- # No specific associated class was given, and one was defined for our
- # parent, use it.
- if inherited is not None:
- return inherited
+ attrs_meta = attrs.pop('Meta', None)
- # Nothing found, return None.
- return None
+ base_meta = resolve_attribute('_meta', bases)
+ options_class = resolve_attribute('_options_class', bases, FactoryOptions)
- @classmethod
- def _extract_declarations(mcs, bases, attributes):
- """Extract declarations from a class definition.
+ meta = options_class()
+ attrs['_meta'] = meta
- Args:
- bases (class list): parent Factory subclasses
- attributes (dict): attributes declared in the class definition
+ new_class = super(FactoryMetaClass, mcs).__new__(
+ mcs, class_name, bases, attrs)
- Returns:
- dict: the original attributes, where declarations have been moved to
- _declarations and post-generation declarations to
- _postgen_declarations.
- """
- declarations = containers.DeclarationDict()
- postgen_declarations = containers.PostGenerationDeclarationDict()
+ meta.contribute_to_class(new_class,
+ meta=attrs_meta,
+ base_meta=base_meta,
+ base_factory=base_factory,
+ )
- # Add parent declarations in reverse order.
- for base in reversed(bases):
- # Import parent PostGenerationDeclaration
- postgen_declarations.update_with_public(
- getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {}))
- # Import all 'public' attributes (avoid those starting with _)
- declarations.update_with_public(
- getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {}))
+ return new_class
- # Import attributes from the class definition
- attributes = postgen_declarations.update_with_public(attributes)
- # Store protected/private attributes in 'non_factory_attrs'.
- attributes = declarations.update_with_public(attributes)
+ def __str__(cls):
+ if cls._meta.abstract:
+ return '<%s (abstract)>' % cls.__name__
+ else:
+ return '<%s for %s>' % (cls.__name__, cls._meta.model)
- # Store the DeclarationDict in the attributes of the newly created class
- attributes[CLASS_ATTRIBUTE_DECLARATIONS] = declarations
- attributes[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations
- return attributes
+class BaseMeta:
+ abstract = True
+ strategy = CREATE_STRATEGY
- def __new__(mcs, class_name, bases, attrs):
- """Record attributes as a pattern for later instance construction.
- This is called when a new Factory subclass is defined; it will collect
- attribute declaration from the class definition.
+class OptionDefault(object):
+ def __init__(self, name, value, inherit=False):
+ self.name = name
+ self.value = value
+ self.inherit = inherit
- Args:
- class_name (str): the name of the class being created
- bases (list of class): the parents of the class being created
- attrs (str => obj dict): the attributes as defined in the class
- definition
-
- Returns:
- A new class
- """
- parent_factories = get_factory_bases(bases)
- if not parent_factories:
- return super(FactoryMetaClass, mcs).__new__(
- mcs, class_name, bases, attrs)
-
- extra_attrs = {}
+ def apply(self, meta, base_meta):
+ value = self.value
+ if self.inherit and base_meta is not None:
+ value = getattr(base_meta, self.name, value)
+ if meta is not None:
+ value = getattr(meta, self.name, value)
+ return value
- is_abstract = attrs.pop('ABSTRACT_FACTORY', False)
+ def __str__(self):
+ return '%s(%r, %r, inherit=%r)' % (
+ self.__class__.__name__,
+ self.name, self.value, self.inherit)
- base = parent_factories[0]
- inherited_associated_class = base._get_target_class()
- associated_class = mcs._discover_associated_class(class_name, attrs,
- inherited_associated_class)
- # Invoke 'lazy-loading' hooks.
- associated_class = base._load_target_class(associated_class)
+class FactoryOptions(object):
+ def __init__(self):
+ self.factory = None
+ self.base_factory = None
+ self.declarations = {}
+ self.postgen_declarations = {}
- if associated_class is None:
- is_abstract = True
+ def _build_default_options(self):
+ """"Provide the default value for all allowed fields.
+ Custom FactoryOptions classes should override this method
+ to update() its return value.
+ """
+ return [
+ OptionDefault('model', None, inherit=True),
+ OptionDefault('abstract', False, inherit=False),
+ OptionDefault('strategy', CREATE_STRATEGY, inherit=True),
+ OptionDefault('inline_args', (), inherit=True),
+ OptionDefault('exclude', (), inherit=True),
+ OptionDefault('rename', {}, inherit=True),
+ ]
+
+ def _fill_from_meta(self, meta, base_meta):
+ # Exclude private/protected fields from the meta
+ if meta is None:
+ meta_attrs = {}
else:
- # If inheriting the factory from a parent, keep a link to it.
- # This allows to use the sequence counters from the parents.
- if (inherited_associated_class is not None
- and issubclass(associated_class, inherited_associated_class)):
- attrs['_base_factory'] = base
+ meta_attrs = dict((k, v)
+ for (k, v) in vars(meta).items()
+ if not k.startswith('_')
+ )
+
+ for option in self._build_default_options():
+ assert not hasattr(self, option.name), "Can't override field %s." % option.name
+ value = option.apply(meta, base_meta)
+ meta_attrs.pop(option.name, None)
+ setattr(self, option.name, value)
+
+ if meta_attrs:
+ # Some attributes in the Meta aren't allowed here
+ raise TypeError("'class Meta' for %r got unknown attribute(s) %s"
+ % (self.factory, ','.join(sorted(meta_attrs.keys()))))
+
+ def contribute_to_class(self, factory,
+ meta=None, base_meta=None, base_factory=None):
+
+ self.factory = factory
+ self.base_factory = base_factory
+
+ self._fill_from_meta(meta=meta, base_meta=base_meta)
+
+ self.model = self.factory._load_model_class(self.model)
+ if self.model is None:
+ self.abstract = True
+
+ self.counter_reference = self._get_counter_reference()
+
+ for parent in reversed(self.factory.__mro__[1:]):
+ if not hasattr(parent, '_meta'):
+ continue
+ self.declarations.update(parent._meta.declarations)
+ self.postgen_declarations.update(parent._meta.postgen_declarations)
+
+ for k, v in vars(self.factory).items():
+ if self._is_declaration(k, v):
+ self.declarations[k] = v
+ if self._is_postgen_declaration(k, v):
+ self.postgen_declarations[k] = v
+
+ def _get_counter_reference(self):
+ """Identify which factory should be used for a shared counter."""
+
+ if (self.model is not None
+ and self.base_factory is not None
+ and self.base_factory._meta.model is not None
+ and issubclass(self.model, self.base_factory._meta.model)):
+ return self.base_factory
+ else:
+ return self.factory
- # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into
- # account when parsing the declared attributes of the new class.
- extra_attrs[CLASS_ATTRIBUTE_ASSOCIATED_CLASS] = associated_class
+ def _is_declaration(self, name, value):
+ """Determines if a class attribute is a field value declaration.
- extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract
+ Based on the name and value of the class attribute, return ``True`` if
+ it looks like a declaration of a default field value, ``False`` if it
+ is private (name starts with '_') or a classmethod or staticmethod.
- # Extract pre- and post-generation declarations
- attributes = mcs._extract_declarations(parent_factories, attrs)
- attributes.update(extra_attrs)
+ """
+ if isinstance(value, (classmethod, staticmethod)):
+ return False
+ elif isinstance(value, declarations.OrderedDeclaration):
+ return True
+ elif isinstance(value, declarations.PostGenerationDeclaration):
+ return False
+ return not name.startswith("_")
- return super(FactoryMetaClass, mcs).__new__(
- mcs, class_name, bases, attributes)
+ def _is_postgen_declaration(self, name, value):
+ """Captures instances of PostGenerationDeclaration."""
+ return isinstance(value, declarations.PostGenerationDeclaration)
- def __str__(cls):
- if cls._abstract_factory:
- return '<%s (abstract)>'
- else:
- return '<%s for %s>' % (cls.__name__,
- getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
+ def __str__(self):
+ return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__)
+
+ def __repr__(self):
+ return str(self)
# Factory base classes
@@ -252,26 +303,11 @@ class BaseFactory(object):
"""Would be called if trying to instantiate the class."""
raise FactoryError('You cannot instantiate BaseFactory')
+ _meta = FactoryOptions()
+
# ID to use for the next 'declarations.Sequence' attribute.
_counter = None
- # Base factory, if this class was inherited from another factory. This is
- # used for sharing the sequence _counter among factories for the same
- # class.
- _base_factory = None
-
- # Holds the target class, once resolved.
- _associated_class = None
-
- # Whether this factory is considered "abstract", thus uncallable.
- _abstract_factory = False
-
- # List of arguments that should be passed as *args instead of **kwargs
- FACTORY_ARG_PARAMETERS = ()
-
- # List of attributes that should not be passed to the underlying class
- FACTORY_HIDDEN_ARGS = ()
-
@classmethod
def reset_sequence(cls, value=None, force=False):
"""Reset the sequence counter.
@@ -282,9 +318,9 @@ class BaseFactory(object):
force (bool): whether to force-reset parent sequence counters
in a factory inheritance chain.
"""
- if cls._base_factory:
+ if cls._meta.counter_reference is not cls:
if force:
- cls._base_factory.reset_sequence(value=value)
+ cls._meta.base_factory.reset_sequence(value=value)
else:
raise ValueError(
"Cannot reset the sequence of a factory subclass. "
@@ -330,9 +366,9 @@ class BaseFactory(object):
"""
# Rely upon our parents
- if cls._base_factory and not cls._base_factory._abstract_factory:
- logger.debug("%r: reusing sequence from %r", cls, cls._base_factory)
- return cls._base_factory._generate_next_sequence()
+ if cls._meta.counter_reference is not cls:
+ logger.debug("%r: reusing sequence from %r", cls, cls._meta.base_factory)
+ return cls._meta.base_factory._generate_next_sequence()
# Make sure _counter is initialized
cls._setup_counter()
@@ -373,7 +409,15 @@ class BaseFactory(object):
extra_defs (dict): additional definitions to insert into the
retrieved DeclarationDict.
"""
- return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs)
+ decls = cls._meta.declarations.copy()
+ decls.update(extra_defs or {})
+ return decls
+
+ @classmethod
+ def _rename_fields(cls, **kwargs):
+ for old_name, new_name in cls._meta.rename.items():
+ kwargs[new_name] = kwargs.pop(old_name)
+ return kwargs
@classmethod
def _adjust_kwargs(cls, **kwargs):
@@ -381,8 +425,8 @@ class BaseFactory(object):
return kwargs
@classmethod
- def _load_target_class(cls, class_definition):
- """Extension point for loading target classes.
+ def _load_model_class(cls, class_definition):
+ """Extension point for loading model classes.
This can be overridden in framework-specific subclasses to hook into
existing model repositories, for instance.
@@ -390,10 +434,10 @@ class BaseFactory(object):
return class_definition
@classmethod
- def _get_target_class(cls):
- """Retrieve the actual, associated target class."""
- definition = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
- return cls._load_target_class(definition)
+ def _get_model_class(cls):
+ """Retrieve the actual, associated model class."""
+ definition = cls._meta.model
+ return cls._load_model_class(definition)
@classmethod
def _prepare(cls, create, **kwargs):
@@ -403,15 +447,16 @@ class BaseFactory(object):
create: bool, whether to create or to build the object
**kwargs: arguments to pass to the creation function
"""
- target_class = cls._get_target_class()
+ model_class = cls._get_model_class()
+ kwargs = cls._rename_fields(**kwargs)
kwargs = cls._adjust_kwargs(**kwargs)
# Remove 'hidden' arguments.
- for arg in cls.FACTORY_HIDDEN_ARGS:
+ for arg in cls._meta.exclude:
del kwargs[arg]
# Extract *args from **kwargs
- args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS)
+ args = tuple(kwargs.pop(key) for key in cls._meta.inline_args)
logger.debug('BaseFactory: Generating %s.%s(%s)',
cls.__module__,
@@ -419,9 +464,9 @@ class BaseFactory(object):
utils.log_pprint(args, kwargs),
)
if create:
- return cls._create(target_class, *args, **kwargs)
+ return cls._create(model_class, *args, **kwargs)
else:
- return cls._build(target_class, *args, **kwargs)
+ return cls._build(model_class, *args, **kwargs)
@classmethod
def _generate(cls, create, attrs):
@@ -431,15 +476,14 @@ class BaseFactory(object):
create (bool): whether to 'build' or 'create' the object
attrs (dict): attributes to use for generating the object
"""
- if cls._abstract_factory:
+ if cls._meta.abstract:
raise FactoryError(
"Cannot generate instances of abstract factory %(f)s; "
- "Ensure %(f)s.FACTORY_FOR is set and %(f)s.ABSTRACT_FACTORY "
- "is either not set or False." % dict(f=cls))
+ "Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract "
+ "is either not set or False." % dict(f=cls.__name__))
# Extract declarations used for post-generation
- postgen_declarations = getattr(cls,
- CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS)
+ postgen_declarations = cls._meta.postgen_declarations
postgen_attributes = {}
for name, decl in sorted(postgen_declarations.items()):
postgen_attributes[name] = decl.extract(name, attrs)
@@ -469,34 +513,34 @@ class BaseFactory(object):
pass
@classmethod
- def _build(cls, target_class, *args, **kwargs):
- """Actually build an instance of the target_class.
+ def _build(cls, model_class, *args, **kwargs):
+ """Actually build an instance of the model_class.
Customization point, will be called once the full set of args and kwargs
has been computed.
Args:
- target_class (type): the class for which an instance should be
+ model_class (type): the class for which an instance should be
built
args (tuple): arguments to use when building the class
kwargs (dict): keyword arguments to use when building the class
"""
- return target_class(*args, **kwargs)
+ return model_class(*args, **kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- """Actually create an instance of the target_class.
+ def _create(cls, model_class, *args, **kwargs):
+ """Actually create an instance of the model_class.
Customization point, will be called once the full set of args and kwargs
has been computed.
Args:
- target_class (type): the class for which an instance should be
+ model_class (type): the class for which an instance should be
created
args (tuple): arguments to use when creating the class
kwargs (dict): keyword arguments to use when creating the class
"""
- return target_class(*args, **kwargs)
+ return model_class(*args, **kwargs)
@classmethod
def build(cls, **kwargs):
@@ -626,8 +670,7 @@ class BaseFactory(object):
Factory = FactoryMetaClass('Factory', (BaseFactory,), {
- 'ABSTRACT_FACTORY': True,
- 'FACTORY_STRATEGY': CREATE_STRATEGY,
+ 'Meta': BaseMeta,
'__doc__': """Factory base with build and create support.
This class has the ability to support multiple ORMs by using custom creation
@@ -642,12 +685,13 @@ Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201
class StubFactory(Factory):
- FACTORY_STRATEGY = STUB_STRATEGY
- FACTORY_FOR = containers.StubObject
+ class Meta:
+ strategy = STUB_STRATEGY
+ model = containers.StubObject
@classmethod
def build(cls, **kwargs):
- raise UnsupportedStrategy()
+ return cls.stub(**kwargs)
@classmethod
def create(cls, **kwargs):
@@ -656,44 +700,48 @@ class StubFactory(Factory):
class BaseDictFactory(Factory):
"""Factory for dictionary-like classes."""
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
+ def _build(cls, model_class, *args, **kwargs):
if args:
raise ValueError(
- "DictFactory %r does not support FACTORY_ARG_PARAMETERS.", cls)
- return target_class(**kwargs)
+ "DictFactory %r does not support Meta.inline_args.", cls)
+ return model_class(**kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return cls._build(target_class, *args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return cls._build(model_class, *args, **kwargs)
class DictFactory(BaseDictFactory):
- FACTORY_FOR = dict
+ class Meta:
+ model = dict
class BaseListFactory(Factory):
"""Factory for list-like classes."""
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
+ def _build(cls, model_class, *args, **kwargs):
if args:
raise ValueError(
- "ListFactory %r does not support FACTORY_ARG_PARAMETERS.", cls)
+ "ListFactory %r does not support Meta.inline_args.", cls)
values = [v for k, v in sorted(kwargs.items())]
- return target_class(values)
+ return model_class(values)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return cls._build(target_class, *args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return cls._build(model_class, *args, **kwargs)
class ListFactory(BaseListFactory):
- FACTORY_FOR = list
+ class Meta:
+ model = list
def use_strategy(new_strategy):
@@ -702,6 +750,6 @@ def use_strategy(new_strategy):
This is an alternative to setting default_strategy in the class definition.
"""
def wrapped_class(klass):
- klass.FACTORY_STRATEGY = new_strategy
+ klass._meta.strategy = new_strategy
return klass
return wrapped_class
diff --git a/factory/compat.py b/factory/compat.py
index 7747b1a..737d91a 100644
--- a/factory/compat.py
+++ b/factory/compat.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -42,14 +42,6 @@ else: # pragma: no cover
from io import BytesIO
-if sys.version_info[:2] == (2, 6): # pragma: no cover
- def float_to_decimal(fl):
- return decimal.Decimal(str(fl))
-else: # pragma: no cover
- def float_to_decimal(fl):
- return decimal.Decimal(fl)
-
-
try: # pragma: no cover
# Python >= 3.2
UTC = datetime.timezone.utc
diff --git a/factory/containers.py b/factory/containers.py
index 4537e44..0ae354b 100644
--- a/factory/containers.py
+++ b/factory/containers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -47,27 +47,27 @@ class LazyStub(object):
__containers (LazyStub list): "parents" of the LazyStub being built.
This allows to have the field of a field depend on the value of
another field
- __target_class (type): the target class to build.
+ __model_class (type): the model class to build.
"""
__initialized = False
- def __init__(self, attrs, containers=(), target_class=object, log_ctx=None):
+ def __init__(self, attrs, containers=(), model_class=object, log_ctx=None):
self.__attrs = attrs
self.__values = {}
self.__pending = []
self.__containers = containers
- self.__target_class = target_class
- self.__log_ctx = log_ctx or '%s.%s' % (target_class.__module__, target_class.__name__)
+ self.__model_class = model_class
+ self.__log_ctx = log_ctx or '%s.%s' % (model_class.__module__, model_class.__name__)
self.factory_parent = containers[0] if containers else None
self.__initialized = True
def __repr__(self):
- return '<LazyStub for %s.%s>' % (self.__target_class.__module__, self.__target_class.__name__)
+ return '<LazyStub for %s.%s>' % (self.__model_class.__module__, self.__model_class.__name__)
def __str__(self):
return '<LazyStub for %s with %s>' % (
- self.__target_class.__name__, list(self.__attrs.keys()))
+ self.__model_class.__name__, list(self.__attrs.keys()))
def __fill__(self):
"""Fill this LazyStub, computing values of all defined attributes.
@@ -121,61 +121,6 @@ class LazyStub(object):
raise AttributeError('Setting of object attributes is not allowed')
-class DeclarationDict(dict):
- """Slightly extended dict to work with OrderedDeclaration."""
-
- def is_declaration(self, name, value):
- """Determines if a class attribute is a field value declaration.
-
- Based on the name and value of the class attribute, return ``True`` if
- it looks like a declaration of a default field value, ``False`` if it
- is private (name starts with '_') or a classmethod or staticmethod.
-
- """
- if isinstance(value, (classmethod, staticmethod)):
- return False
- elif isinstance(value, declarations.OrderedDeclaration):
- return True
- return (not name.startswith("_") and not name.startswith("FACTORY_"))
-
- def update_with_public(self, d):
- """Updates the DeclarationDict from a class definition dict.
-
- Takes into account all public attributes and OrderedDeclaration
- instances; ignores all class/staticmethods and private attributes
- (starting with '_').
-
- Returns a dict containing all remaining elements.
- """
- remaining = {}
- for k, v in d.items():
- if self.is_declaration(k, v):
- self[k] = v
- else:
- remaining[k] = v
- return remaining
-
- def copy(self, extra=None):
- """Copy this DeclarationDict into another one, including extra values.
-
- Args:
- extra (dict): additional attributes to include in the copy.
- """
- new = self.__class__()
- new.update(self)
- if extra:
- new.update(extra)
- return new
-
-
-class PostGenerationDeclarationDict(DeclarationDict):
- """Alternate DeclarationDict for PostGenerationDeclaration."""
-
- def is_declaration(self, name, value):
- """Captures instances of PostGenerationDeclaration."""
- return isinstance(value, declarations.PostGenerationDeclaration)
-
-
class LazyValue(object):
"""Some kind of "lazy evaluating" object."""
@@ -279,7 +224,7 @@ class AttributeBuilder(object):
wrapped_attrs[k] = v
stub = LazyStub(wrapped_attrs, containers=self._containers,
- target_class=self.factory, log_ctx=self._log_ctx)
+ model_class=self.factory, log_ctx=self._log_ctx)
return stub.__fill__()
diff --git a/factory/declarations.py b/factory/declarations.py
index 037a679..f0dbfe5 100644
--- a/factory/declarations.py
+++ b/factory/declarations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,6 @@
import itertools
-import warnings
import logging
from . import compat
@@ -50,7 +49,7 @@ class OrderedDeclaration(object):
attributes
containers (list of containers.LazyStub): The chain of SubFactory
which led to building this object.
- create (bool): whether the target class should be 'built' or
+ create (bool): whether the model class should be 'built' or
'created'
extra (DeclarationDict or None): extracted key/value extracted from
the attribute prefix
@@ -161,12 +160,19 @@ class Iterator(OrderedDeclaration):
def __init__(self, iterator, cycle=True, getter=None):
super(Iterator, self).__init__()
self.getter = getter
+ self.iterator = None
if cycle:
- iterator = itertools.cycle(iterator)
- self.iterator = utils.ResetableIterator(iterator)
+ self.iterator_builder = lambda: utils.ResetableIterator(itertools.cycle(iterator))
+ else:
+ self.iterator_builder = lambda: utils.ResetableIterator(iterator)
def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ # Begin unrolling as late as possible.
+ # This helps with ResetableIterator(MyModel.objects.all())
+ if self.iterator is None:
+ self.iterator = self.iterator_builder()
+
logger.debug("Iterator: Fetching next value from %r", self.iterator)
value = next(iter(self.iterator))
if self.getter is None:
@@ -195,7 +201,7 @@ class Sequence(OrderedDeclaration):
self.type = type
def evaluate(self, sequence, obj, create, extra=None, containers=()):
- logger.debug("Sequence: Computing next value of %r for seq=%d", self.function, sequence)
+ logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, sequence)
return self.function(self.type(sequence))
@@ -209,7 +215,7 @@ class LazyAttributeSequence(Sequence):
of counter for the 'function' attribute.
"""
def evaluate(self, sequence, obj, create, extra=None, containers=()):
- logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%d, obj=%r",
+ logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%r",
self.function, sequence, obj)
return self.function(obj, self.type(sequence))
@@ -434,7 +440,7 @@ class ExtractionContext(object):
class PostGenerationDeclaration(object):
- """Declarations to be called once the target object has been generated."""
+ """Declarations to be called once the model object has been generated."""
def extract(self, name, attrs):
"""Extract relevant attributes from a dict.
@@ -502,14 +508,6 @@ class RelatedFactory(PostGenerationDeclaration):
def __init__(self, factory, factory_related_name='', **defaults):
super(RelatedFactory, self).__init__()
- if factory_related_name == '' and defaults.get('name') is not None:
- warnings.warn(
- "Usage of RelatedFactory(SomeFactory, name='foo') is deprecated"
- " and will be removed in the future. Please use the"
- " RelatedFactory(SomeFactory, 'foo') or"
- " RelatedFactory(SomeFactory, factory_related_name='foo')"
- " syntax instead", PendingDeprecationWarning, 2)
- factory_related_name = defaults.pop('name')
self.name = factory_related_name
self.defaults = defaults
diff --git a/factory/django.py b/factory/django.py
index a3dfdfc..b3c508c 100644
--- a/factory/django.py
+++ b/factory/django.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,8 +32,10 @@ import functools
"""factory_boy extensions for use with the Django framework."""
try:
+ import django
from django.core import files as django_files
except ImportError as e: # pragma: no cover
+ django = None
django_files = None
import_failure = e
@@ -45,6 +47,8 @@ from .compat import BytesIO, is_string
logger = logging.getLogger('factory.generate')
+DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS
+
def require_django():
"""Simple helper to ensure Django is available."""
@@ -52,6 +56,56 @@ def require_django():
raise import_failure
+_LAZY_LOADS = {}
+
+def get_model(app, model):
+ """Wrapper around django's get_model."""
+ if 'get_model' not in _LAZY_LOADS:
+ _lazy_load_get_model()
+
+ _get_model = _LAZY_LOADS['get_model']
+ return _get_model(app, model)
+
+
+def _lazy_load_get_model():
+ """Lazy loading of get_model.
+
+ get_model loads django.conf.settings, which may fail if
+ the settings haven't been configured yet.
+ """
+ if django is None:
+ def get_model(app, model):
+ raise import_failure
+
+ elif django.VERSION[:2] < (1, 7):
+ from django.db.models.loading import get_model
+
+ else:
+ from django import apps as django_apps
+ get_model = django_apps.apps.get_model
+ _LAZY_LOADS['get_model'] = get_model
+
+
+class DjangoOptions(base.FactoryOptions):
+ def _build_default_options(self):
+ return super(DjangoOptions, self)._build_default_options() + [
+ base.OptionDefault('django_get_or_create', (), inherit=True),
+ base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True),
+ ]
+
+ def _get_counter_reference(self):
+ counter_reference = super(DjangoOptions, self)._get_counter_reference()
+ if (counter_reference == self.base_factory
+ and self.base_factory._meta.model is not None
+ and self.base_factory._meta.model._meta.abstract
+ and self.model is not None
+ and not self.model._meta.abstract):
+ # Target factory is for an abstract model, yet we're for another,
+ # concrete subclass => don't reuse the counter.
+ return self.factory
+ return counter_reference
+
+
class DjangoModelFactory(base.Factory):
"""Factory for Django models.
@@ -61,53 +115,48 @@ class DjangoModelFactory(base.Factory):
handle those for non-numerical primary keys.
"""
- ABSTRACT_FACTORY = True # Optional, but explicit.
- FACTORY_DJANGO_GET_OR_CREATE = ()
+ _options_class = DjangoOptions
+ class Meta:
+ abstract = True # Optional, but explicit.
@classmethod
- def _load_target_class(cls, definition):
+ def _load_model_class(cls, definition):
if is_string(definition) and '.' in definition:
app, model = definition.split('.', 1)
- from django.db.models import loading as django_loading
- return django_loading.get_model(app, model)
+ return get_model(app, model)
return definition
@classmethod
- def _get_manager(cls, target_class):
+ def _get_manager(cls, model_class):
+ if model_class is None:
+ raise base.AssociatedClassError("No model set on %s.%s.Meta"
+ % (cls.__module__, cls.__name__))
+
try:
- return target_class._default_manager # pylint: disable=W0212
+ manager = model_class.objects
except AttributeError:
- return target_class.objects
-
- @classmethod
- def _setup_next_sequence(cls):
- """Compute the next available PK, based on the 'pk' database field."""
+ # When inheriting from an abstract model with a custom
+ # manager, the class has no 'objects' field.
+ manager = model_class._default_manager
- model = cls._get_target_class() # pylint: disable=E1101
- manager = cls._get_manager(model)
-
- try:
- return 1 + manager.values_list('pk', flat=True
- ).order_by('-pk')[0]
- except (IndexError, TypeError):
- # IndexError: No instance exist yet
- # TypeError: pk isn't an integer type
- return 1
+ if cls._meta.database != DEFAULT_DB_ALIAS:
+ manager = manager.using(cls._meta.database)
+ return manager
@classmethod
- def _get_or_create(cls, target_class, *args, **kwargs):
+ def _get_or_create(cls, model_class, *args, **kwargs):
"""Create an instance of the model through objects.get_or_create."""
- manager = cls._get_manager(target_class)
+ manager = cls._get_manager(model_class)
- assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, (
+ assert 'defaults' not in cls._meta.django_get_or_create, (
"'defaults' is a reserved keyword for get_or_create "
- "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)"
- % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE))
+ "(in %s._meta.django_get_or_create=%r)"
+ % (cls, cls._meta.django_get_or_create))
key_fields = {}
- for field in cls.FACTORY_DJANGO_GET_OR_CREATE:
+ for field in cls._meta.django_get_or_create:
key_fields[field] = kwargs.pop(field)
key_fields['defaults'] = kwargs
@@ -115,12 +164,12 @@ class DjangoModelFactory(base.Factory):
return obj
@classmethod
- def _create(cls, target_class, *args, **kwargs):
+ def _create(cls, model_class, *args, **kwargs):
"""Create an instance of the model, and save it to the database."""
- manager = cls._get_manager(target_class)
+ manager = cls._get_manager(model_class)
- if cls.FACTORY_DJANGO_GET_OR_CREATE:
- return cls._get_or_create(target_class, *args, **kwargs)
+ if cls._meta.django_get_or_create:
+ return cls._get_or_create(model_class, *args, **kwargs)
return manager.create(*args, **kwargs)
@@ -132,24 +181,22 @@ class DjangoModelFactory(base.Factory):
obj.save()
-class FileField(declarations.PostGenerationDeclaration):
+class FileField(declarations.ParameteredAttribute):
"""Helper to fill in django.db.models.FileField from a Factory."""
DEFAULT_FILENAME = 'example.dat'
+ EXTEND_CONTAINERS = True
def __init__(self, **defaults):
require_django()
- self.defaults = defaults
- super(FileField, self).__init__()
+ super(FileField, self).__init__(**defaults)
def _make_data(self, params):
"""Create data for the field."""
return params.get('data', b'')
- def _make_content(self, extraction_context):
+ def _make_content(self, params):
path = ''
- params = dict(self.defaults)
- params.update(extraction_context.extra)
if params.get('from_path') and params.get('from_file'):
raise ValueError(
@@ -157,12 +204,7 @@ class FileField(declarations.PostGenerationDeclaration):
"be non-empty when calling factory.django.FileField."
)
- if extraction_context.did_extract:
- # Should be a django.core.files.File
- content = extraction_context.value
- path = content.name
-
- elif params.get('from_path'):
+ if params.get('from_path'):
path = params['from_path']
f = open(path, 'rb')
content = django_files.File(f, name=path)
@@ -184,19 +226,13 @@ class FileField(declarations.PostGenerationDeclaration):
filename = params.get('filename', default_filename)
return filename, content
- def call(self, obj, create, extraction_context):
+ def generate(self, sequence, obj, create, params):
"""Fill in the field."""
- if extraction_context.did_extract and extraction_context.value is None:
- # User passed an empty value, don't fill
- return
- filename, content = self._make_content(extraction_context)
- field_file = getattr(obj, extraction_context.for_field)
- try:
- field_file.save(filename, content, save=create)
- finally:
- content.file.close()
- return field_file
+ params.setdefault('__sequence', sequence)
+ params = base.DictFactory.simple_generate(create, **params)
+ filename, content = self._make_content(params)
+ return django_files.File(content.file, filename)
class ImageField(FileField):
@@ -250,6 +286,9 @@ class mute_signals(object):
logger.debug('mute_signals: Disabling signal handlers %r',
signal.receivers)
+ # Note that we're using implementation details of
+ # django.signals, since arguments to signal.connect()
+ # are lost in signal.receivers
self.paused[signal] = signal.receivers
signal.receivers = []
@@ -259,8 +298,17 @@ class mute_signals(object):
receivers)
signal.receivers = receivers
+ if django.VERSION[:2] >= (1, 6):
+ with signal.lock:
+ # Django uses some caching for its signals.
+ # Since we're bypassing signal.connect and signal.disconnect,
+ # we have to keep messing with django's internals.
+ signal.sender_receivers_cache.clear()
self.paused = {}
+ def copy(self):
+ return mute_signals(*self.signals)
+
def __call__(self, callable_obj):
if isinstance(callable_obj, base.FactoryMetaClass):
# Retrieve __func__, the *actual* callable object.
@@ -269,7 +317,8 @@ class mute_signals(object):
@classmethod
@functools.wraps(generate_method)
def wrapped_generate(*args, **kwargs):
- with self:
+ # A mute_signals() object is not reentrant; use a copy everytime.
+ with self.copy():
return generate_method(*args, **kwargs)
callable_obj._generate = wrapped_generate
@@ -278,7 +327,8 @@ class mute_signals(object):
else:
@functools.wraps(callable_obj)
def wrapper(*args, **kwargs):
- with self:
+ # A mute_signals() object is not reentrant; use a copy everytime.
+ with self.copy():
return callable_obj(*args, **kwargs)
return wrapper
diff --git a/factory/faker.py b/factory/faker.py
new file mode 100644
index 0000000..5411985
--- /dev/null
+++ b/factory/faker.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2015 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+"""Additional declarations for "faker" attributes.
+
+Usage:
+
+ class MyFactory(factory.Factory):
+ class Meta:
+ model = MyProfile
+
+ first_name = factory.Faker('name')
+"""
+
+
+from __future__ import absolute_import
+from __future__ import unicode_literals
+
+import contextlib
+
+import faker
+import faker.config
+
+from . import declarations
+
+class Faker(declarations.OrderedDeclaration):
+ """Wrapper for 'faker' values.
+
+ Args:
+ provider (str): the name of the Faker field
+ locale (str): the locale to use for the faker
+
+ All other kwargs will be passed to the underlying provider
+ (e.g ``factory.Faker('ean', length=10)``
+ calls ``faker.Faker.ean(length=10)``)
+
+ Usage:
+ >>> foo = factory.Faker('name')
+ """
+ def __init__(self, provider, locale=None, **kwargs):
+ self.provider = provider
+ self.provider_kwargs = kwargs
+ self.locale = locale
+
+ def generate(self, extra_kwargs):
+ kwargs = {}
+ kwargs.update(self.provider_kwargs)
+ kwargs.update(extra_kwargs)
+ faker = self._get_faker(self.locale)
+ return faker.format(self.provider, **kwargs)
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ return self.generate(extra or {})
+
+ _FAKER_REGISTRY = {}
+ _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE
+
+ @classmethod
+ @contextlib.contextmanager
+ def override_default_locale(cls, locale):
+ old_locale = cls._DEFAULT_LOCALE
+ cls._DEFAULT_LOCALE = locale
+ try:
+ yield
+ finally:
+ cls._DEFAULT_LOCALE = old_locale
+
+ @classmethod
+ def _get_faker(cls, locale=None):
+ if locale is None:
+ locale = cls._DEFAULT_LOCALE
+
+ if locale not in cls._FAKER_REGISTRY:
+ cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale)
+
+ return cls._FAKER_REGISTRY[locale]
+
+ @classmethod
+ def add_provider(cls, provider, locale=None):
+ """Add a new Faker provider for the specified locale"""
+ cls._get_faker(locale).add_provider(provider)
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index 94599b7..a7e834c 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,6 +34,25 @@ from . import compat
from . import declarations
+_random = random.Random()
+
+
+def get_random_state():
+ """Retrieve the state of factory.fuzzy's random generator."""
+ return _random.getstate()
+
+
+def set_random_state(state):
+ """Force-set the state of factory.fuzzy's random generator."""
+ return _random.setstate(state)
+
+
+def reseed_random(seed):
+ """Reseed factory.fuzzy's random generator."""
+ r = random.Random(seed)
+ set_random_state(r.getstate())
+
+
class BaseFuzzyAttribute(declarations.OrderedDeclaration):
"""Base class for fuzzy attributes.
@@ -81,7 +100,7 @@ class FuzzyText(BaseFuzzyAttribute):
"""
def __init__(self, prefix='', length=12, suffix='',
- chars=string.ascii_letters, **kwargs):
+ chars=string.ascii_letters, **kwargs):
super(FuzzyText, self).__init__(**kwargs)
self.prefix = prefix
self.suffix = suffix
@@ -89,19 +108,27 @@ class FuzzyText(BaseFuzzyAttribute):
self.chars = tuple(chars) # Unroll iterators
def fuzz(self):
- chars = [random.choice(self.chars) for _i in range(self.length)]
+ chars = [_random.choice(self.chars) for _i in range(self.length)]
return self.prefix + ''.join(chars) + self.suffix
class FuzzyChoice(BaseFuzzyAttribute):
- """Handles fuzzy choice of an attribute."""
+ """Handles fuzzy choice of an attribute.
+
+ Args:
+ choices (iterable): An iterable yielding options; will only be unrolled
+ on the first call.
+ """
def __init__(self, choices, **kwargs):
- self.choices = list(choices)
+ self.choices = None
+ self.choices_generator = choices
super(FuzzyChoice, self).__init__(**kwargs)
def fuzz(self):
- return random.choice(self.choices)
+ if self.choices is None:
+ self.choices = list(self.choices_generator)
+ return _random.choice(self.choices)
class FuzzyInteger(BaseFuzzyAttribute):
@@ -119,7 +146,7 @@ class FuzzyInteger(BaseFuzzyAttribute):
super(FuzzyInteger, self).__init__(**kwargs)
def fuzz(self):
- return random.randrange(self.low, self.high + 1, self.step)
+ return _random.randrange(self.low, self.high + 1, self.step)
class FuzzyDecimal(BaseFuzzyAttribute):
@@ -137,7 +164,7 @@ class FuzzyDecimal(BaseFuzzyAttribute):
super(FuzzyDecimal, self).__init__(**kwargs)
def fuzz(self):
- base = compat.float_to_decimal(random.uniform(self.low, self.high))
+ base = decimal.Decimal(str(_random.uniform(self.low, self.high)))
return base.quantize(decimal.Decimal(10) ** -self.precision)
@@ -155,7 +182,7 @@ class FuzzyFloat(BaseFuzzyAttribute):
super(FuzzyFloat, self).__init__(**kwargs)
def fuzz(self):
- return random.uniform(self.low, self.high)
+ return _random.uniform(self.low, self.high)
class FuzzyDate(BaseFuzzyAttribute):
@@ -175,7 +202,7 @@ class FuzzyDate(BaseFuzzyAttribute):
self.end_date = end_date.toordinal()
def fuzz(self):
- return datetime.date.fromordinal(random.randint(self.start_date, self.end_date))
+ return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date))
class BaseFuzzyDateTime(BaseFuzzyAttribute):
@@ -215,7 +242,7 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute):
delta = self.end_dt - self.start_dt
microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))
- offset = random.randint(0, microseconds)
+ offset = _random.randint(0, microseconds)
result = self.start_dt + datetime.timedelta(microseconds=offset)
if self.force_year is not None:
@@ -270,10 +297,10 @@ class FuzzyDateTime(BaseFuzzyDateTime):
def _check_bounds(self, start_dt, end_dt):
if start_dt.tzinfo is None:
raise ValueError(
- "FuzzyDateTime only handles aware datetimes, got start=%r"
+ "FuzzyDateTime requires timezone-aware datetimes, got start=%r"
% start_dt)
if end_dt.tzinfo is None:
raise ValueError(
- "FuzzyDateTime only handles aware datetimes, got end=%r"
+ "FuzzyDateTime requires timezone-aware datetimes, got end=%r"
% end_dt)
super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt)
diff --git a/factory/helpers.py b/factory/helpers.py
index 4a2a254..60a4d75 100644
--- a/factory/helpers.py
+++ b/factory/helpers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,7 +28,6 @@ import logging
from . import base
from . import declarations
-from . import django
@contextlib.contextmanager
@@ -50,7 +49,9 @@ def debug(logger='factory', stream=None):
def make_factory(klass, **kwargs):
"""Create a new, simple factory for the given class."""
factory_name = '%sFactory' % klass.__name__
- kwargs[base.FACTORY_CLASS_DECLARATION] = klass
+ class Meta:
+ model = klass
+ kwargs['Meta'] = Meta
base_class = kwargs.pop('FACTORY_CLASS', base.Factory)
factory_class = type(base.Factory).__new__(
diff --git a/factory/mogo.py b/factory/mogo.py
index 48d9677..aa9f28b 100644
--- a/factory/mogo.py
+++ b/factory/mogo.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,14 +32,15 @@ from . import base
class MogoFactory(base.Factory):
"""Factory for mogo objects."""
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
- return target_class.new(*args, **kwargs)
+ def _build(cls, model_class, *args, **kwargs):
+ return model_class(*args, **kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- instance = target_class.new(*args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ instance = model_class(*args, **kwargs)
instance.save()
return instance
diff --git a/factory/mongoengine.py b/factory/mongoengine.py
index 462f5f2..f50b727 100644
--- a/factory/mongoengine.py
+++ b/factory/mongoengine.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,15 +32,17 @@ from . import base
class MongoEngineFactory(base.Factory):
"""Factory for mongoengine objects."""
- ABSTRACT_FACTORY = True
+
+ class Meta:
+ abstract = True
@classmethod
- def _build(cls, target_class, *args, **kwargs):
- return target_class(*args, **kwargs)
+ def _build(cls, model_class, *args, **kwargs):
+ return model_class(*args, **kwargs)
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- instance = target_class(*args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ instance = model_class(*args, **kwargs)
if instance._is_document:
instance.save()
return instance
diff --git a/factory/utils.py b/factory/utils.py
index 6f0c763..15dba0a 100644
--- a/factory/utils.py
+++ b/factory/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -110,15 +110,29 @@ def _safe_repr(obj):
return obj_repr.decode('utf-8')
-def log_pprint(args=(), kwargs=None):
- kwargs = kwargs or {}
- return ', '.join(
- [_safe_repr(arg) for arg in args] +
- [
- '%s=%s' % (key, _safe_repr(value))
- for key, value in kwargs.items()
- ]
- )
+class log_pprint(object):
+ """Helper for properly printing args / kwargs passed to an object.
+
+ Since it is only used with factory.debug(), the computation is
+ performed lazily.
+ """
+ __slots__ = ['args', 'kwargs']
+
+ def __init__(self, args=(), kwargs=None):
+ self.args = args
+ self.kwargs = kwargs or {}
+
+ def __repr__(self):
+ return repr(str(self))
+
+ def __str__(self):
+ return ', '.join(
+ [_safe_repr(arg) for arg in self.args] +
+ [
+ '%s=%s' % (key, _safe_repr(value))
+ for key, value in self.kwargs.items()
+ ]
+ )
class ResetableIterator(object):
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..bd2a4a6
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+fake-factory>=0.5.0
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2a9acf1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index 54e4caa..8ca7e4b 100755
--- a/setup.py
+++ b/setup.py
@@ -35,7 +35,7 @@ PACKAGE = 'factory'
setup(
name='factory_boy',
version=get_version(PACKAGE),
- description="A verstile test fixtures replacement based on thoughtbot's factory_girl for Ruby.",
+ description="A versatile test fixtures replacement based on thoughtbot's factory_girl for Ruby.",
author='Mark Sandstrom',
author_email='mark@deliciouslynerdy.com',
maintainer='Raphaël Barrois',
@@ -44,11 +44,14 @@ setup(
keywords=['factory_boy', 'factory', 'fixtures'],
packages=['factory'],
license='MIT',
+ install_requires=[
+ 'fake-factory>=0.5.0',
+ ],
setup_requires=[
'setuptools>=0.8',
],
tests_require=[
- 'mock',
+ #'mock',
],
classifiers=[
"Development Status :: 5 - Production/Stable",
@@ -63,9 +66,10 @@ setup(
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Testing",
- "Topic :: Software Development :: Libraries :: Python Modules"
+ "Topic :: Software Development :: Libraries :: Python Modules",
],
test_suite='tests',
test_loader=test_loader,
diff --git a/tests/__init__.py b/tests/__init__.py
index 5b6fc55..b2c772d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
+
+# factory.django needs a configured Django.
+from .test_django import *
from .test_base import *
from .test_containers import *
from .test_declarations import *
-from .test_django import *
+from .test_faker import *
from .test_fuzzy import *
from .test_helpers import *
from .test_using import *
diff --git a/tests/alter_time.py b/tests/alter_time.py
index db0a611..aa2db3b 100644
--- a/tests/alter_time.py
+++ b/tests/alter_time.py
@@ -7,7 +7,7 @@
from __future__ import print_function
import datetime
-import mock
+from .compat import mock
real_datetime_class = datetime.datetime
diff --git a/tests/compat.py b/tests/compat.py
index ff96f13..167c185 100644
--- a/tests/compat.py
+++ b/tests/compat.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py
index fed0602..b4f8e0c 100644
--- a/tests/cyclic/bar.py
+++ b/tests/cyclic/bar.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,7 +30,8 @@ class Bar(object):
class BarFactory(factory.Factory):
- FACTORY_FOR = Bar
+ class Meta:
+ model = Bar
y = 13
foo = factory.SubFactory('cyclic.foo.FooFactory')
diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py
index e584ed1..62e58c0 100644
--- a/tests/cyclic/foo.py
+++ b/tests/cyclic/foo.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,7 +32,8 @@ class Foo(object):
class FooFactory(factory.Factory):
- FACTORY_FOR = Foo
+ class Meta:
+ model = Foo
x = 42
bar = factory.SubFactory(bar_mod.BarFactory)
diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py
new file mode 100644
index 0000000..d98b3ab
--- /dev/null
+++ b/tests/cyclic/self_ref.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011-2015 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""Helper to test circular factory dependencies."""
+
+import factory
+
+class TreeElement(object):
+ def __init__(self, name, parent):
+ self.parent = parent
+ self.name = name
+
+
+class TreeElementFactory(factory.Factory):
+ class Meta:
+ model = TreeElement
+
+ name = factory.Sequence(lambda n: "tree%s" % n)
+ parent = factory.SubFactory('tests.cyclic.self_ref.TreeElementFactory')
diff --git a/tests/djapp/models.py b/tests/djapp/models.py
index a65b50a..cadefbc 100644
--- a/tests/djapp/models.py
+++ b/tests/djapp/models.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -55,10 +55,28 @@ class ConcreteSon(AbstractBase):
pass
+class AbstractSon(AbstractBase):
+ class Meta:
+ abstract = True
+
+
+class ConcreteGrandSon(AbstractSon):
+ pass
+
+
class StandardSon(StandardModel):
pass
+class PointedModel(models.Model):
+ foo = models.CharField(max_length=20)
+
+
+class PointingModel(models.Model):
+ foo = models.CharField(max_length=20)
+ pointed = models.OneToOneField(PointedModel, related_name='pointer', null=True)
+
+
WITHFILE_UPLOAD_TO = 'django'
WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO)
@@ -70,6 +88,7 @@ if Image is not None: # PIL is available
class WithImage(models.Model):
animage = models.ImageField(upload_to=WITHFILE_UPLOAD_TO)
+ size = models.IntegerField(default=0)
else:
class WithImage(models.Model):
@@ -77,4 +96,28 @@ else:
class WithSignals(models.Model):
- foo = models.CharField(max_length=20) \ No newline at end of file
+ foo = models.CharField(max_length=20)
+
+
+class CustomManager(models.Manager):
+
+ def create(self, arg=None, **kwargs):
+ return super(CustomManager, self).create(**kwargs)
+
+
+class WithCustomManager(models.Model):
+
+ foo = models.CharField(max_length=20)
+
+ objects = CustomManager()
+
+
+class AbstractWithCustomManager(models.Model):
+ custom_objects = CustomManager()
+
+ class Meta:
+ abstract = True
+
+
+class FromAbstractWithCustomManager(AbstractWithCustomManager):
+ pass
diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py
index c1b79b0..1ef16d5 100644
--- a/tests/djapp/settings.py
+++ b/tests/djapp/settings.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,6 +34,9 @@ DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
},
+ 'replica': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ },
}
@@ -41,5 +44,6 @@ INSTALLED_APPS = [
'tests.djapp'
]
+MIDDLEWARE_CLASSES = ()
SECRET_KEY = 'testing.'
diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py
index 4255417..5d8f275 100644
--- a/tests/test_alchemy.py
+++ b/tests/test_alchemy.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2013 Romain Command&
+# Copyright (c) 2015 Romain Command&
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -23,6 +23,7 @@
import factory
from .compat import unittest
+import mock
try:
@@ -36,7 +37,8 @@ if sqlalchemy:
else:
class Fake(object):
- FACTORY_SESSION = None
+ class Meta:
+ sqlalchemy_session = None
models = Fake()
models.StandardModel = Fake()
@@ -46,16 +48,28 @@ else:
class StandardFactory(SQLAlchemyModelFactory):
- FACTORY_FOR = models.StandardModel
- FACTORY_SESSION = models.session
+ class Meta:
+ model = models.StandardModel
+ sqlalchemy_session = models.session
+
+ id = factory.Sequence(lambda n: n)
+ foo = factory.Sequence(lambda n: 'foo%d' % n)
+
+
+class ForceFlushingStandardFactory(SQLAlchemyModelFactory):
+ class Meta:
+ model = models.StandardModel
+ sqlalchemy_session = mock.MagicMock()
+ force_flush = True
id = factory.Sequence(lambda n: n)
foo = factory.Sequence(lambda n: 'foo%d' % n)
class NonIntegerPkFactory(SQLAlchemyModelFactory):
- FACTORY_FOR = models.NonIntegerPk
- FACTORY_SESSION = models.session
+ class Meta:
+ model = models.NonIntegerPk
+ sqlalchemy_session = models.session
id = factory.Sequence(lambda n: 'foo%d' % n)
@@ -66,7 +80,7 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase):
def setUp(self):
super(SQLAlchemyPkSequenceTestCase, self).setUp()
StandardFactory.reset_sequence(1)
- NonIntegerPkFactory.FACTORY_SESSION.rollback()
+ NonIntegerPkFactory._meta.sqlalchemy_session.rollback()
def test_pk_first(self):
std = StandardFactory.build()
@@ -85,18 +99,39 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase):
StandardFactory.reset_sequence()
std2 = StandardFactory.create()
- self.assertEqual('foo2', std2.foo)
- self.assertEqual(2, std2.id)
+ self.assertEqual('foo0', std2.foo)
+ self.assertEqual(0, std2.id)
def test_pk_force_value(self):
std1 = StandardFactory.create(id=10)
- self.assertEqual('foo1', std1.foo) # sequence was set before pk
+ self.assertEqual('foo1', std1.foo) # sequence and pk are unrelated
self.assertEqual(10, std1.id)
StandardFactory.reset_sequence()
std2 = StandardFactory.create()
- self.assertEqual('foo11', std2.foo)
- self.assertEqual(11, std2.id)
+ self.assertEqual('foo0', std2.foo) # Sequence doesn't care about pk
+ self.assertEqual(0, std2.id)
+
+
+@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.")
+class SQLAlchemyForceFlushTestCase(unittest.TestCase):
+ def setUp(self):
+ super(SQLAlchemyForceFlushTestCase, self).setUp()
+ ForceFlushingStandardFactory.reset_sequence(1)
+ ForceFlushingStandardFactory._meta.sqlalchemy_session.rollback()
+ ForceFlushingStandardFactory._meta.sqlalchemy_session.reset_mock()
+
+ def test_force_flush_called(self):
+ self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+ ForceFlushingStandardFactory.create()
+ self.assertTrue(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+
+ def test_force_flush_not_called(self):
+ ForceFlushingStandardFactory._meta.force_flush = False
+ self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+ ForceFlushingStandardFactory.create()
+ self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+ ForceFlushingStandardFactory._meta.force_flush = True
@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.")
@@ -104,26 +139,26 @@ class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase):
def setUp(self):
super(SQLAlchemyNonIntegerPkTestCase, self).setUp()
NonIntegerPkFactory.reset_sequence()
- NonIntegerPkFactory.FACTORY_SESSION.rollback()
+ NonIntegerPkFactory._meta.sqlalchemy_session.rollback()
def test_first(self):
nonint = NonIntegerPkFactory.build()
- self.assertEqual('foo1', nonint.id)
+ self.assertEqual('foo0', nonint.id)
def test_many(self):
nonint1 = NonIntegerPkFactory.build()
nonint2 = NonIntegerPkFactory.build()
- self.assertEqual('foo1', nonint1.id)
- self.assertEqual('foo2', nonint2.id)
+ self.assertEqual('foo0', nonint1.id)
+ self.assertEqual('foo1', nonint2.id)
def test_creation(self):
nonint1 = NonIntegerPkFactory.create()
- self.assertEqual('foo1', nonint1.id)
+ self.assertEqual('foo0', nonint1.id)
NonIntegerPkFactory.reset_sequence()
nonint2 = NonIntegerPkFactory.build()
- self.assertEqual('foo1', nonint2.id)
+ self.assertEqual('foo0', nonint2.id)
def test_force_pk(self):
nonint1 = NonIntegerPkFactory.create(id='foo10')
@@ -131,4 +166,4 @@ class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase):
NonIntegerPkFactory.reset_sequence()
nonint2 = NonIntegerPkFactory.create()
- self.assertEqual('foo1', nonint2.id)
+ self.assertEqual('foo0', nonint2.id)
diff --git a/tests/test_base.py b/tests/test_base.py
index 8cea6fc..24f64e5 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -49,11 +49,12 @@ class FakeDjangoModel(object):
class FakeModelFactory(base.Factory):
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return target_class.create(**kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return model_class.create(**kwargs)
class TestModel(FakeDjangoModel):
@@ -67,18 +68,21 @@ class SafetyTestCase(unittest.TestCase):
class AbstractFactoryTestCase(unittest.TestCase):
def test_factory_for_optional(self):
- """Ensure that FACTORY_FOR is optional for ABSTRACT_FACTORY."""
+ """Ensure that model= is optional for abstract=True."""
class TestObjectFactory(base.Factory):
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
- # Passed
+ self.assertTrue(TestObjectFactory._meta.abstract)
+ self.assertIsNone(TestObjectFactory._meta.model)
def test_factory_for_and_abstract_factory_optional(self):
- """Ensure that ABSTRACT_FACTORY is optional."""
+ """Ensure that Meta.abstract is optional."""
class TestObjectFactory(base.Factory):
pass
- # passed
+ self.assertTrue(TestObjectFactory._meta.abstract)
+ self.assertIsNone(TestObjectFactory._meta.model)
def test_abstract_factory_cannot_be_called(self):
class TestObjectFactory(base.Factory):
@@ -87,26 +91,155 @@ class AbstractFactoryTestCase(unittest.TestCase):
self.assertRaises(base.FactoryError, TestObjectFactory.build)
self.assertRaises(base.FactoryError, TestObjectFactory.create)
+ def test_abstract_factory_not_inherited(self):
+ """abstract=True isn't propagated to child classes."""
+
+ class TestObjectFactory(base.Factory):
+ class Meta:
+ abstract = True
+ model = TestObject
+
+ class TestObjectChildFactory(TestObjectFactory):
+ pass
+
+ self.assertFalse(TestObjectChildFactory._meta.abstract)
+
+ def test_abstract_or_model_is_required(self):
+ class TestObjectFactory(base.Factory):
+ class Meta:
+ abstract = False
+ model = None
+
+ self.assertRaises(base.FactoryError, TestObjectFactory.build)
+ self.assertRaises(base.FactoryError, TestObjectFactory.create)
+
+
+class OptionsTests(unittest.TestCase):
+ def test_base_attrs(self):
+ class AbstractFactory(base.Factory):
+ pass
+
+ # Declarative attributes
+ self.assertTrue(AbstractFactory._meta.abstract)
+ self.assertIsNone(AbstractFactory._meta.model)
+ self.assertEqual((), AbstractFactory._meta.inline_args)
+ self.assertEqual((), AbstractFactory._meta.exclude)
+ self.assertEqual(base.CREATE_STRATEGY, AbstractFactory._meta.strategy)
+
+ # Non-declarative attributes
+ self.assertEqual({}, AbstractFactory._meta.declarations)
+ self.assertEqual({}, AbstractFactory._meta.postgen_declarations)
+ self.assertEqual(AbstractFactory, AbstractFactory._meta.factory)
+ self.assertEqual(base.Factory, AbstractFactory._meta.base_factory)
+ self.assertEqual(AbstractFactory, AbstractFactory._meta.counter_reference)
+
+ def test_declaration_collecting(self):
+ lazy = declarations.LazyAttribute(lambda _o: 1)
+ postgen = declarations.PostGenerationDeclaration()
+
+ class AbstractFactory(base.Factory):
+ x = 1
+ y = lazy
+ z = postgen
+
+ # Declarations aren't removed
+ self.assertEqual(1, AbstractFactory.x)
+ self.assertEqual(lazy, AbstractFactory.y)
+ self.assertEqual(postgen, AbstractFactory.z)
+
+ # And are available in class Meta
+ self.assertEqual({'x': 1, 'y': lazy}, AbstractFactory._meta.declarations)
+ self.assertEqual({'z': postgen}, AbstractFactory._meta.postgen_declarations)
+
+ def test_inherited_declaration_collecting(self):
+ lazy = declarations.LazyAttribute(lambda _o: 1)
+ lazy2 = declarations.LazyAttribute(lambda _o: 2)
+ postgen = declarations.PostGenerationDeclaration()
+ postgen2 = declarations.PostGenerationDeclaration()
+
+ class AbstractFactory(base.Factory):
+ x = 1
+ y = lazy
+ z = postgen
+
+ class OtherFactory(AbstractFactory):
+ a = lazy2
+ b = postgen2
+
+ # Declarations aren't removed
+ self.assertEqual(lazy2, OtherFactory.a)
+ self.assertEqual(postgen2, OtherFactory.b)
+ self.assertEqual(1, OtherFactory.x)
+ self.assertEqual(lazy, OtherFactory.y)
+ self.assertEqual(postgen, OtherFactory.z)
+
+ # And are available in class Meta
+ self.assertEqual({'x': 1, 'y': lazy, 'a': lazy2}, OtherFactory._meta.declarations)
+ self.assertEqual({'z': postgen, 'b': postgen2}, OtherFactory._meta.postgen_declarations)
+
+ def test_inherited_declaration_shadowing(self):
+ lazy = declarations.LazyAttribute(lambda _o: 1)
+ lazy2 = declarations.LazyAttribute(lambda _o: 2)
+ postgen = declarations.PostGenerationDeclaration()
+ postgen2 = declarations.PostGenerationDeclaration()
+
+ class AbstractFactory(base.Factory):
+ x = 1
+ y = lazy
+ z = postgen
+
+ class OtherFactory(AbstractFactory):
+ y = lazy2
+ z = postgen2
+
+ # Declarations aren't removed
+ self.assertEqual(1, OtherFactory.x)
+ self.assertEqual(lazy2, OtherFactory.y)
+ self.assertEqual(postgen2, OtherFactory.z)
+
+ # And are available in class Meta
+ self.assertEqual({'x': 1, 'y': lazy2}, OtherFactory._meta.declarations)
+ self.assertEqual({'z': postgen2}, OtherFactory._meta.postgen_declarations)
+
+
+class DeclarationParsingTests(unittest.TestCase):
+ def test_classmethod(self):
+ class TestObjectFactory(base.Factory):
+ class Meta:
+ model = TestObject
+
+ @classmethod
+ def some_classmethod(cls):
+ return cls.create()
+
+ self.assertTrue(hasattr(TestObjectFactory, 'some_classmethod'))
+ obj = TestObjectFactory.some_classmethod()
+ self.assertEqual(TestObject, obj.__class__)
+
class FactoryTestCase(unittest.TestCase):
- def test_factory_for(self):
+ def test_magic_happens(self):
+ """Calling a FooFactory doesn't yield a FooFactory instance."""
class TestObjectFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
- self.assertEqual(TestObject, TestObjectFactory.FACTORY_FOR)
+ self.assertEqual(TestObject, TestObjectFactory._meta.model)
obj = TestObjectFactory.build()
- self.assertFalse(hasattr(obj, 'FACTORY_FOR'))
+ self.assertFalse(hasattr(obj, '_meta'))
def test_display(self):
class TestObjectFactory(base.Factory):
- FACTORY_FOR = FakeDjangoModel
+ class Meta:
+ model = FakeDjangoModel
self.assertIn('TestObjectFactory', str(TestObjectFactory))
self.assertIn('FakeDjangoModel', str(TestObjectFactory))
def test_lazy_attribute_non_existent_param(self):
class TestObjectFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = declarations.LazyAttribute(lambda a: a.does_not_exist )
@@ -115,12 +248,14 @@ class FactoryTestCase(unittest.TestCase):
def test_inheritance_with_sequence(self):
"""Tests that sequence IDs are shared between parent and son."""
class TestObjectFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = declarations.Sequence(lambda a: a)
class TestSubFactory(TestObjectFactory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
pass
@@ -137,7 +272,8 @@ class FactorySequenceTestCase(unittest.TestCase):
super(FactorySequenceTestCase, self).setUp()
class TestObjectFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = declarations.Sequence(lambda n: n)
self.TestObjectFactory = TestObjectFactory
@@ -212,16 +348,17 @@ class FactorySequenceTestCase(unittest.TestCase):
class FactoryDefaultStrategyTestCase(unittest.TestCase):
def setUp(self):
- self.default_strategy = base.Factory.FACTORY_STRATEGY
+ self.default_strategy = base.Factory._meta.strategy
def tearDown(self):
- base.Factory.FACTORY_STRATEGY = self.default_strategy
+ base.Factory._meta.strategy = self.default_strategy
def test_build_strategy(self):
- base.Factory.FACTORY_STRATEGY = base.BUILD_STRATEGY
+ base.Factory._meta.strategy = base.BUILD_STRATEGY
class TestModelFactory(base.Factory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -230,10 +367,11 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
self.assertFalse(test_model.id)
def test_create_strategy(self):
- # Default FACTORY_STRATEGY
+ # Default Meta.strategy
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -242,10 +380,11 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
self.assertTrue(test_model.id)
def test_stub_strategy(self):
- base.Factory.FACTORY_STRATEGY = base.STUB_STRATEGY
+ base.Factory._meta.strategy = base.STUB_STRATEGY
class TestModelFactory(base.Factory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -254,42 +393,56 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
self.assertFalse(hasattr(test_model, 'id')) # We should have a plain old object
def test_unknown_strategy(self):
- base.Factory.FACTORY_STRATEGY = 'unknown'
+ base.Factory._meta.strategy = 'unknown'
class TestModelFactory(base.Factory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
self.assertRaises(base.Factory.UnknownStrategy, TestModelFactory)
- def test_stub_with_non_stub_strategy(self):
+ def test_stub_with_create_strategy(self):
class TestModelFactory(base.StubFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
- TestModelFactory.FACTORY_STRATEGY = base.CREATE_STRATEGY
+ TestModelFactory._meta.strategy = base.CREATE_STRATEGY
self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory)
- TestModelFactory.FACTORY_STRATEGY = base.BUILD_STRATEGY
- self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory)
+ def test_stub_with_build_strategy(self):
+ class TestModelFactory(base.StubFactory):
+ class Meta:
+ model = TestModel
+
+ one = 'one'
+
+ TestModelFactory._meta.strategy = base.BUILD_STRATEGY
+ obj = TestModelFactory()
+
+ # For stubs, build() is an alias of stub().
+ self.assertFalse(isinstance(obj, TestModel))
def test_change_strategy(self):
@base.use_strategy(base.CREATE_STRATEGY)
class TestModelFactory(base.StubFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
- self.assertEqual(base.CREATE_STRATEGY, TestModelFactory.FACTORY_STRATEGY)
+ self.assertEqual(base.CREATE_STRATEGY, TestModelFactory._meta.strategy)
class FactoryCreationTestCase(unittest.TestCase):
def test_factory_for(self):
class TestFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
self.assertTrue(isinstance(TestFactory.build(), TestObject))
@@ -297,22 +450,41 @@ class FactoryCreationTestCase(unittest.TestCase):
class TestFactory(base.StubFactory):
pass
- self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY)
+ self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY)
def test_inheritance_with_stub(self):
class TestObjectFactory(base.StubFactory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
pass
class TestFactory(TestObjectFactory):
pass
- self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY)
+ self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY)
+
+ def test_stub_and_subfactory(self):
+ class StubA(base.StubFactory):
+ class Meta:
+ model = TestObject
+
+ one = 'blah'
+
+ class StubB(base.StubFactory):
+ class Meta:
+ model = TestObject
+
+ stubbed = declarations.SubFactory(StubA, two='two')
+
+ b = StubB()
+ self.assertEqual('blah', b.stubbed.one)
+ self.assertEqual('two', b.stubbed.two)
def test_custom_creation(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
@classmethod
def _prepare(cls, create, **kwargs):
@@ -335,28 +507,30 @@ class FactoryCreationTestCase(unittest.TestCase):
class Test(base.Factory):
pass
- self.assertTrue(Test._abstract_factory)
+ self.assertTrue(Test._meta.abstract)
class PostGenerationParsingTestCase(unittest.TestCase):
def test_extraction(self):
class TestObjectFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
foo = declarations.PostGenerationDeclaration()
- self.assertIn('foo', TestObjectFactory._postgen_declarations)
+ self.assertIn('foo', TestObjectFactory._meta.postgen_declarations)
def test_classlevel_extraction(self):
class TestObjectFactory(base.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
foo = declarations.PostGenerationDeclaration()
foo__bar = 42
- self.assertIn('foo', TestObjectFactory._postgen_declarations)
- self.assertIn('foo__bar', TestObjectFactory._declarations)
+ self.assertIn('foo', TestObjectFactory._meta.postgen_declarations)
+ self.assertIn('foo__bar', TestObjectFactory._meta.declarations)
diff --git a/tests/test_containers.py b/tests/test_containers.py
index 8b78dc7..083b306 100644
--- a/tests/test_containers.py
+++ b/tests/test_containers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -94,111 +94,12 @@ class LazyStubTestCase(unittest.TestCase):
class RandomObj(object):
pass
- stub = containers.LazyStub({'one': 1, 'two': 2}, target_class=RandomObj)
+ stub = containers.LazyStub({'one': 1, 'two': 2}, model_class=RandomObj)
self.assertIn('RandomObj', repr(stub))
self.assertIn('RandomObj', str(stub))
self.assertIn('one', str(stub))
-class OrderedDeclarationMock(declarations.OrderedDeclaration):
- pass
-
-
-class DeclarationDictTestCase(unittest.TestCase):
- def test_basics(self):
- one = OrderedDeclarationMock()
- two = 2
- three = OrderedDeclarationMock()
-
- d = containers.DeclarationDict(dict(one=one, two=two, three=three))
-
- self.assertTrue('one' in d)
- self.assertTrue('two' in d)
- self.assertTrue('three' in d)
-
- self.assertEqual(one, d['one'])
- self.assertEqual(two, d['two'])
- self.assertEqual(three, d['three'])
-
- self.assertEqual(one, d.pop('one'))
- self.assertFalse('one' in d)
-
- d['one'] = one
- self.assertTrue('one' in d)
- self.assertEqual(one, d['one'])
-
- self.assertEqual(set(['one', 'two', 'three']),
- set(d))
-
- def test_insert(self):
- one = OrderedDeclarationMock()
- two = 2
- three = OrderedDeclarationMock()
- four = OrderedDeclarationMock()
-
- d = containers.DeclarationDict(dict(one=one, two=two, four=four))
-
- self.assertEqual(set(['two', 'one', 'four']), set(d))
-
- d['three'] = three
- self.assertEqual(set(['two', 'one', 'three', 'four']), set(d))
-
- def test_replace(self):
- one = OrderedDeclarationMock()
- two = 2
- three = OrderedDeclarationMock()
- four = OrderedDeclarationMock()
-
- d = containers.DeclarationDict(dict(one=one, two=two, three=three))
-
- self.assertEqual(set(['two', 'one', 'three']), set(d))
-
- d['three'] = four
- self.assertEqual(set(['two', 'one', 'three']), set(d))
- self.assertEqual(set([two, one, four]), set(d.values()))
-
- def test_copy(self):
- one = OrderedDeclarationMock()
- two = 2
- three = OrderedDeclarationMock()
- four = OrderedDeclarationMock()
-
- d = containers.DeclarationDict(dict(one=one, two=two, three=three))
- d2 = d.copy({'five': 5})
-
- self.assertEqual(5, d2['five'])
- self.assertFalse('five' in d)
-
- d.pop('one')
- self.assertEqual(one, d2['one'])
-
- d2['two'] = four
- self.assertEqual(four, d2['two'])
- self.assertEqual(two, d['two'])
-
- def test_update_with_public(self):
- d = containers.DeclarationDict()
- d.update_with_public({
- 'one': 1,
- '_two': 2,
- 'three': 3,
- 'classmethod': classmethod(lambda c: 1),
- 'staticmethod': staticmethod(lambda: 1),
- })
- self.assertEqual(set(['one', 'three']), set(d))
- self.assertEqual(set([1, 3]), set(d.values()))
-
- def test_update_with_public_ignores_factory_attributes(self):
- """Ensure that a DeclarationDict ignores FACTORY_ keys."""
- d = containers.DeclarationDict()
- d.update_with_public({
- 'one': 1,
- 'FACTORY_FOR': 2,
- 'FACTORY_ARG_PARAMETERS': 3,
- })
- self.assertEqual(['one'], list(d))
- self.assertEqual([1], list(d.values()))
-
class AttributeBuilderTestCase(unittest.TestCase):
def test_empty(self):
@@ -320,7 +221,7 @@ class AttributeBuilderTestCase(unittest.TestCase):
class FakeFactory(object):
@classmethod
def declarations(cls, extra):
- d = containers.DeclarationDict({'one': 1, 'two': la})
+ d = {'one': 1, 'two': la}
d.update(extra)
return d
diff --git a/tests/test_declarations.py b/tests/test_declarations.py
index 86bc8b5..2601a38 100644
--- a/tests/test_declarations.py
+++ b/tests/test_declarations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,7 +22,6 @@
import datetime
import itertools
-import warnings
from factory import declarations
from factory import helpers
@@ -207,20 +206,6 @@ class FactoryWrapperTestCase(unittest.TestCase):
datetime.date = orig_date
-class RelatedFactoryTestCase(unittest.TestCase):
-
- def test_deprecate_name(self):
- with warnings.catch_warnings(record=True) as w:
-
- warnings.simplefilter('always')
- f = declarations.RelatedFactory('datetime.date', name='blah')
-
- self.assertEqual('blah', f.name)
- self.assertEqual(1, len(w))
- self.assertIn('RelatedFactory', str(w[0].message))
- self.assertIn('factory_related_name', str(w[0].message))
-
-
class PostGenerationMethodCallTestCase(unittest.TestCase):
def setUp(self):
self.obj = mock.MagicMock()
diff --git a/tests/test_django.py b/tests/test_django.py
index 50a67a3..103df91 100644
--- a/tests/test_django.py
+++ b/tests/test_django.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -21,9 +21,7 @@
"""Tests for factory_boy/Django interactions."""
import os
-
-import factory
-import factory.django
+from .compat import is_python2, unittest, mock
try:
@@ -31,6 +29,28 @@ try:
except ImportError: # pragma: no cover
django = None
+# Setup Django as soon as possible
+if django is not None:
+ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings')
+
+ if django.VERSION >= (1, 7, 0):
+ django.setup()
+ from django import test as django_test
+ from django.conf import settings
+ from django.db import models as django_models
+ if django.VERSION <= (1, 8, 0):
+ from django.test.simple import DjangoTestSuiteRunner
+ else:
+ from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner
+ from django.test import utils as django_test_utils
+ from django.db.models import signals
+ from .djapp import models
+
+else:
+ django_test = unittest
+
+
+
try:
from PIL import Image
except ImportError: # pragma: no cover
@@ -42,38 +62,13 @@ except ImportError: # pragma: no cover
Image = None
-from .compat import is_python2, unittest, mock
+import factory
+import factory.django
+
from . import testdata
from . import tools
-if django is not None:
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings')
-
- from django import test as django_test
- from django.conf import settings
- from django.db import models as django_models
- from django.test import simple as django_test_simple
- from django.test import utils as django_test_utils
- from django.db.models import signals
- from .djapp import models
-else: # pragma: no cover
- django_test = unittest
-
- class Fake(object):
- pass
-
- models = Fake()
- models.StandardModel = Fake
- models.StandardSon = None
- models.AbstractBase = Fake
- models.ConcreteSon = Fake
- models.NonIntegerPk = Fake
- models.WithFile = Fake
- models.WithImage = Fake
- models.WithSignals = Fake
-
-
test_state = {}
@@ -81,7 +76,7 @@ def setUpModule():
if django is None: # pragma: no cover
raise unittest.SkipTest("Django not installed")
django_test_utils.setup_test_environment()
- runner = django_test_simple.DjangoTestSuiteRunner()
+ runner = DjangoTestSuiteRunner()
runner_state = runner.setup_databases()
test_state.update({
'runner': runner,
@@ -98,54 +93,99 @@ def tearDownModule():
django_test_utils.teardown_test_environment()
-class StandardFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.StandardModel
+if django is not None:
+ class StandardFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.StandardModel
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+
+
+ class StandardFactoryWithPKField(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.StandardModel
+ django_get_or_create = ('pk',)
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+ pk = None
+
+
+ class NonIntegerPkFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.NonIntegerPk
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+ bar = ''
- foo = factory.Sequence(lambda n: "foo%d" % n)
+ class AbstractBaseFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.AbstractBase
+ abstract = True
-class StandardFactoryWithPKField(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.StandardModel
- FACTORY_DJANGO_GET_OR_CREATE = ('pk',)
+ foo = factory.Sequence(lambda n: "foo%d" % n)
- foo = factory.Sequence(lambda n: "foo%d" % n)
- pk = None
+ class ConcreteSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.ConcreteSon
-class NonIntegerPkFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.NonIntegerPk
- foo = factory.Sequence(lambda n: "foo%d" % n)
- bar = ''
+ class AbstractSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.AbstractSon
-class AbstractBaseFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.AbstractBase
- ABSTRACT_FACTORY = True
+ class ConcreteGrandSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.ConcreteGrandSon
- foo = factory.Sequence(lambda n: "foo%d" % n)
+ class WithFileFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithFile
-class ConcreteSonFactory(AbstractBaseFactory):
- FACTORY_FOR = models.ConcreteSon
+ if django is not None:
+ afile = factory.django.FileField()
-class WithFileFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.WithFile
+ class WithImageFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithImage
- if django is not None:
- afile = factory.django.FileField()
+ if django is not None:
+ animage = factory.django.ImageField()
-class WithImageFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.WithImage
+ class WithSignalsFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithSignals
- if django is not None:
- animage = factory.django.ImageField()
+ class WithCustomManagerFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithCustomManager
-class WithSignalsFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.WithSignals
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class ModelTests(django_test.TestCase):
+ def test_unset_model(self):
+ class UnsetModelFactory(factory.django.DjangoModelFactory):
+ pass
+
+ self.assertRaises(factory.FactoryError, UnsetModelFactory.create)
+
+ def test_cross_database(self):
+ class OtherDBFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.StandardModel
+ database = 'replica'
+
+ obj = OtherDBFactory()
+ self.assertFalse(models.StandardModel.objects.exists())
+ self.assertEqual(obj, models.StandardModel.objects.using('replica').get())
@unittest.skipIf(django is None, "Django not installed.")
@@ -156,32 +196,32 @@ class DjangoPkSequenceTestCase(django_test.TestCase):
def test_pk_first(self):
std = StandardFactory.build()
- self.assertEqual('foo1', std.foo)
+ self.assertEqual('foo0', std.foo)
def test_pk_many(self):
std1 = StandardFactory.build()
std2 = StandardFactory.build()
- self.assertEqual('foo1', std1.foo)
- self.assertEqual('foo2', std2.foo)
+ self.assertEqual('foo0', std1.foo)
+ self.assertEqual('foo1', std2.foo)
def test_pk_creation(self):
std1 = StandardFactory.create()
- self.assertEqual('foo1', std1.foo)
+ self.assertEqual('foo0', std1.foo)
self.assertEqual(1, std1.pk)
StandardFactory.reset_sequence()
std2 = StandardFactory.create()
- self.assertEqual('foo2', std2.foo)
+ self.assertEqual('foo0', std2.foo)
self.assertEqual(2, std2.pk)
def test_pk_force_value(self):
std1 = StandardFactory.create(pk=10)
- self.assertEqual('foo1', std1.foo) # sequence was set before pk
+ self.assertEqual('foo0', std1.foo) # sequence is unrelated to pk
self.assertEqual(10, std1.pk)
StandardFactory.reset_sequence()
std2 = StandardFactory.create()
- self.assertEqual('foo11', std2.foo)
+ self.assertEqual('foo0', std2.foo)
self.assertEqual(11, std2.pk)
@@ -194,12 +234,12 @@ class DjangoPkForceTestCase(django_test.TestCase):
def test_no_pk(self):
std = StandardFactoryWithPKField()
self.assertIsNotNone(std.pk)
- self.assertEqual('foo1', std.foo)
+ self.assertEqual('foo0', std.foo)
def test_force_pk(self):
std = StandardFactoryWithPKField(pk=42)
self.assertIsNotNone(std.pk)
- self.assertEqual('foo1', std.foo)
+ self.assertEqual('foo0', std.foo)
def test_reuse_pk(self):
std1 = StandardFactoryWithPKField(foo='bar')
@@ -212,17 +252,20 @@ class DjangoPkForceTestCase(django_test.TestCase):
@unittest.skipIf(django is None, "Django not installed.")
class DjangoModelLoadingTestCase(django_test.TestCase):
- """Tests FACTORY_FOR = 'app.Model' pattern."""
+ """Tests class Meta:
+ model = 'app.Model' pattern."""
def test_loading(self):
class ExampleFactory(factory.DjangoModelFactory):
- FACTORY_FOR = 'djapp.StandardModel'
+ class Meta:
+ model = 'djapp.StandardModel'
- self.assertEqual(models.StandardModel, ExampleFactory._get_target_class())
+ self.assertEqual(models.StandardModel, ExampleFactory._get_model_class())
def test_building(self):
class ExampleFactory(factory.DjangoModelFactory):
- FACTORY_FOR = 'djapp.StandardModel'
+ class Meta:
+ model = 'djapp.StandardModel'
e = ExampleFactory.build()
self.assertEqual(models.StandardModel, e.__class__)
@@ -233,7 +276,8 @@ class DjangoModelLoadingTestCase(django_test.TestCase):
See https://github.com/rbarrois/factory_boy/issues/109.
"""
class ExampleFactory(factory.DjangoModelFactory):
- FACTORY_FOR = 'djapp.StandardModel'
+ class Meta:
+ model = 'djapp.StandardModel'
class Example2Factory(ExampleFactory):
pass
@@ -247,14 +291,16 @@ class DjangoModelLoadingTestCase(django_test.TestCase):
See https://github.com/rbarrois/factory_boy/issues/109.
"""
class ExampleFactory(factory.DjangoModelFactory):
- FACTORY_FOR = 'djapp.StandardModel'
+ class Meta:
+ model = 'djapp.StandardModel'
foo = factory.Sequence(lambda n: n)
class Example2Factory(ExampleFactory):
- FACTORY_FOR = 'djapp.StandardSon'
+ class Meta:
+ model = 'djapp.StandardSon'
- self.assertEqual(models.StandardSon, Example2Factory._get_target_class())
+ self.assertEqual(models.StandardSon, Example2Factory._get_model_class())
e1 = ExampleFactory.build()
e2 = Example2Factory.build()
@@ -262,9 +308,9 @@ class DjangoModelLoadingTestCase(django_test.TestCase):
self.assertEqual(models.StandardModel, e1.__class__)
self.assertEqual(models.StandardSon, e2.__class__)
self.assertEqual(models.StandardModel, e3.__class__)
- self.assertEqual(1, e1.foo)
- self.assertEqual(2, e2.foo)
- self.assertEqual(3, e3.foo)
+ self.assertEqual(0, e1.foo)
+ self.assertEqual(1, e2.foo)
+ self.assertEqual(2, e3.foo)
@unittest.skipIf(django is None, "Django not installed.")
@@ -275,23 +321,23 @@ class DjangoNonIntegerPkTestCase(django_test.TestCase):
def test_first(self):
nonint = NonIntegerPkFactory.build()
- self.assertEqual('foo1', nonint.foo)
+ self.assertEqual('foo0', nonint.foo)
def test_many(self):
nonint1 = NonIntegerPkFactory.build()
nonint2 = NonIntegerPkFactory.build()
- self.assertEqual('foo1', nonint1.foo)
- self.assertEqual('foo2', nonint2.foo)
+ self.assertEqual('foo0', nonint1.foo)
+ self.assertEqual('foo1', nonint2.foo)
def test_creation(self):
nonint1 = NonIntegerPkFactory.create()
- self.assertEqual('foo1', nonint1.foo)
- self.assertEqual('foo1', nonint1.pk)
+ self.assertEqual('foo0', nonint1.foo)
+ self.assertEqual('foo0', nonint1.pk)
NonIntegerPkFactory.reset_sequence()
nonint2 = NonIntegerPkFactory.build()
- self.assertEqual('foo1', nonint2.foo)
+ self.assertEqual('foo0', nonint2.foo)
def test_force_pk(self):
nonint1 = NonIntegerPkFactory.create(pk='foo10')
@@ -300,19 +346,50 @@ class DjangoNonIntegerPkTestCase(django_test.TestCase):
NonIntegerPkFactory.reset_sequence()
nonint2 = NonIntegerPkFactory.create()
- self.assertEqual('foo1', nonint2.foo)
- self.assertEqual('foo1', nonint2.pk)
+ self.assertEqual('foo0', nonint2.foo)
+ self.assertEqual('foo0', nonint2.pk)
@unittest.skipIf(django is None, "Django not installed.")
class DjangoAbstractBaseSequenceTestCase(django_test.TestCase):
def test_auto_sequence(self):
- with factory.debug():
- obj = ConcreteSonFactory()
+ """The sequence of the concrete son of an abstract model should be autonomous."""
+ obj = ConcreteSonFactory()
+ self.assertEqual(1, obj.pk)
+
+ def test_auto_sequence(self):
+ """The sequence of the concrete grandson of an abstract model should be autonomous."""
+ obj = ConcreteGrandSonFactory()
self.assertEqual(1, obj.pk)
@unittest.skipIf(django is None, "Django not installed.")
+class DjangoRelatedFieldTestCase(django_test.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(DjangoRelatedFieldTestCase, cls).setUpClass()
+ class PointedFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.PointedModel
+ foo = 'ahah'
+
+ class PointerFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.PointingModel
+ pointed = factory.SubFactory(PointedFactory, foo='hihi')
+ foo = 'bar'
+
+ cls.PointedFactory = PointedFactory
+ cls.PointerFactory = PointerFactory
+
+ def test_direct_related_create(self):
+ ptr = self.PointerFactory()
+ self.assertEqual('hihi', ptr.pointed.foo)
+ self.assertEqual(ptr.pointed, models.PointedModel.objects.get())
+
+
+@unittest.skipIf(django is None, "Django not installed.")
class DjangoFileFieldTestCase(unittest.TestCase):
def tearDown(self):
@@ -325,6 +402,9 @@ class DjangoFileFieldTestCase(unittest.TestCase):
o = WithFileFactory.build()
self.assertIsNone(o.pk)
self.assertEqual(b'', o.afile.read())
+ self.assertEqual('example.dat', o.afile.name)
+
+ o.save()
self.assertEqual('django/example.dat', o.afile.name)
def test_default_create(self):
@@ -336,19 +416,26 @@ class DjangoFileFieldTestCase(unittest.TestCase):
def test_with_content(self):
o = WithFileFactory.build(afile__data='foo')
self.assertIsNone(o.pk)
+
+ # Django only allocates the full path on save()
+ o.save()
self.assertEqual(b'foo', o.afile.read())
self.assertEqual('django/example.dat', o.afile.name)
def test_with_file(self):
with open(testdata.TESTFILE_PATH, 'rb') as f:
o = WithFileFactory.build(afile__from_file=f)
- self.assertIsNone(o.pk)
+ o.save()
+
self.assertEqual(b'example_data\n', o.afile.read())
self.assertEqual('django/example.data', o.afile.name)
def test_with_path(self):
o = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH)
self.assertIsNone(o.pk)
+
+ # Django only allocates the full path on save()
+ o.save()
self.assertEqual(b'example_data\n', o.afile.read())
self.assertEqual('django/example.data', o.afile.name)
@@ -358,7 +445,9 @@ class DjangoFileFieldTestCase(unittest.TestCase):
afile__from_file=f,
afile__from_path=''
)
- self.assertIsNone(o.pk)
+ # Django only allocates the full path on save()
+ o.save()
+
self.assertEqual(b'example_data\n', o.afile.read())
self.assertEqual('django/example.data', o.afile.name)
@@ -368,6 +457,9 @@ class DjangoFileFieldTestCase(unittest.TestCase):
afile__from_file=None,
)
self.assertIsNone(o.pk)
+
+ # Django only allocates the full path on save()
+ o.save()
self.assertEqual(b'example_data\n', o.afile.read())
self.assertEqual('django/example.data', o.afile.name)
@@ -383,16 +475,24 @@ class DjangoFileFieldTestCase(unittest.TestCase):
afile__filename='example.foo',
)
self.assertIsNone(o.pk)
+
+ # Django only allocates the full path on save()
+ o.save()
self.assertEqual(b'example_data\n', o.afile.read())
self.assertEqual('django/example.foo', o.afile.name)
def test_existing_file(self):
o1 = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH)
+ o1.save()
+ self.assertEqual('django/example.data', o1.afile.name)
- o2 = WithFileFactory.build(afile=o1.afile)
+ o2 = WithFileFactory.build(afile__from_file=o1.afile)
self.assertIsNone(o2.pk)
+ o2.save()
+
self.assertEqual(b'example_data\n', o2.afile.read())
- self.assertEqual('django/example_1.data', o2.afile.name)
+ self.assertNotEqual('django/example.data', o2.afile.name)
+ self.assertRegexpMatches(o2.afile.name, r'django/example_\w+.data')
def test_no_file(self):
o = WithFileFactory.build(afile=None)
@@ -413,6 +513,8 @@ class DjangoImageFieldTestCase(unittest.TestCase):
def test_default_build(self):
o = WithImageFactory.build()
self.assertIsNone(o.pk)
+ o.save()
+
self.assertEqual(100, o.animage.width)
self.assertEqual(100, o.animage.height)
self.assertEqual('django/example.jpg', o.animage.name)
@@ -420,13 +522,28 @@ class DjangoImageFieldTestCase(unittest.TestCase):
def test_default_create(self):
o = WithImageFactory.create()
self.assertIsNotNone(o.pk)
+ o.save()
+
self.assertEqual(100, o.animage.width)
self.assertEqual(100, o.animage.height)
self.assertEqual('django/example.jpg', o.animage.name)
+ def test_complex_create(self):
+ o = WithImageFactory.create(
+ size=10,
+ animage__filename=factory.Sequence(lambda n: 'img%d.jpg' % n),
+ __sequence=42,
+ animage__width=factory.SelfAttribute('..size'),
+ animage__height=factory.SelfAttribute('width'),
+ )
+ self.assertIsNotNone(o.pk)
+ self.assertEqual('django/img42.jpg', o.animage.name)
+
def test_with_content(self):
o = WithImageFactory.build(animage__width=13, animage__color='red')
self.assertIsNone(o.pk)
+ o.save()
+
self.assertEqual(13, o.animage.width)
self.assertEqual(13, o.animage.height)
self.assertEqual('django/example.jpg', o.animage.name)
@@ -440,20 +557,23 @@ class DjangoImageFieldTestCase(unittest.TestCase):
def test_gif(self):
o = WithImageFactory.build(animage__width=13, animage__color='blue', animage__format='GIF')
self.assertIsNone(o.pk)
+ o.save()
+
self.assertEqual(13, o.animage.width)
self.assertEqual(13, o.animage.height)
self.assertEqual('django/example.jpg', o.animage.name)
i = Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name))
- colors = i.getcolors()
- # 169 pixels with color 190 from the GIF palette
- self.assertEqual([(169, 190)], colors)
+ colors = i.convert('RGB').getcolors()
+ # 169 pixels with rgb(0, 0, 255)
+ self.assertEqual([(169, (0, 0, 255))], colors)
self.assertEqual('GIF', i.format)
def test_with_file(self):
with open(testdata.TESTIMAGE_PATH, 'rb') as f:
o = WithImageFactory.build(animage__from_file=f)
- self.assertIsNone(o.pk)
+ o.save()
+
# Image file for a 42x42 green jpeg: 301 bytes long.
self.assertEqual(301, len(o.animage.read()))
self.assertEqual('django/example.jpeg', o.animage.name)
@@ -461,6 +581,8 @@ class DjangoImageFieldTestCase(unittest.TestCase):
def test_with_path(self):
o = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH)
self.assertIsNone(o.pk)
+ o.save()
+
# Image file for a 42x42 green jpeg: 301 bytes long.
self.assertEqual(301, len(o.animage.read()))
self.assertEqual('django/example.jpeg', o.animage.name)
@@ -471,7 +593,8 @@ class DjangoImageFieldTestCase(unittest.TestCase):
animage__from_file=f,
animage__from_path=''
)
- self.assertIsNone(o.pk)
+ o.save()
+
# Image file for a 42x42 green jpeg: 301 bytes long.
self.assertEqual(301, len(o.animage.read()))
self.assertEqual('django/example.jpeg', o.animage.name)
@@ -482,6 +605,8 @@ class DjangoImageFieldTestCase(unittest.TestCase):
animage__from_file=None,
)
self.assertIsNone(o.pk)
+ o.save()
+
# Image file for a 42x42 green jpeg: 301 bytes long.
self.assertEqual(301, len(o.animage.read()))
self.assertEqual('django/example.jpeg', o.animage.name)
@@ -498,18 +623,24 @@ class DjangoImageFieldTestCase(unittest.TestCase):
animage__filename='example.foo',
)
self.assertIsNone(o.pk)
+ o.save()
+
# Image file for a 42x42 green jpeg: 301 bytes long.
self.assertEqual(301, len(o.animage.read()))
self.assertEqual('django/example.foo', o.animage.name)
def test_existing_file(self):
o1 = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH)
+ o1.save()
- o2 = WithImageFactory.build(animage=o1.animage)
+ o2 = WithImageFactory.build(animage__from_file=o1.animage)
self.assertIsNone(o2.pk)
+ o2.save()
+
# Image file for a 42x42 green jpeg: 301 bytes long.
self.assertEqual(301, len(o2.animage.read()))
- self.assertEqual('django/example_1.jpeg', o2.animage.name)
+ self.assertNotEqual('django/example.jpeg', o2.animage.name)
+ self.assertRegexpMatches(o2.animage.name, r'django/example_\w+.jpeg')
def test_no_file(self):
o = WithImageFactory.build(animage=None)
@@ -547,10 +678,24 @@ class PreventSignalsTestCase(unittest.TestCase):
self.assertSignalsReactivated()
+ def test_signal_cache(self):
+ with factory.django.mute_signals(signals.pre_save, signals.post_save):
+ signals.post_save.connect(self.handlers.mute_block_receiver)
+ WithSignalsFactory()
+
+ self.assertTrue(self.handlers.mute_block_receiver.call_count, 1)
+ self.assertEqual(self.handlers.pre_init.call_count, 1)
+ self.assertFalse(self.handlers.pre_save.called)
+ self.assertFalse(self.handlers.post_save.called)
+
+ self.assertSignalsReactivated()
+ self.assertTrue(self.handlers.mute_block_receiver.call_count, 1)
+
def test_class_decorator(self):
@factory.django.mute_signals(signals.pre_save, signals.post_save)
class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.WithSignals
+ class Meta:
+ model = models.WithSignals
WithSignalsDecoratedFactory()
@@ -560,10 +705,32 @@ class PreventSignalsTestCase(unittest.TestCase):
self.assertSignalsReactivated()
+ def test_class_decorator_with_subfactory(self):
+ @factory.django.mute_signals(signals.pre_save, signals.post_save)
+ class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithSignals
+
+ @factory.post_generation
+ def post(obj, create, extracted, **kwargs):
+ if not extracted:
+ WithSignalsDecoratedFactory.create(post=42)
+
+ # This will disable the signals (twice), create two objects,
+ # and reactivate the signals.
+ WithSignalsDecoratedFactory()
+
+ self.assertEqual(self.handlers.pre_init.call_count, 2)
+ self.assertFalse(self.handlers.pre_save.called)
+ self.assertFalse(self.handlers.post_save.called)
+
+ self.assertSignalsReactivated()
+
def test_class_decorator_build(self):
@factory.django.mute_signals(signals.pre_save, signals.post_save)
class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = models.WithSignals
+ class Meta:
+ model = models.WithSignals
WithSignalsDecoratedFactory.build()
@@ -601,5 +768,21 @@ class PreventSignalsTestCase(unittest.TestCase):
self.assertSignalsReactivated()
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoCustomManagerTestCase(unittest.TestCase):
+
+ def test_extra_args(self):
+ # Our CustomManager will remove the 'arg=' argument.
+ model = WithCustomManagerFactory(arg='foo')
+
+ def test_with_manager_on_abstract(self):
+ class ObjFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.FromAbstractWithCustomManager
+
+ # Our CustomManager will remove the 'arg=' argument,
+ # invalid for the actual model.
+ ObjFactory.create(arg='invalid')
+
if __name__ == '__main__': # pragma: no cover
unittest.main()
diff --git a/tests/test_faker.py b/tests/test_faker.py
new file mode 100644
index 0000000..99e54af
--- /dev/null
+++ b/tests/test_faker.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2015 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+import unittest
+
+import faker.providers
+
+import factory
+
+
+class MockFaker(object):
+ def __init__(self, expected):
+ self.expected = expected
+
+ def format(self, provider, **kwargs):
+ return self.expected[provider]
+
+
+class FakerTests(unittest.TestCase):
+ def setUp(self):
+ self._real_fakers = factory.Faker._FAKER_REGISTRY
+ factory.Faker._FAKER_REGISTRY = {}
+
+ def tearDown(self):
+ factory.Faker._FAKER_REGISTRY = self._real_fakers
+
+ def _setup_mock_faker(self, locale=None, **definitions):
+ if locale is None:
+ locale = factory.Faker._DEFAULT_LOCALE
+ factory.Faker._FAKER_REGISTRY[locale] = MockFaker(definitions)
+
+ def test_simple_biased(self):
+ self._setup_mock_faker(name="John Doe")
+ faker_field = factory.Faker('name')
+ self.assertEqual("John Doe", faker_field.generate({}))
+
+ def test_full_factory(self):
+ class Profile(object):
+ def __init__(self, first_name, last_name, email):
+ self.first_name = first_name
+ self.last_name = last_name
+ self.email = email
+
+ class ProfileFactory(factory.Factory):
+ class Meta:
+ model = Profile
+ first_name = factory.Faker('first_name')
+ last_name = factory.Faker('last_name', locale='fr_FR')
+ email = factory.Faker('email')
+
+ self._setup_mock_faker(first_name="John", last_name="Doe", email="john.doe@example.org")
+ self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@exemple.fr", locale='fr_FR')
+
+ profile = ProfileFactory()
+ self.assertEqual("John", profile.first_name)
+ self.assertEqual("Valjean", profile.last_name)
+ self.assertEqual('john.doe@example.org', profile.email)
+
+ def test_override_locale(self):
+ class Profile(object):
+ def __init__(self, first_name, last_name):
+ self.first_name = first_name
+ self.last_name = last_name
+
+ class ProfileFactory(factory.Factory):
+ class Meta:
+ model = Profile
+
+ first_name = factory.Faker('first_name')
+ last_name = factory.Faker('last_name', locale='fr_FR')
+
+ self._setup_mock_faker(first_name="John", last_name="Doe")
+ self._setup_mock_faker(first_name="Jean", last_name="Valjean", locale='fr_FR')
+ self._setup_mock_faker(first_name="Johannes", last_name="Brahms", locale='de_DE')
+
+ profile = ProfileFactory()
+ self.assertEqual("John", profile.first_name)
+ self.assertEqual("Valjean", profile.last_name)
+
+ with factory.Faker.override_default_locale('de_DE'):
+ profile = ProfileFactory()
+ self.assertEqual("Johannes", profile.first_name)
+ self.assertEqual("Valjean", profile.last_name)
+
+ profile = ProfileFactory()
+ self.assertEqual("John", profile.first_name)
+ self.assertEqual("Valjean", profile.last_name)
+
+ def test_add_provider(self):
+ class Face(object):
+ def __init__(self, smiley, french_smiley):
+ self.smiley = smiley
+ self.french_smiley = french_smiley
+
+ class FaceFactory(factory.Factory):
+ class Meta:
+ model = Face
+
+ smiley = factory.Faker('smiley')
+ french_smiley = factory.Faker('smiley', locale='fr_FR')
+
+ class SmileyProvider(faker.providers.BaseProvider):
+ def smiley(self):
+ return ':)'
+
+ class FrenchSmileyProvider(faker.providers.BaseProvider):
+ def smiley(self):
+ return '(:'
+
+ factory.Faker.add_provider(SmileyProvider)
+ factory.Faker.add_provider(FrenchSmileyProvider, 'fr_FR')
+
+ face = FaceFactory()
+ self.assertEqual(":)", face.smiley)
+ self.assertEqual("(:", face.french_smiley)
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index 1caeb0a..4c3873a 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -55,7 +55,7 @@ class FuzzyChoiceTestCase(unittest.TestCase):
d = fuzzy.FuzzyChoice(options)
- with mock.patch('random.choice', fake_choice):
+ with mock.patch('factory.fuzzy._random.choice', fake_choice):
res = d.evaluate(2, None, False)
self.assertEqual(6, res)
@@ -74,6 +74,24 @@ class FuzzyChoiceTestCase(unittest.TestCase):
res = d.evaluate(2, None, False)
self.assertIn(res, [0, 1, 2])
+ def test_lazy_generator(self):
+ class Gen(object):
+ def __init__(self, options):
+ self.options = options
+ self.unrolled = False
+
+ def __iter__(self):
+ self.unrolled = True
+ return iter(self.options)
+
+ opts = Gen([1, 2, 3])
+ d = fuzzy.FuzzyChoice(opts)
+ self.assertFalse(opts.unrolled)
+
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, [1, 2, 3])
+ self.assertTrue(opts.unrolled)
+
class FuzzyIntegerTestCase(unittest.TestCase):
def test_definition(self):
@@ -93,7 +111,7 @@ class FuzzyIntegerTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyInteger(2, 8)
- with mock.patch('random.randrange', fake_randrange):
+ with mock.patch('factory.fuzzy._random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
self.assertEqual((2 + 8 + 1) * 1, res)
@@ -103,7 +121,7 @@ class FuzzyIntegerTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyInteger(8)
- with mock.patch('random.randrange', fake_randrange):
+ with mock.patch('factory.fuzzy._random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
self.assertEqual((0 + 8 + 1) * 1, res)
@@ -113,7 +131,7 @@ class FuzzyIntegerTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyInteger(5, 8, 3)
- with mock.patch('random.randrange', fake_randrange):
+ with mock.patch('factory.fuzzy._random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
self.assertEqual((5 + 8 + 1) * 3, res)
@@ -146,7 +164,7 @@ class FuzzyDecimalTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDecimal(2.0, 8.0)
- with mock.patch('random.uniform', fake_uniform):
+ with mock.patch('factory.fuzzy._random.uniform', fake_uniform):
res = fuzz.evaluate(2, None, False)
self.assertEqual(decimal.Decimal('10.0'), res)
@@ -156,7 +174,7 @@ class FuzzyDecimalTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDecimal(8.0)
- with mock.patch('random.uniform', fake_uniform):
+ with mock.patch('factory.fuzzy._random.uniform', fake_uniform):
res = fuzz.evaluate(2, None, False)
self.assertEqual(decimal.Decimal('8.0'), res)
@@ -166,11 +184,24 @@ class FuzzyDecimalTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDecimal(8.0, precision=3)
- with mock.patch('random.uniform', fake_uniform):
+ with mock.patch('factory.fuzzy._random.uniform', fake_uniform):
res = fuzz.evaluate(2, None, False)
self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res)
+ @unittest.skipIf(compat.PY2, "decimal.FloatOperation was added in Py3")
+ def test_no_approximation(self):
+ """We should not go through floats in our fuzzy calls unless actually needed."""
+ fuzz = fuzzy.FuzzyDecimal(0, 10)
+
+ decimal_context = decimal.getcontext()
+ old_traps = decimal_context.traps[decimal.FloatOperation]
+ try:
+ decimal_context.traps[decimal.FloatOperation] = True
+ fuzz.evaluate(2, None, None)
+ finally:
+ decimal_context.traps[decimal.FloatOperation] = old_traps
+
class FuzzyDateTestCase(unittest.TestCase):
@classmethod
@@ -214,7 +245,7 @@ class FuzzyDateTestCase(unittest.TestCase):
fake_randint = lambda low, high: (low + high) // 2
fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.date(2013, 1, 16), res)
@@ -225,7 +256,7 @@ class FuzzyDateTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDate(self.jan1)
fake_randint = lambda low, high: (low + high) // 2
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.date(2013, 1, 2), res)
@@ -332,7 +363,7 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase):
fake_randint = lambda low, high: (low + high) // 2
fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 16), res)
@@ -343,7 +374,7 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1)
fake_randint = lambda low, high: (low + high) // 2
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 2), res)
@@ -450,7 +481,7 @@ class FuzzyDateTimeTestCase(unittest.TestCase):
fake_randint = lambda low, high: (low + high) // 2
fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res)
@@ -461,7 +492,7 @@ class FuzzyDateTimeTestCase(unittest.TestCase):
fuzz = fuzzy.FuzzyDateTime(self.jan1)
fake_randint = lambda low, high: (low + high) // 2
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('factory.fuzzy._random.randint', fake_randint):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res)
@@ -486,7 +517,7 @@ class FuzzyTextTestCase(unittest.TestCase):
chars = ['a', 'b', 'c']
fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4)
- with mock.patch('random.choice', fake_choice):
+ with mock.patch('factory.fuzzy._random.choice', fake_choice):
res = fuzz.evaluate(2, None, False)
self.assertEqual('preaaaapost', res)
@@ -504,3 +535,25 @@ class FuzzyTextTestCase(unittest.TestCase):
for char in res:
self.assertIn(char, ['a', 'b', 'c'])
+
+
+class FuzzyRandomTestCase(unittest.TestCase):
+ def test_seeding(self):
+ fuzz = fuzzy.FuzzyInteger(1, 1000)
+
+ fuzzy.reseed_random(42)
+ value = fuzz.evaluate(sequence=1, obj=None, create=False)
+
+ fuzzy.reseed_random(42)
+ value2 = fuzz.evaluate(sequence=1, obj=None, create=False)
+ self.assertEqual(value, value2)
+
+ def test_reset_state(self):
+ fuzz = fuzzy.FuzzyInteger(1, 1000)
+
+ state = fuzzy.get_random_state()
+ value = fuzz.evaluate(sequence=1, obj=None, create=False)
+
+ fuzzy.set_random_state(state)
+ value2 = fuzz.evaluate(sequence=1, obj=None, create=False)
+ self.assertEqual(value, value2)
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index f5a66e5..bee66ca 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py
index 803607a..148d274 100644
--- a/tests/test_mongoengine.py
+++ b/tests/test_mongoengine.py
@@ -31,6 +31,9 @@ try:
except ImportError:
mongoengine = None
+if os.environ.get('SKIP_MONGOENGINE') == '1':
+ mongoengine = None
+
if mongoengine:
from factory.mongoengine import MongoEngineFactory
@@ -42,12 +45,14 @@ if mongoengine:
address = mongoengine.EmbeddedDocumentField(Address)
class AddressFactory(MongoEngineFactory):
- FACTORY_FOR = Address
+ class Meta:
+ model = Address
street = factory.Sequence(lambda n: 'street%d' % n)
class PersonFactory(MongoEngineFactory):
- FACTORY_FOR = Person
+ class Meta:
+ model = Person
name = factory.Sequence(lambda n: 'name%d' % n)
address = factory.SubFactory(AddressFactory)
@@ -59,10 +64,20 @@ class MongoEngineTestCase(unittest.TestCase):
db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test')
db_host = os.environ.get('MONGO_HOST', 'localhost')
db_port = int(os.environ.get('MONGO_PORT', '27017'))
+ server_timeout_ms = int(os.environ.get('MONGO_TIMEOUT', '300'))
@classmethod
def setUpClass(cls):
- cls.db = mongoengine.connect(cls.db_name, host=cls.db_host, port=cls.db_port)
+ from pymongo import read_preferences as mongo_rp
+ cls.db = mongoengine.connect(
+ db=cls.db_name,
+ host=cls.db_host,
+ port=cls.db_port,
+ # PyMongo>=2.1 requires an explicit read_preference.
+ read_preference=mongo_rp.ReadPreference.PRIMARY,
+ # PyMongo>=2.1 has a 20s timeout, use 100ms instead
+ serverselectiontimeoutms=cls.server_timeout_ms,
+ )
@classmethod
def tearDownClass(cls):
diff --git a/tests/test_using.py b/tests/test_using.py
index 3979cd0..0a893c1 100644
--- a/tests/test_using.py
+++ b/tests/test_using.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -69,6 +69,9 @@ class FakeModel(object):
def order_by(self, *args, **kwargs):
return [1]
+ def using(self, db):
+ return self
+
objects = FakeModelManager()
def __init__(self, **kwargs):
@@ -78,11 +81,12 @@ class FakeModel(object):
class FakeModelFactory(factory.Factory):
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return target_class.create(**kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return model_class.create(**kwargs)
class TestModel(FakeModel):
@@ -292,17 +296,19 @@ class SimpleBuildTestCase(unittest.TestCase):
class UsingFactoryTestCase(unittest.TestCase):
def test_attribute(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'one'
test_object = TestObjectFactory.build()
self.assertEqual(test_object.one, 'one')
- def test_inheriting_target_class(self):
+ def test_inheriting_model_class(self):
@factory.use_strategy(factory.BUILD_STRATEGY)
class TestObjectFactory(factory.Factory, TestObject):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'one'
@@ -311,18 +317,22 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_abstract(self):
class SomeAbstractFactory(factory.Factory):
- ABSTRACT_FACTORY = True
+ class Meta:
+ abstract = True
+
one = 'one'
class InheritedFactory(SomeAbstractFactory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
test_object = InheritedFactory.build()
self.assertEqual(test_object.one, 'one')
def test_sequence(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: 'one%d' % n)
two = factory.Sequence(lambda n: 'two%d' % n)
@@ -337,7 +347,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_sequence_custom_begin(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@classmethod
def _setup_next_sequence(cls):
@@ -356,7 +367,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_sequence_override(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: 'one%d' % n)
@@ -372,13 +384,14 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_custom_create(self):
class TestModelFactory(factory.Factory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
two = 2
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- obj = target_class.create(**kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ obj = model_class.create(**kwargs)
obj.properly_created = True
return obj
@@ -395,7 +408,8 @@ class UsingFactoryTestCase(unittest.TestCase):
self.y = y
class NonDjangoFactory(factory.Factory):
- FACTORY_FOR = NonDjango
+ class Meta:
+ model = NonDjango
x = 3
@@ -405,7 +419,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_sequence_batch(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: 'one%d' % n)
two = factory.Sequence(lambda n: 'two%d' % n)
@@ -420,7 +435,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_lazy_attribute(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.LazyAttribute(lambda a: 'abc' )
two = factory.LazyAttribute(lambda a: a.one + ' xyz')
@@ -431,7 +447,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_lazy_attribute_sequence(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.LazyAttributeSequence(lambda a, n: 'abc%d' % n)
two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz%d' % n)
@@ -446,7 +463,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_lazy_attribute_decorator(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@factory.lazy_attribute
def one(a):
@@ -460,7 +478,8 @@ class UsingFactoryTestCase(unittest.TestCase):
n = 3
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'xx'
two = factory.SelfAttribute('one')
@@ -479,12 +498,14 @@ class UsingFactoryTestCase(unittest.TestCase):
pass
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 3
three = factory.SelfAttribute('..bar')
class TestModel2Factory(FakeModelFactory):
- FACTORY_FOR = TestModel2
+ class Meta:
+ model = TestModel2
bar = 4
two = factory.SubFactory(TestModelFactory, one=1)
@@ -493,7 +514,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_sequence_decorator(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@factory.sequence
def one(n):
@@ -504,7 +526,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_lazy_attribute_sequence_decorator(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@factory.lazy_attribute_sequence
def one(a, n):
@@ -519,7 +542,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_build_with_parameters(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: 'one%d' % n)
two = factory.Sequence(lambda n: 'two%d' % n)
@@ -535,7 +559,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_create(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -545,7 +570,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_create_batch(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -561,7 +587,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_generate_build(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -571,7 +598,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_generate_create(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -581,7 +609,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_generate_stub(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -591,7 +620,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_generate_batch_build(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -607,7 +637,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_generate_batch_create(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -623,7 +654,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_generate_batch_stub(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -639,7 +671,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_simple_generate_build(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -649,7 +682,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_simple_generate_create(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -659,7 +693,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_simple_generate_batch_build(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -675,7 +710,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_simple_generate_batch_create(self):
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 'one'
@@ -691,7 +727,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_stub_batch(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'one'
two = factory.LazyAttribute(lambda a: a.one + ' two')
@@ -710,13 +747,15 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_inheritance(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'one'
two = factory.LazyAttribute(lambda a: a.one + ' two')
class TestObjectFactory2(TestObjectFactory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
three = 'three'
four = factory.LazyAttribute(lambda a: a.three + ' four')
@@ -730,15 +769,48 @@ class UsingFactoryTestCase(unittest.TestCase):
test_object_alt = TestObjectFactory.build()
self.assertEqual(None, test_object_alt.three)
+ def test_override_inherited(self):
+ """Overriding inherited declarations"""
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ one = 'one'
+
+ class TestObjectFactory2(TestObjectFactory):
+ one = 'two'
+
+ test_object = TestObjectFactory2.build()
+ self.assertEqual('two', test_object.one)
+
+ def test_override_inherited_deep(self):
+ """Overriding inherited declarations"""
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ one = 'one'
+
+ class TestObjectFactory2(TestObjectFactory):
+ one = 'two'
+
+ class TestObjectFactory3(TestObjectFactory2):
+ pass
+
+ test_object = TestObjectFactory3.build()
+ self.assertEqual('two', test_object.one)
+
def test_inheritance_and_sequences(self):
"""Sequence counters should be kept within an inheritance chain."""
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: n)
class TestObjectFactory2(TestObjectFactory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
to1a = TestObjectFactory()
self.assertEqual(0, to1a.one)
@@ -755,12 +827,14 @@ class UsingFactoryTestCase(unittest.TestCase):
pass
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: n)
class TestObjectFactory2(TestObjectFactory):
- FACTORY_FOR = TestObject2
+ class Meta:
+ model = TestObject2
to1a = TestObjectFactory()
self.assertEqual(0, to1a.one)
@@ -785,12 +859,14 @@ class UsingFactoryTestCase(unittest.TestCase):
self.one = one
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: n)
class TestObjectFactory2(TestObjectFactory):
- FACTORY_FOR = TestObject2
+ class Meta:
+ model = TestObject2
to1a = TestObjectFactory()
self.assertEqual(0, to1a.one)
@@ -804,7 +880,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_inheritance_with_inherited_class(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'one'
two = factory.LazyAttribute(lambda a: a.one + ' two')
@@ -821,12 +898,14 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_dual_inheritance(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 'one'
class TestOtherFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
two = 'two'
four = 'four'
@@ -841,7 +920,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_class_method_accessible(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@classmethod
def alt_create(cls, **kwargs):
@@ -851,7 +931,8 @@ class UsingFactoryTestCase(unittest.TestCase):
def test_static_method_accessible(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@staticmethod
def alt_create(**kwargs):
@@ -859,15 +940,16 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1})
- def test_arg_parameters(self):
+ def test_inline_args(self):
class TestObject(object):
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
- FACTORY_ARG_PARAMETERS = ('x', 'y')
+ class Meta:
+ model = TestObject
+ inline_args = ('x', 'y')
x = 1
y = 2
@@ -878,15 +960,16 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual((42, 2), obj.args)
self.assertEqual({'z': 5, 't': 4}, obj.kwargs)
- def test_hidden_args(self):
+ def test_exclude(self):
class TestObject(object):
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
- FACTORY_HIDDEN_ARGS = ('x', 'z')
+ class Meta:
+ model = TestObject
+ exclude = ('x', 'z')
x = 1
y = 2
@@ -897,16 +980,17 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual((), obj.args)
self.assertEqual({'y': 2, 't': 4}, obj.kwargs)
- def test_hidden_args_and_arg_parameters(self):
+ def test_exclude_and_inline_args(self):
class TestObject(object):
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
- FACTORY_HIDDEN_ARGS = ('x', 'z')
- FACTORY_ARG_PARAMETERS = ('y',)
+ class Meta:
+ model = TestObject
+ exclude = ('x', 'z')
+ inline_args = ('y',)
x = 1
y = 2
@@ -927,8 +1011,9 @@ class NonKwargParametersTestCase(unittest.TestCase):
self.kwargs = kwargs
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
- FACTORY_ARG_PARAMETERS = ('one', 'two',)
+ class Meta:
+ model = TestObject
+ inline_args = ('one', 'two',)
one = 1
two = 2
@@ -952,16 +1037,17 @@ class NonKwargParametersTestCase(unittest.TestCase):
return inst
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
- FACTORY_ARG_PARAMETERS = ('one', 'two')
+ class Meta:
+ model = TestObject
+ inline_args = ('one', 'two')
one = 1
two = 2
three = 3
@classmethod
- def _create(cls, target_class, *args, **kwargs):
- return target_class.create(*args, **kwargs)
+ def _create(cls, model_class, *args, **kwargs):
+ return model_class.create(*args, **kwargs)
obj = TestObjectFactory.create()
self.assertEqual((1, 2), obj.args)
@@ -978,7 +1064,8 @@ class KwargAdjustTestCase(unittest.TestCase):
self.kwargs = kwargs
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@classmethod
def _adjust_kwargs(cls, **kwargs):
@@ -989,6 +1076,21 @@ class KwargAdjustTestCase(unittest.TestCase):
self.assertEqual({'x': 1, 'y': 2, 'z': 3, 'foo': 3}, obj.kwargs)
self.assertEqual((), obj.args)
+ def test_rename(self):
+ class TestObject(object):
+ def __init__(self, attributes=None):
+ self.attributes = attributes
+
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+ rename = {'attributes_': 'attributes'}
+
+ attributes_ = 42
+
+ obj = TestObjectFactory.build()
+ self.assertEqual(42, obj.attributes)
+
class SubFactoryTestCase(unittest.TestCase):
def test_sub_factory(self):
@@ -996,11 +1098,13 @@ class SubFactoryTestCase(unittest.TestCase):
pass
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 3
class TestModel2Factory(FakeModelFactory):
- FACTORY_FOR = TestModel2
+ class Meta:
+ model = TestModel2
two = factory.SubFactory(TestModelFactory, one=1)
test_model = TestModel2Factory(two__one=4)
@@ -1013,10 +1117,12 @@ class SubFactoryTestCase(unittest.TestCase):
pass
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
class TestModel2Factory(FakeModelFactory):
- FACTORY_FOR = TestModel2
+ class Meta:
+ model = TestModel2
two = factory.SubFactory(TestModelFactory,
one=factory.Sequence(lambda n: 'x%dx' % n),
two=factory.LazyAttribute(lambda o: '%s%s' % (o.one, o.one)),
@@ -1033,12 +1139,14 @@ class SubFactoryTestCase(unittest.TestCase):
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Sequence(lambda n: int(n))
class WrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrapped = factory.SubFactory(TestObjectFactory)
@@ -1054,7 +1162,8 @@ class SubFactoryTestCase(unittest.TestCase):
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
class OtherTestObject(object):
@@ -1063,7 +1172,8 @@ class SubFactoryTestCase(unittest.TestCase):
setattr(self, k, v)
class WrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = OtherTestObject
+ class Meta:
+ model = OtherTestObject
wrapped = factory.SubFactory(TestObjectFactory, two=2, four=4)
wrapped__two = 4
@@ -1083,16 +1193,19 @@ class SubFactoryTestCase(unittest.TestCase):
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
class WrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrapped = factory.SubFactory(TestObjectFactory)
wrapped_bis = factory.SubFactory(TestObjectFactory, one=1)
class OuterWrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrap = factory.SubFactory(WrappingTestObjectFactory, wrapped__two=2)
@@ -1109,17 +1222,20 @@ class SubFactoryTestCase(unittest.TestCase):
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
two = 'two'
class WrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrapped = factory.SubFactory(TestObjectFactory)
friend = factory.LazyAttribute(lambda o: o.wrapped.two.four + 1)
class OuterWrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrap = factory.SubFactory(WrappingTestObjectFactory,
wrapped__two=factory.SubFactory(TestObjectFactory, four=4))
@@ -1139,12 +1255,14 @@ class SubFactoryTestCase(unittest.TestCase):
# Innermost factory
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
two = 'two'
# Intermediary factory
class WrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrapped = factory.SubFactory(TestObjectFactory)
wrapped__two = 'three'
@@ -1162,11 +1280,13 @@ class SubFactoryTestCase(unittest.TestCase):
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
two = 'two'
class WrappingTestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
wrapped = factory.SubFactory(TestObjectFactory)
friend = factory.LazyAttribute(lambda o: o.wrapped.two + 1)
@@ -1200,20 +1320,24 @@ class SubFactoryTestCase(unittest.TestCase):
self.side_b = side_b
class InnerMostFactory(factory.Factory):
- FACTORY_FOR = InnerMost
+ class Meta:
+ model = InnerMost
a = 15
b = 20
class SideAFactory(factory.Factory):
- FACTORY_FOR = SideA
+ class Meta:
+ model = SideA
inner_from_a = factory.SubFactory(InnerMostFactory, a=20)
class SideBFactory(factory.Factory):
- FACTORY_FOR = SideB
+ class Meta:
+ model = SideB
inner_from_b = factory.SubFactory(InnerMostFactory, b=15)
class OuterMostFactory(factory.Factory):
- FACTORY_FOR = OuterMost
+ class Meta:
+ model = OuterMost
foo = 30
side_a = factory.SubFactory(SideAFactory,
@@ -1238,12 +1362,14 @@ class SubFactoryTestCase(unittest.TestCase):
pass
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 3
two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=False)
class TestModel2Factory(FakeModelFactory):
- FACTORY_FOR = TestModel2
+ class Meta:
+ model = TestModel2
one = 1
two = factory.SubFactory(TestModelFactory, one=1)
@@ -1261,12 +1387,14 @@ class SubFactoryTestCase(unittest.TestCase):
pass
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 3
two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=True)
class TestModel2Factory(FakeModelFactory):
- FACTORY_FOR = TestModel2
+ class Meta:
+ model = TestModel2
one = 1
two = factory.SubFactory(TestModelFactory, one=1)
@@ -1282,7 +1410,8 @@ class SubFactoryTestCase(unittest.TestCase):
pass
class TestModelFactory(FakeModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
one = 3
@factory.container_attribute
@@ -1292,7 +1421,8 @@ class SubFactoryTestCase(unittest.TestCase):
return 42
class TestModel2Factory(FakeModelFactory):
- FACTORY_FOR = TestModel2
+ class Meta:
+ model = TestModel2
one = 1
two = factory.SubFactory(TestModelFactory, one=1)
@@ -1310,7 +1440,8 @@ class IteratorTestCase(unittest.TestCase):
def test_iterator(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Iterator(range(10, 30))
@@ -1323,7 +1454,8 @@ class IteratorTestCase(unittest.TestCase):
@tools.disable_warnings
def test_iterator_list_comprehension_scope_bleeding(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Iterator([j * 3 for j in range(5)])
@@ -1334,7 +1466,8 @@ class IteratorTestCase(unittest.TestCase):
@tools.disable_warnings
def test_iterator_list_comprehension_protected(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Iterator([_j * 3 for _j in range(5)])
@@ -1347,7 +1480,8 @@ class IteratorTestCase(unittest.TestCase):
def test_iterator_decorator(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
@factory.iterator
def one():
@@ -1359,6 +1493,38 @@ class IteratorTestCase(unittest.TestCase):
for i, obj in enumerate(objs):
self.assertEqual(i + 10, obj.one)
+ def test_iterator_late_loading(self):
+ """Ensure that Iterator doesn't unroll on class creation.
+
+ This allows, for Django objects, to call:
+ foo = factory.Iterator(models.MyThingy.objects.all())
+ """
+ class DBRequest(object):
+ def __init__(self):
+ self.ready = False
+
+ def __iter__(self):
+ if not self.ready:
+ raise ValueError("Not ready!!")
+ return iter([1, 2, 3])
+
+ # calling __iter__() should crash
+ req1 = DBRequest()
+ with self.assertRaises(ValueError):
+ iter(req1)
+
+ req2 = DBRequest()
+
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ one = factory.Iterator(req2)
+
+ req2.ready = True
+ obj = TestObjectFactory()
+ self.assertEqual(1, obj.one)
+
class BetterFakeModelManager(object):
def __init__(self, keys, instance):
@@ -1374,12 +1540,9 @@ class BetterFakeModelManager(object):
instance.id = 2
return instance, True
- def values_list(self, *args, **kwargs):
+ def using(self, db):
return self
- def order_by(self, *args, **kwargs):
- return [1]
-
class BetterFakeModel(object):
@classmethod
@@ -1397,7 +1560,8 @@ class BetterFakeModel(object):
class DjangoModelFactoryTestCase(unittest.TestCase):
def test_simple(self):
class FakeModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = FakeModel
+ class Meta:
+ model = FakeModel
obj = FakeModelFactory(one=1)
self.assertEqual(1, obj.one)
@@ -1411,8 +1575,9 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
objects = BetterFakeModelManager({'x': 1}, prev)
class MyFakeModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = MyFakeModel
- FACTORY_DJANGO_GET_OR_CREATE = ('x',)
+ class Meta:
+ model = MyFakeModel
+ django_get_or_create = ('x',)
x = 1
y = 4
z = 6
@@ -1432,8 +1597,9 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev)
class MyFakeModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = MyFakeModel
- FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z')
+ class Meta:
+ model = MyFakeModel
+ django_get_or_create = ('x', 'y', 'z')
x = 1
y = 4
z = 6
@@ -1453,8 +1619,9 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
objects = BetterFakeModelManager({'x': 1}, prev)
class MyFakeModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = MyFakeModel
- FACTORY_DJANGO_GET_OR_CREATE = ('x',)
+ class Meta:
+ model = MyFakeModel
+ django_get_or_create = ('x',)
x = 1
y = 4
z = 6
@@ -1474,8 +1641,9 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev)
class MyFakeModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = MyFakeModel
- FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z')
+ class Meta:
+ model = MyFakeModel
+ django_get_or_create = ('x', 'y', 'z')
x = 1
y = 4
z = 6
@@ -1489,37 +1657,40 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
def test_sequence(self):
class TestModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
a = factory.Sequence(lambda n: 'foo_%s' % n)
o1 = TestModelFactory()
o2 = TestModelFactory()
- self.assertEqual('foo_2', o1.a)
- self.assertEqual('foo_3', o2.a)
+ self.assertEqual('foo_0', o1.a)
+ self.assertEqual('foo_1', o2.a)
o3 = TestModelFactory.build()
o4 = TestModelFactory.build()
- self.assertEqual('foo_4', o3.a)
- self.assertEqual('foo_5', o4.a)
+ self.assertEqual('foo_2', o3.a)
+ self.assertEqual('foo_3', o4.a)
def test_no_get_or_create(self):
class TestModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = TestModel
+ class Meta:
+ model = TestModel
a = factory.Sequence(lambda n: 'foo_%s' % n)
o = TestModelFactory()
self.assertEqual(None, o._defaults)
- self.assertEqual('foo_2', o.a)
+ self.assertEqual('foo_0', o.a)
self.assertEqual(2, o.id)
def test_get_or_create(self):
class TestModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = TestModel
- FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b')
+ class Meta:
+ model = TestModel
+ django_get_or_create = ('a', 'b')
a = factory.Sequence(lambda n: 'foo_%s' % n)
b = 2
@@ -1528,7 +1699,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
o = TestModelFactory()
self.assertEqual({'c': 3, 'd': 4}, o._defaults)
- self.assertEqual('foo_2', o.a)
+ self.assertEqual('foo_0', o.a)
self.assertEqual(2, o.b)
self.assertEqual(3, o.c)
self.assertEqual(4, o.d)
@@ -1537,8 +1708,9 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
def test_full_get_or_create(self):
"""Test a DjangoModelFactory with all fields in get_or_create."""
class TestModelFactory(factory.django.DjangoModelFactory):
- FACTORY_FOR = TestModel
- FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b', 'c', 'd')
+ class Meta:
+ model = TestModel
+ django_get_or_create = ('a', 'b', 'c', 'd')
a = factory.Sequence(lambda n: 'foo_%s' % n)
b = 2
@@ -1547,7 +1719,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
o = TestModelFactory()
self.assertEqual({}, o._defaults)
- self.assertEqual('foo_2', o.a)
+ self.assertEqual('foo_0', o.a)
self.assertEqual(2, o.b)
self.assertEqual(3, o.c)
self.assertEqual(4, o.d)
@@ -1557,7 +1729,8 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
class PostGenerationTestCase(unittest.TestCase):
def test_post_generation(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 1
@@ -1575,7 +1748,8 @@ class PostGenerationTestCase(unittest.TestCase):
def test_post_generation_hook(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 1
@@ -1596,7 +1770,8 @@ class PostGenerationTestCase(unittest.TestCase):
def test_post_generation_extraction(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 1
@@ -1621,7 +1796,8 @@ class PostGenerationTestCase(unittest.TestCase):
self.assertEqual(kwargs, {'foo': 13})
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
bar = factory.PostGeneration(my_lambda)
@@ -1640,7 +1816,8 @@ class PostGenerationTestCase(unittest.TestCase):
self.extra = (args, kwargs)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 3
two = 2
post_call = factory.PostGenerationMethodCall('call', one=1)
@@ -1664,15 +1841,17 @@ class PostGenerationTestCase(unittest.TestCase):
self.three = obj
class TestRelatedObjectFactory(factory.Factory):
- FACTORY_FOR = TestRelatedObject
+ class Meta:
+ model = TestRelatedObject
one = 1
two = factory.LazyAttribute(lambda o: o.one + 1)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 3
two = 2
- three = factory.RelatedFactory(TestRelatedObjectFactory, name='obj')
+ three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj')
obj = TestObjectFactory.build()
# Normal fields
@@ -1709,12 +1888,14 @@ class PostGenerationTestCase(unittest.TestCase):
self.three = obj
class TestRelatedObjectFactory(factory.Factory):
- FACTORY_FOR = TestRelatedObject
+ class Meta:
+ model = TestRelatedObject
one = 1
two = factory.LazyAttribute(lambda o: o.one + 1)
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 3
two = 2
three = factory.RelatedFactory(TestRelatedObjectFactory)
@@ -1743,6 +1924,36 @@ class PostGenerationTestCase(unittest.TestCase):
self.assertEqual(3, related.one)
self.assertEqual(4, related.two)
+ def test_related_factory_selfattribute(self):
+ class TestRelatedObject(object):
+ def __init__(self, obj=None, one=None, two=None):
+ obj.related = self
+ self.one = one
+ self.two = two
+ self.three = obj
+
+ class TestRelatedObjectFactory(factory.Factory):
+ class Meta:
+ model = TestRelatedObject
+ one = 1
+ two = factory.LazyAttribute(lambda o: o.one + 1)
+
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+ one = 3
+ two = 2
+ three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj',
+ two=factory.SelfAttribute('obj.two'),
+ )
+
+ obj = TestObjectFactory.build(two=4)
+ self.assertEqual(3, obj.one)
+ self.assertEqual(4, obj.two)
+ self.assertEqual(1, obj.related.one)
+ self.assertEqual(4, obj.related.two)
+
+
class RelatedFactoryExtractionTestCase(unittest.TestCase):
def setUp(self):
@@ -1755,10 +1966,12 @@ class RelatedFactoryExtractionTestCase(unittest.TestCase):
obj.related = subself
class TestRelatedObjectFactory(factory.Factory):
- FACTORY_FOR = TestRelatedObject
+ class Meta:
+ model = TestRelatedObject
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.RelatedFactory(TestRelatedObjectFactory, 'obj')
self.TestRelatedObject = TestRelatedObject
@@ -1802,10 +2015,28 @@ class CircularTestCase(unittest.TestCase):
self.assertIsNone(b.foo.bar.foo.bar)
+class SelfReferentialTests(unittest.TestCase):
+ def test_no_parent(self):
+ from .cyclic import self_ref
+
+ obj = self_ref.TreeElementFactory(parent=None)
+ self.assertIsNone(obj.parent)
+
+ def test_deep(self):
+ from .cyclic import self_ref
+
+ obj = self_ref.TreeElementFactory(parent__parent__parent__parent=None)
+ self.assertIsNotNone(obj.parent)
+ self.assertIsNotNone(obj.parent.parent)
+ self.assertIsNotNone(obj.parent.parent.parent)
+ self.assertIsNone(obj.parent.parent.parent.parent)
+
+
class DictTestCase(unittest.TestCase):
def test_empty_dict(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Dict({})
o = TestObjectFactory()
@@ -1813,7 +2044,8 @@ class DictTestCase(unittest.TestCase):
def test_naive_dict(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Dict({'a': 1})
o = TestObjectFactory()
@@ -1821,7 +2053,8 @@ class DictTestCase(unittest.TestCase):
def test_sequence_dict(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Dict({'a': factory.Sequence(lambda n: n + 2)})
o1 = TestObjectFactory()
@@ -1832,7 +2065,8 @@ class DictTestCase(unittest.TestCase):
def test_dict_override(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Dict({'a': 1})
o = TestObjectFactory(one__a=2)
@@ -1840,7 +2074,8 @@ class DictTestCase(unittest.TestCase):
def test_dict_extra_key(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.Dict({'a': 1})
o = TestObjectFactory(one__b=2)
@@ -1848,7 +2083,8 @@ class DictTestCase(unittest.TestCase):
def test_dict_merged_fields(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
two = 13
one = factory.Dict({
'one': 1,
@@ -1861,7 +2097,8 @@ class DictTestCase(unittest.TestCase):
def test_nested_dicts(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = 1
two = factory.Dict({
'one': 3,
@@ -1889,7 +2126,8 @@ class DictTestCase(unittest.TestCase):
class ListTestCase(unittest.TestCase):
def test_empty_list(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.List([])
o = TestObjectFactory()
@@ -1897,7 +2135,8 @@ class ListTestCase(unittest.TestCase):
def test_naive_list(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.List([1])
o = TestObjectFactory()
@@ -1905,7 +2144,8 @@ class ListTestCase(unittest.TestCase):
def test_sequence_list(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.List([factory.Sequence(lambda n: n + 2)])
o1 = TestObjectFactory()
@@ -1916,7 +2156,8 @@ class ListTestCase(unittest.TestCase):
def test_list_override(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.List([1])
o = TestObjectFactory(one__0=2)
@@ -1924,7 +2165,8 @@ class ListTestCase(unittest.TestCase):
def test_list_extra_key(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
one = factory.List([1])
o = TestObjectFactory(one__1=2)
@@ -1932,7 +2174,8 @@ class ListTestCase(unittest.TestCase):
def test_list_merged_fields(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
two = 13
one = factory.List([
1,
@@ -1945,7 +2188,9 @@ class ListTestCase(unittest.TestCase):
def test_nested_lists(self):
class TestObjectFactory(factory.Factory):
- FACTORY_FOR = TestObject
+ class Meta:
+ model = TestObject
+
one = 1
two = factory.List([
3,
diff --git a/tests/test_utils.py b/tests/test_utils.py
index d321c2a..77598e1 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -238,33 +238,33 @@ class ImportObjectTestCase(unittest.TestCase):
class LogPPrintTestCase(unittest.TestCase):
def test_nothing(self):
- txt = utils.log_pprint()
+ txt = str(utils.log_pprint())
self.assertEqual('', txt)
def test_only_args(self):
- txt = utils.log_pprint((1, 2, 3))
+ txt = str(utils.log_pprint((1, 2, 3)))
self.assertEqual('1, 2, 3', txt)
def test_only_kwargs(self):
- txt = utils.log_pprint(kwargs={'a': 1, 'b': 2})
+ txt = str(utils.log_pprint(kwargs={'a': 1, 'b': 2}))
self.assertIn(txt, ['a=1, b=2', 'b=2, a=1'])
def test_bytes_args(self):
- txt = utils.log_pprint((b'\xe1\xe2',))
+ txt = str(utils.log_pprint((b'\xe1\xe2',)))
expected = "b'\\xe1\\xe2'"
if is_python2:
expected = expected.lstrip('b')
self.assertEqual(expected, txt)
def test_text_args(self):
- txt = utils.log_pprint(('ŧêßŧ',))
+ txt = str(utils.log_pprint(('ŧêßŧ',)))
expected = "'ŧêßŧ'"
if is_python2:
expected = "u'\\u0167\\xea\\xdf\\u0167'"
self.assertEqual(expected, txt)
def test_bytes_kwargs(self):
- txt = utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'})
+ txt = str(utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'}))
expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'"
expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'"
if is_python2:
@@ -273,7 +273,7 @@ class LogPPrintTestCase(unittest.TestCase):
self.assertIn(txt, (expected1, expected2))
def test_text_kwargs(self):
- txt = utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'})
+ txt = str(utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'}))
expected1 = "x='ŧêßŧ', y='ŧßêŧ'"
expected2 = "y='ŧßêŧ', x='ŧêßŧ'"
if is_python2:
diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py
index 9956610..b534998 100644
--- a/tests/testdata/__init__.py
+++ b/tests/testdata/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
diff --git a/tests/tools.py b/tests/tools.py
index 571899b..47f705c 100644
--- a/tests/tools.py
+++ b/tests/tools.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
diff --git a/tests/utils.py b/tests/utils.py
index 215fc83..7a31ed2 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011-2013 Raphaël Barrois
+# Copyright (c) 2011-2015 Raphaël Barrois
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 2200f56..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,17 +0,0 @@
-[tox]
-envlist = py26,py27,pypy
-
-[testenv]
-commands=
- python -W default setup.py test
-
-[testenv:py26]
-
-deps=
- mock
- unittest2
-
-[textenv:py27]
-
-deps=
- mock