summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOndřej Nový <novy@ondrej.org>2016-02-14 19:26:09 +0100
committerOndřej Nový <novy@ondrej.org>2016-02-14 19:26:09 +0100
commita85231280accdfec8ef9cf67213ff706eb242889 (patch)
treec84bb817ccf5bae224e473bb15959a4fb97d41ba
parentd92aeedcf27326270cb3dcd8b780566728a489a9 (diff)
parent41560aa54e83fe539c0a5a1935bcaaf6363a522c (diff)
downloadfactory-boy-a85231280accdfec8ef9cf67213ff706eb242889.tar
factory-boy-a85231280accdfec8ef9cf67213ff706eb242889.tar.gz
Merge tag '2.6.1' into debian/unstable
Release of factory_boy 2.6.1
-rw-r--r--.gitignore1
-rw-r--r--.travis.yml11
-rw-r--r--LICENSE2
-rw-r--r--Makefile36
-rw-r--r--README.rst95
-rw-r--r--dev_requirements.txt9
-rw-r--r--docs/changelog.rst105
-rw-r--r--docs/conf.py8
-rw-r--r--docs/examples.rst8
-rw-r--r--docs/fuzzy.rst39
-rw-r--r--docs/ideas.rst2
-rw-r--r--docs/orms.rst177
-rw-r--r--docs/recipes.rst129
-rw-r--r--docs/reference.rst146
-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__.py17
-rw-r--r--factory/alchemy.py21
-rw-r--r--factory/base.py40
-rw-r--r--factory/compat.py10
-rw-r--r--factory/containers.py2
-rw-r--r--factory/declarations.py26
-rw-r--r--factory/django.py120
-rw-r--r--factory/faker.py101
-rw-r--r--factory/fuzzy.py53
-rw-r--r--factory/helpers.py3
-rw-r--r--factory/mogo.py6
-rw-r--r--factory/mongoengine.py2
-rw-r--r--factory/utils.py36
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py9
-rw-r--r--tests/__init__.py8
-rw-r--r--tests/compat.py2
-rw-r--r--tests/cyclic/bar.py2
-rw-r--r--tests/cyclic/foo.py2
-rw-r--r--tests/cyclic/self_ref.py (renamed from tests/test_deprecation.py)34
-rw-r--r--tests/djapp/models.py36
-rw-r--r--tests/djapp/settings.py6
-rw-r--r--tests/test_alchemy.py56
-rw-r--r--tests/test_base.py33
-rw-r--r--tests/test_containers.py2
-rw-r--r--tests/test_declarations.py17
-rw-r--r--tests/test_django.py349
-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.py15
-rw-r--r--tests/test_using.py120
-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
58 files changed, 1809 insertions, 477 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 32b93bd..4d114e5 100644
--- a/README.rst
+++ b/README.rst
@@ -4,6 +4,22 @@ factory_boy
.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master
:target: http://travis-ci.org/rbarrois/factory_boy/
+.. image:: https://img.shields.io/pypi/v/factory_boy.svg
+ :target: http://factoryboy.readthedocs.org/en/latest/changelog.html
+ :alt: Latest Version
+
+.. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg
+ :target: https://pypi.python.org/pypi/factory_boy/
+ :alt: Supported Python versions
+
+.. image:: https://img.shields.io/pypi/wheel/factory_boy.svg
+ :target: https://pypi.python.org/pypi/factory_boy/
+ :alt: Wheel status
+
+.. image:: https://img.shields.io/pypi/l/factory_boy.svg
+ :target: https://pypi.python.org/pypi/factory_boy/
+ :alt: License
+
factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_.
As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures
@@ -52,7 +68,7 @@ Its main features include:
- Straightforward declarative syntax
- Chaining factory calls while retaining the global context
-- Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects)
+- Support for multiple build strategies (saved/unsaved instances, stubbed objects)
- Multiple factories per class support, including inheritance
@@ -62,8 +78,9 @@ Links
* Documentation: http://factoryboy.readthedocs.org/
* Repository: https://github.com/rbarrois/factory_boy
* Package: https://pypi.python.org/pypi/factory_boy/
+* Mailing-list: `factoryboy@googlegroups.com <mailto:factoryboy@googlegroups.com>`_ | https://groups.google.com/forum/#!forum/factoryboy
-factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library.
+factory_boy supports Python 2.6, 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library.
Download
@@ -123,7 +140,7 @@ The class of the object must be defined in the ``model`` field of a ``class Meta
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
@@ -133,8 +150,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:
@@ -159,12 +176,38 @@ It is also possible to create a bunch of objects in a single call:
.. code-block:: pycon
- >>> users = UserFactory.build(10, first_name="Joe")
+ >>> 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
"""""""""""""""
@@ -250,7 +293,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
@@ -281,12 +324,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
------------
@@ -294,25 +337,41 @@ Contributing
factory_boy is distributed under the MIT License.
Issues should be opened through `GitHub Issues <http://github.com/rbarrois/factory_boy/issues/>`_; whenever possible, a pull request should be included.
+Questions and suggestions are welcome on the `mailing-list <mailto:factoryboy@googlegroups.com>`_.
All pull request should pass the test suite, which can be launched simply with:
.. code-block:: sh
- $ python setup.py test
+ $ make test
+
-.. note::
+In order to test coverage, please use:
+
+.. code-block:: sh
- Running test requires the unittest2 (standard in Python 2.7+) and mock libraries.
+ $ make coverage
-In order to test coverage, please use:
+To test with a specific framework version, you may use:
+
+.. code-block:: sh
+
+ $ make DJANGO=1.9 test
+
+Valid options are:
+
+* ``DJANGO`` for ``Django``
+* ``MONGOENGINE`` for ``mongoengine``
+* ``ALCHEMY`` for ``SQLAlchemy``
+
+
+To avoid running ``mongoengine`` tests (e.g no mongo server installed), run:
.. code-block:: sh
- $ pip install coverage
- $ coverage erase; coverage run --branch setup.py test; coverage report
+ $ make SKIP_MONGOENGINE=1 test
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 7d77f7f..fa542f4 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,107 @@
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:
@@ -19,7 +120,7 @@ ChangeLog
*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.
@@ -31,8 +132,6 @@ ChangeLog
For :class:`factory.Factory`:
* Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model`
-
- * 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`
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 ee521e3..e7f6057 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -114,9 +114,9 @@ We can now use our factories, for tests:
def test_get_profile_stats(self):
profiles = []
- profiles.extend(factories.ProfileFactory.batch_create(4))
- profiles.extend(factories.FemaleProfileFactory.batch_create(2))
- profiles.extend(factories.ProfileFactory.batch_create(2, 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)
@@ -130,7 +130,7 @@ Or for fixtures:
from . import factories
def make_objects():
- factories.ProfileFactory.batch_create(size=50)
+ 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 f3c9e62..6e3962d 100644
--- a/docs/ideas.rst
+++ b/docs/ideas.rst
@@ -6,4 +6,4 @@ This is a list of future features that may be incorporated into factory_boy:
* 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/orms.rst b/docs/orms.rst
index 2aa27b2..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.
@@ -35,20 +35,29 @@ All factories for a Django :class:`~django.db.models.Model` should use the
* 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
- .. deprecated:: 2.4.0
- See :attr:`DjangoOptions.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 an extra parameter:
+ 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
@@ -117,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
@@ -264,6 +273,34 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin
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
----------
@@ -284,12 +321,7 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:
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
-
- .. deprecated:: 2.4.0
- See :attr:`~SQLAlchemyOptions.sqlalchemy_session`.
.. class:: SQLAlchemyOptions(factory.base.FactoryOptions)
@@ -301,7 +333,11 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:
SQLAlchemy session to use to communicate with the database when creating
an object through this :class:`SQLAlchemyModelFactory`.
-A (very) simple exemple:
+ .. attribute:: force_flush
+
+ Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`.
+
+A (very) simple example:
.. code-block:: python
@@ -309,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()
@@ -324,8 +359,9 @@ A (very) simple exemple:
Base.metadata.create_all(engine)
+ import factory
- class UserFactory(SQLAlchemyModelFactory):
+ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta:
model = User
sqlalchemy_session = session # the SQLAlchemy session object
@@ -341,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 72dacef..df86bac 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -33,6 +33,29 @@ use the :class:`~factory.SubFactory` declaration:
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)
-----------------------------------------
@@ -125,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`
@@ -144,7 +167,7 @@ hook:
class User(models.Model):
name = models.CharField()
- groups = models.ManyToMany(Group)
+ groups = models.ManyToManyField(Group)
# factories.py
@@ -181,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`.
@@ -196,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)
@@ -327,3 +350,97 @@ default :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>`
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 b0dda50..b5ccd16 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -42,7 +42,7 @@ The :class:`Factory` class
It will be automatically set to ``True`` if neither the :class:`Factory`
subclass nor its parents define the :attr:`~FactoryOptions.model` attribute.
- .. warning:: This flag is reset to ``False`` When a :class:`Factory` subclasses
+ .. 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
@@ -106,43 +106,36 @@ The :class:`Factory` class
.. versionadded:: 2.4.0
- .. attribute:: strategy
-
- Use this attribute to change the strategy used by a :class:`Factory`.
- The default is :data:`BUILD_STRATEGY`.
-
+ .. attribute:: rename
+ Sometimes, a model expect a field with a name already used by one
+ of :class:`Factory`'s methods.
-.. class:: Factory
-
- .. note:: In previous versions, the fields of :class:`class Meta <factory.FactoryOptions>` were
- defined as class attributes on :class:`Factory`. This is now deprecated and will be removed
- in 2.5.0.
+ 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:
- .. attribute:: FACTORY_FOR
+ .. code-block:: python
- .. deprecated:: 2.4.0
- See :attr:`FactoryOptions.model`.
+ class ImageFactory(factory.Factory):
+ # The model expects "attributes"
+ form_attributes = ['thumbnail', 'black-and-white']
- .. attribute:: ABSTRACT_FACTORY
+ class Meta:
+ model = Image
+ rename = {'form_attributes': 'attributes'}
- .. deprecated:: 2.4.0
- See :attr:`FactoryOptions.abstract`.
+ .. versionadded: 2.6.0
- .. attribute:: FACTORY_ARG_PARAMETERS
- .. deprecated:: 2.4.0
- See :attr:`FactoryOptions.inline_args`.
+ .. attribute:: strategy
- .. attribute:: FACTORY_HIDDEN_ARGS
+ Use this attribute to change the strategy used by a :class:`Factory`.
+ The default is :data:`CREATE_STRATEGY`.
- .. deprecated:: 2.4.0
- See :attr:`FactoryOptions.exclude`.
- .. attribute:: FACTORY_STRATEGY
- .. deprecated:: 2.4.0
- See :attr:`FactoryOptions.strategy`.
+.. class:: Factory
**Class-level attributes:**
@@ -258,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
@@ -398,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 <FactoryOptions.model` 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.
@@ -482,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
"""""""""""""
@@ -565,7 +634,7 @@ 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
@@ -1221,7 +1290,7 @@ For instance, a :class:`PostGeneration` hook is declared as ``post``:
model = SomeObject
@post_generation
- def post(self, create, extracted, **kwargs):
+ def post(obj, create, extracted, **kwargs):
obj.set_origin(create)
.. OHAI_VIM**
@@ -1343,6 +1412,23 @@ 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
""""""""""""""
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 8fc8ef8..c8bc396 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.4.1'
+__version__ = '2.6.1'
__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
@@ -40,9 +40,7 @@ from .base import (
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,
@@ -83,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 3c91411..a9aab23 100644
--- a/factory/alchemy.py
+++ b/factory/alchemy.py
@@ -19,7 +19,6 @@
# 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
@@ -28,6 +27,7 @@ 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),
]
@@ -38,27 +38,12 @@ class SQLAlchemyModelFactory(base.Factory):
class Meta:
abstract = True
- _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy()
- _OLDSTYLE_ATTRIBUTES.update({
- 'FACTORY_SESSION': 'sqlalchemy_session',
- })
-
- @classmethod
- def _setup_next_sequence(cls, *args, **kwargs):
- """Compute the next available PK, based on the 'pk' database field."""
- session = cls._meta.sqlalchemy_session
- model = cls._meta.model
- 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
-
@classmethod
def _create(cls, model_class, *args, **kwargs):
"""Create an instance of the model, and save it to the database."""
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 9e07899..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
@@ -21,7 +21,6 @@
# THE SOFTWARE.
import logging
-import warnings
from . import containers
from . import declarations
@@ -109,23 +108,6 @@ class FactoryMetaClass(type):
attrs_meta = attrs.pop('Meta', None)
- oldstyle_attrs = {}
- converted_attrs = {}
- for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items():
- if old_name in attrs:
- oldstyle_attrs[old_name] = new_name
- converted_attrs[new_name] = attrs.pop(old_name)
- if oldstyle_attrs:
- warnings.warn(
- "Declaring any of %s at class-level is deprecated"
- " and will be removed in the future. Please set them"
- " as %s attributes of a 'class Meta' attribute." % (
- ', '.join(oldstyle_attrs.keys()),
- ', '.join(oldstyle_attrs.values()),
- ),
- PendingDeprecationWarning, 2)
- attrs_meta = type('Meta', (object,), converted_attrs)
-
base_meta = resolve_attribute('_meta', bases)
options_class = resolve_attribute('_options_class', bases, FactoryOptions)
@@ -194,6 +176,7 @@ class FactoryOptions(object):
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):
@@ -322,14 +305,6 @@ class BaseFactory(object):
_meta = FactoryOptions()
- _OLDSTYLE_ATTRIBUTES = {
- 'FACTORY_FOR': 'model',
- 'ABSTRACT_FACTORY': 'abstract',
- 'FACTORY_STRATEGY': 'strategy',
- 'FACTORY_ARG_PARAMETERS': 'inline_args',
- 'FACTORY_HIDDEN_ARGS': 'exclude',
- }
-
# ID to use for the next 'declarations.Sequence' attribute.
_counter = None
@@ -435,10 +410,16 @@ class BaseFactory(object):
retrieved DeclarationDict.
"""
decls = cls._meta.declarations.copy()
- decls.update(extra_defs)
+ 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):
"""Extension point for custom kwargs adjustment."""
return kwargs
@@ -467,6 +448,7 @@ class BaseFactory(object):
**kwargs: arguments to pass to the creation function
"""
model_class = cls._get_model_class()
+ kwargs = cls._rename_fields(**kwargs)
kwargs = cls._adjust_kwargs(**kwargs)
# Remove 'hidden' arguments.
@@ -709,7 +691,7 @@ class StubFactory(Factory):
@classmethod
def build(cls, **kwargs):
- raise UnsupportedStrategy()
+ return cls.stub(**kwargs)
@classmethod
def create(cls, **kwargs):
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 5116320..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
diff --git a/factory/declarations.py b/factory/declarations.py
index 5e7e734..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
@@ -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))
@@ -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 2b6c463..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,10 +56,41 @@ 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):
@@ -84,18 +119,12 @@ class DjangoModelFactory(base.Factory):
class Meta:
abstract = True # Optional, but explicit.
- _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy()
- _OLDSTYLE_ATTRIBUTES.update({
- 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create',
- })
-
@classmethod
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
@@ -104,25 +133,17 @@ class DjangoModelFactory(base.Factory):
if model_class is None:
raise base.AssociatedClassError("No model set on %s.%s.Meta"
% (cls.__module__, cls.__name__))
+
try:
- return model_class._default_manager # pylint: disable=W0212
+ manager = model_class.objects
except AttributeError:
- return model_class.objects
-
- @classmethod
- def _setup_next_sequence(cls):
- """Compute the next available PK, based on the 'pk' database field."""
-
- model = cls._get_model_class() # pylint: disable=E1101
- manager = cls._get_manager(model)
+ # When inheriting from an abstract model with a custom
+ # manager, the class has no 'objects' field.
+ manager = model_class._default_manager
- 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, model_class, *args, **kwargs):
@@ -160,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(
@@ -185,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)
@@ -212,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):
@@ -278,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 = []
@@ -287,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.
@@ -297,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
@@ -306,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 19431df..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
diff --git a/factory/mogo.py b/factory/mogo.py
index 5541043..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
@@ -37,10 +37,10 @@ class MogoFactory(base.Factory):
@classmethod
def _build(cls, model_class, *args, **kwargs):
- return model_class.new(*args, **kwargs)
+ return model_class(*args, **kwargs)
@classmethod
def _create(cls, model_class, *args, **kwargs):
- instance = model_class.new(*args, **kwargs)
+ instance = model_class(*args, **kwargs)
instance.save()
return instance
diff --git a/factory/mongoengine.py b/factory/mongoengine.py
index e3ab99c..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
diff --git a/factory/utils.py b/factory/utils.py
index 276977a..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
@@ -101,7 +101,7 @@ def import_object(module_name, attribute_name):
def _safe_repr(obj):
try:
obj_repr = repr(obj)
- except UnicodeError:
+ except Exception:
return '<bad_repr object at %s>' % id(obj)
try: # Convert to "text type" (= unicode)
@@ -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 f637a48..003dd08 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,6 +44,9 @@ setup(
keywords=['factory_boy', 'factory', 'fixtures'],
packages=['factory'],
license='MIT',
+ install_requires=[
+ 'fake-factory>=0.5.0',
+ ],
setup_requires=[
'setuptools>=0.8',
],
@@ -63,9 +66,11 @@ setup(
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
"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 855beea..b2c772d 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,11 +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_deprecation 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/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 a5e6bf1..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
diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py
index 18de362..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
diff --git a/tests/test_deprecation.py b/tests/cyclic/self_ref.py
index a07cbf3..d98b3ab 100644
--- a/tests/test_deprecation.py
+++ b/tests/cyclic/self_ref.py
@@ -1,6 +1,5 @@
# -*- 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,30 +19,19 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-"""Tests for deprecated features."""
-
-import warnings
+"""Helper to test circular factory dependencies."""
import factory
-from .compat import mock, unittest
-from . import tools
-
+class TreeElement(object):
+ def __init__(self, name, parent):
+ self.parent = parent
+ self.name = name
-class DeprecationTests(unittest.TestCase):
- def test_factory_for(self):
- class Foo(object):
- pass
- with warnings.catch_warnings(record=True) as w:
- warnings.simplefilter('always')
- class FooFactory(factory.Factory):
- FACTORY_FOR = Foo
+class TreeElementFactory(factory.Factory):
+ class Meta:
+ model = TreeElement
- self.assertEqual(1, len(w))
- warning = w[0]
- # Message is indeed related to the current file
- # This is to ensure error messages are readable by end users.
- self.assertIn(warning.filename, __file__)
- self.assertIn('FACTORY_FOR', str(warning.message))
- self.assertIn('model', str(warning.message))
+ 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 9b21181..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
@@ -68,6 +68,15 @@ 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)
@@ -79,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):
@@ -87,3 +97,27 @@ else:
class WithSignals(models.Model):
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 b9222eb..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:
@@ -55,6 +56,16 @@ class StandardFactory(SQLAlchemyModelFactory):
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):
class Meta:
model = models.NonIntegerPk
@@ -88,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.")
@@ -111,22 +143,22 @@ class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase):
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')
@@ -134,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 d1df58e..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
@@ -403,7 +403,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
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):
class Meta:
model = TestModel
@@ -414,8 +414,18 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
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
- self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory)
+ 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)
@@ -454,6 +464,23 @@ class FactoryCreationTestCase(unittest.TestCase):
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):
class Meta:
diff --git a/tests/test_containers.py b/tests/test_containers.py
index bd7019e..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
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 41a26cf..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,72 +93,80 @@ def tearDownModule():
django_test_utils.teardown_test_environment()
-class StandardFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = 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)
- foo = factory.Sequence(lambda n: "foo%d" % n)
+ class StandardFactoryWithPKField(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.StandardModel
+ django_get_or_create = ('pk',)
-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
- foo = factory.Sequence(lambda n: "foo%d" % n)
- pk = None
+ class NonIntegerPkFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.NonIntegerPk
-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)
- bar = ''
+ class AbstractBaseFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.AbstractBase
+ abstract = True
-class AbstractBaseFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = models.AbstractBase
- abstract = True
+ foo = factory.Sequence(lambda n: "foo%d" % n)
- foo = factory.Sequence(lambda n: "foo%d" % n)
+ class ConcreteSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.ConcreteSon
-class ConcreteSonFactory(AbstractBaseFactory):
- class Meta:
- model = models.ConcreteSon
+ class AbstractSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.AbstractSon
-class AbstractSonFactory(AbstractBaseFactory):
- class Meta:
- model = models.AbstractSon
+ class ConcreteGrandSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.ConcreteGrandSon
-class ConcreteGrandSonFactory(AbstractBaseFactory):
- class Meta:
- model = models.ConcreteGrandSon
+ class WithFileFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithFile
-class WithFileFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = models.WithFile
+ if django is not None:
+ afile = factory.django.FileField()
- if django is not None:
- afile = factory.django.FileField()
+ class WithImageFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithImage
-class WithImageFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = models.WithImage
+ if django is not None:
+ animage = factory.django.ImageField()
- if django is not None:
- animage = factory.django.ImageField()
+ class WithSignalsFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithSignals
-class WithSignalsFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = models.WithSignals
+
+ class WithCustomManagerFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.WithCustomManager
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
@unittest.skipIf(django is None, "Django not installed.")
@@ -174,6 +177,16 @@ class ModelTests(django_test.TestCase):
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.")
class DjangoPkSequenceTestCase(django_test.TestCase):
@@ -183,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)
@@ -221,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')
@@ -295,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.")
@@ -308,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')
@@ -333,8 +346,8 @@ 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.")
@@ -351,6 +364,32 @@ class DjangoAbstractBaseSequenceTestCase(django_test.TestCase):
@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):
@@ -363,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):
@@ -374,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)
@@ -396,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)
@@ -406,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)
@@ -421,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)
@@ -451,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)
@@ -458,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)
@@ -478,6 +557,8 @@ 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)
@@ -491,7 +572,8 @@ class DjangoImageFieldTestCase(unittest.TestCase):
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)
@@ -499,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)
@@ -509,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)
@@ -520,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)
@@ -536,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)
@@ -585,6 +678,19 @@ 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):
@@ -599,6 +705,27 @@ 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):
@@ -641,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 988c179..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
@@ -61,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 f18df4d..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):
@@ -1073,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):
@@ -1475,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):
@@ -1490,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
@@ -1618,14 +1665,14 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
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):
@@ -1636,7 +1683,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
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):
@@ -1652,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)
@@ -1672,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)
@@ -1804,7 +1851,7 @@ class PostGenerationTestCase(unittest.TestCase):
model = TestObject
one = 3
two = 2
- three = factory.RelatedFactory(TestRelatedObjectFactory, name='obj')
+ three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj')
obj = TestObjectFactory.build()
# Normal fields
@@ -1877,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):
@@ -1938,6 +2015,23 @@ 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):
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