summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Goirand <thomas@goirand.fr>2014-05-03 22:57:46 +0800
committerThomas Goirand <thomas@goirand.fr>2014-05-03 22:57:46 +0800
commit1073f2ae50fb0999712b6744b082f7424e4490c3 (patch)
tree26943f3545ce1fc1ae54e0398fbbd78df641f54d
parentc9f01d77941527b62ca67b1064bd3fb849b6a064 (diff)
parent90db123ada9739a19f3b408b50e006700923f651 (diff)
downloadfactory-boy-1073f2ae50fb0999712b6744b082f7424e4490c3.tar
factory-boy-1073f2ae50fb0999712b6744b082f7424e4490c3.tar.gz
Merge tag '2.3.1' into debian/unstable
Release of factory_boy 2.3.1
-rw-r--r--.gitignore16
-rw-r--r--.travis.yml4
-rw-r--r--MANIFEST.in4
-rw-r--r--Makefile13
-rw-r--r--README233
-rw-r--r--[l---------]README.rst278
-rw-r--r--dev_requirements.txt5
-rw-r--r--docs/changelog.rst167
-rw-r--r--docs/conf.py15
-rw-r--r--docs/fuzzy.rst220
-rw-r--r--docs/introduction.rst22
-rw-r--r--docs/orms.rst212
-rw-r--r--docs/recipes.rst79
-rw-r--r--docs/reference.rst185
-rw-r--r--factory/__init__.py11
-rw-r--r--factory/alchemy.py51
-rw-r--r--factory/base.py261
-rw-r--r--factory/compat.py47
-rw-r--r--factory/containers.py36
-rw-r--r--factory/declarations.py205
-rw-r--r--factory/django.py216
-rw-r--r--factory/fuzzy.py177
-rw-r--r--factory/helpers.py18
-rw-r--r--factory/mogo.py45
-rw-r--r--factory/mongoengine.py46
-rw-r--r--factory/utils.py47
-rwxr-xr-xsetup.py104
-rw-r--r--tests/__init__.py4
-rw-r--r--tests/alchemyapp/__init__.py0
-rw-r--r--tests/alchemyapp/models.py47
-rw-r--r--tests/alter_time.py113
-rw-r--r--tests/compat.py13
-rw-r--r--tests/cyclic/foo.py4
-rw-r--r--tests/djapp/__init__.py0
-rw-r--r--tests/djapp/models.py76
-rw-r--r--tests/djapp/settings.py45
-rw-r--r--tests/test_alchemy.py134
-rw-r--r--tests/test_base.py106
-rw-r--r--tests/test_containers.py12
-rw-r--r--tests/test_declarations.py135
-rw-r--r--tests/test_django.py515
-rw-r--r--tests/test_fuzzy.py394
-rw-r--r--tests/test_helpers.py76
-rw-r--r--tests/test_mongoengine.py84
-rw-r--r--tests/test_using.py305
-rw-r--r--tests/test_utils.py157
-rw-r--r--tests/testdata/__init__.py27
-rw-r--r--tests/testdata/example.data1
-rw-r--r--tests/testdata/example.jpegbin0 -> 301 bytes
-rw-r--r--tests/utils.py68
50 files changed, 4325 insertions, 708 deletions
diff --git a/.gitignore b/.gitignore
index 46b64e3..b4d25fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,11 +1,15 @@
-*.pyc
+# Temporary files
.*.swp
+*.pyc
+*.pyo
+
+# Build-related files
+docs/_build/
.coverage
-MANIFEST
+.tox
+*.egg-info
+*.egg
build/
dist/
htmlcov/
-docs/_build
-docs/_static
-docs/_templates
-.tox
+MANIFEST
diff --git a/.travis.yml b/.travis.yml
index d0735d8..2bfb978 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,7 +11,9 @@ script:
- python setup.py test
install:
- - "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi"
+ - 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
notifications:
email: false
diff --git a/MANIFEST.in b/MANIFEST.in
index 3912fee..3dfc1be 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,6 @@
-include README
+include README.rst
include docs/Makefile
recursive-include docs *.py *.rst
include docs/_static/.keep_dir
prune docs/_build
-recursive-include tests *.py
+recursive-include tests *.py *.data
diff --git a/Makefile b/Makefile
index 274ee32..bb0428b 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,8 @@ PACKAGE=factory
TESTS_DIR=tests
DOC_DIR=docs
+# Use current python binary instead of system default.
+COVERAGE = python $(shell which coverage)
all: default
@@ -11,6 +13,9 @@ default:
clean:
find . -type f -name '*.pyc' -delete
+ find . -type f -path '*/__pycache__/*' -delete
+ find . -type d -empty -delete
+ @rm -rf tmp_test/
test:
@@ -20,10 +25,10 @@ pylint:
pylint --rcfile=.pylintrc --report=no $(PACKAGE)/
coverage:
- coverage erase
- coverage run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test
- coverage report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py"
- coverage html "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py"
+ $(COVERAGE) erase
+ $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test
+ $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py"
+ $(COVERAGE) html "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py"
doc:
$(MAKE) -C $(DOC_DIR) html
diff --git a/README b/README
deleted file mode 100644
index cc26087..0000000
--- a/README
+++ /dev/null
@@ -1,233 +0,0 @@
-factory_boy
-===========
-
-.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master
- :target: http://travis-ci.org/rbarrois/factory_boy/
-
-factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_.
-
-Its features include:
-
-- Straightforward syntax
-- Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects)
-- Powerful helpers for common cases (sequences, sub-factories, reverse dependencies, circular factories, ...)
-- Multiple factories per class support, including inheritance
-- Support for various ORMs (currently Django, Mogo)
-
-
-Links
------
-
-* Documentation: http://factoryboy.readthedocs.org/
-* Official repository: https://github.com/rbarrois/factory_boy
-* Package: https://pypi.python.org/pypi/factory_boy/
-
-factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library.
-
-
-Download
---------
-
-PyPI: https://pypi.python.org/pypi/factory_boy/
-
-.. code-block:: sh
-
- $ pip install factory_boy
-
-Source: https://github.com/rbarrois/factory_boy/
-
-.. code-block:: sh
-
- $ git clone git://github.com/rbarrois/factory_boy/
- $ python setup.py install
-
-
-Usage
------
-
-
-.. note:: This section provides a quick summary of factory_boy features.
- A more detailed listing is available in the full documentation.
-
-
-Defining factories
-""""""""""""""""""
-
-Factories declare a set of attributes used to instantiate an object. The class of the object must be defined in the FACTORY_FOR attribute:
-
-.. code-block:: python
-
- import factory
- from . import models
-
- class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
-
- first_name = 'John'
- last_name = 'Doe'
- admin = False
-
- # Another, different, factory for the same object
- class AdminFactory(factory.Factory):
- FACTORY_FOR = models.User
-
- first_name = 'Admin'
- last_name = 'User'
- admin = True
-
-
-Using factories
-"""""""""""""""
-
-factory_boy supports several different build strategies: build, create, attributes and stub:
-
-.. code-block:: python
-
- # Returns a User instance that's not saved
- user = UserFactory.build()
-
- # 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()
-
-
-You can use the Factory class as a shortcut for the default build strategy:
-
-.. code-block:: python
-
- # Same as UserFactory.create()
- user = UserFactory()
-
-
-No matter which strategy is used, it's possible to override the defined attributes by passing keyword arguments:
-
-.. code-block:: pycon
-
- # Build a User instance and override first_name
- >>> user = UserFactory.build(first_name='Joe')
- >>> user.first_name
- "Joe"
-
-
-Lazy Attributes
-"""""""""""""""
-
-Most factory attributes can be added using static values that are evaluated when the factory is defined,
-but some attributes (such as fields whose value is computed from other elements)
-will need values assigned each time an instance is generated.
-
-These "lazy" attributes can be added as follows:
-
-.. code-block:: python
-
- class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
- first_name = 'Joe'
- last_name = 'Blow'
- email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower())
-
-.. code-block:: pycon
-
- >>> UserFactory().email
- "joe.blow@example.com"
-
-
-Sequences
-"""""""""
-
-Unique values in a specific format (for example, e-mail addresses) can be generated using sequences. Sequences are defined by using ``Sequence`` or the decorator ``sequence``:
-
-.. code-block:: python
-
- class UserFactory(factory.Factory):
- FACTORY_FOR = models.User
- email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n))
-
- >>> UserFactory().email
- 'person0@example.com'
- >>> UserFactory().email
- 'person1@example.com'
-
-
-Associations
-""""""""""""
-
-Some objects have a complex field, that should itself be defined from a dedicated factories.
-This is handled by the ``SubFactory`` helper:
-
-.. code-block:: python
-
- class PostFactory(factory.Factory):
- FACTORY_FOR = models.Post
- author = factory.SubFactory(UserFactory)
-
-
-The associated object's strategy will be used:
-
-
-.. code-block:: python
-
- # Builds and saves a User and a Post
- >>> post = PostFactory()
- >>> post.id is None # Post has been 'saved'
- False
- >>> post.author.id is None # post.author has been saved
- False
-
- # Builds but does not save a User, and then builds but does not save a Post
- >>> post = PostFactory.build()
- >>> post.id is None
- True
- >>> post.author.id is None
- True
-
-
-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.
-
-All pull request should pass the test suite, which can be launched simply with:
-
-.. code-block:: sh
-
- $ python setup.py test
-
-
-.. note::
-
- Running test requires the unittest2 (standard in Python 2.7+) and mock libraries.
-
-
-In order to test coverage, please use:
-
-.. code-block:: sh
-
- $ pip install coverage
- $ coverage erase; coverage run --branch setup.py test; coverage report
-
-
-Contents, indices and tables
-----------------------------
-
-.. toctree::
- :maxdepth: 2
-
- introduction
- reference
- orms
- recipes
- fuzzy
- examples
- internals
- changelog
- ideas
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
-
diff --git a/README.rst b/README.rst
index 100b938..0371b28 120000..100644
--- a/README.rst
+++ b/README.rst
@@ -1 +1,277 @@
-README \ No newline at end of file
+factory_boy
+===========
+
+.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master
+ :target: http://travis-ci.org/rbarrois/factory_boy/
+
+factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_.
+
+Its features include:
+
+- Straightforward syntax
+- Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects)
+- Powerful helpers for common cases (sequences, sub-factories, reverse dependencies, circular factories, ...)
+- Multiple factories per class support, including inheritance
+- Support for various ORMs (currently Django, Mogo, SQLAlchemy)
+
+
+Links
+-----
+
+* Documentation: http://factoryboy.readthedocs.org/
+* Official repository: https://github.com/rbarrois/factory_boy
+* Package: https://pypi.python.org/pypi/factory_boy/
+
+factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library.
+
+
+Download
+--------
+
+PyPI: https://pypi.python.org/pypi/factory_boy/
+
+.. code-block:: sh
+
+ $ pip install factory_boy
+
+Source: https://github.com/rbarrois/factory_boy/
+
+.. code-block:: sh
+
+ $ git clone git://github.com/rbarrois/factory_boy/
+ $ python setup.py install
+
+
+Usage
+-----
+
+
+.. note:: This section provides a quick summary of factory_boy features.
+ A more detailed listing is available in the full documentation.
+
+
+Defining factories
+""""""""""""""""""
+
+Factories declare a set of attributes used to instantiate an object. The class of the object must be defined in the FACTORY_FOR attribute:
+
+.. code-block:: python
+
+ import factory
+ from . import models
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+
+ first_name = 'John'
+ last_name = 'Doe'
+ admin = False
+
+ # Another, different, factory for the same object
+ class AdminFactory(factory.Factory):
+ FACTORY_FOR = models.User
+
+ first_name = 'Admin'
+ last_name = 'User'
+ admin = True
+
+
+Using factories
+"""""""""""""""
+
+factory_boy supports several different build strategies: build, create, attributes and stub:
+
+.. code-block:: python
+
+ # Returns a User instance that's not saved
+ user = UserFactory.build()
+
+ # 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()
+
+
+You can use the Factory class as a shortcut for the default build strategy:
+
+.. code-block:: python
+
+ # Same as UserFactory.create()
+ user = UserFactory()
+
+
+No matter which strategy is used, it's possible to override the defined attributes by passing keyword arguments:
+
+.. code-block:: pycon
+
+ # Build a User instance and override first_name
+ >>> user = UserFactory.build(first_name='Joe')
+ >>> user.first_name
+ "Joe"
+
+
+Lazy Attributes
+"""""""""""""""
+
+Most factory attributes can be added using static values that are evaluated when the factory is defined,
+but some attributes (such as fields whose value is computed from other elements)
+will need values assigned each time an instance is generated.
+
+These "lazy" attributes can be added as follows:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+ first_name = 'Joe'
+ last_name = 'Blow'
+ email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower())
+
+.. code-block:: pycon
+
+ >>> UserFactory().email
+ "joe.blow@example.com"
+
+
+Sequences
+"""""""""
+
+Unique values in a specific format (for example, e-mail addresses) can be generated using sequences. Sequences are defined by using ``Sequence`` or the decorator ``sequence``:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+ email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n))
+
+ >>> UserFactory().email
+ 'person0@example.com'
+ >>> UserFactory().email
+ 'person1@example.com'
+
+
+Associations
+""""""""""""
+
+Some objects have a complex field, that should itself be defined from a dedicated factories.
+This is handled by the ``SubFactory`` helper:
+
+.. code-block:: python
+
+ class PostFactory(factory.Factory):
+ FACTORY_FOR = models.Post
+ author = factory.SubFactory(UserFactory)
+
+
+The associated object's strategy will be used:
+
+
+.. code-block:: python
+
+ # Builds and saves a User and a Post
+ >>> post = PostFactory()
+ >>> post.id is None # Post has been 'saved'
+ False
+ >>> post.author.id is None # post.author has been saved
+ False
+
+ # Builds but does not save a User, and then builds but does not save a Post
+ >>> post = PostFactory.build()
+ >>> post.id is None
+ True
+ >>> post.author.id is None
+ True
+
+
+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:
+
+.. code-block:: python
+
+ with factory.debug():
+ obj = TestModel2Factory()
+
+
+ import logging
+ logger = logging.getLogger('factory')
+ logger.addHandler(logging.StreamHandler())
+ logger.setLevel(logging.DEBUG)
+
+This will yield messages similar to those (artificial indentation):
+
+.. code-block:: ini
+
+ BaseFactory: Preparing tests.test_using.TestModel2Factory(extra={})
+ LazyStub: Computing values for tests.test_using.TestModel2Factory(two=<OrderedDeclarationWrapper for <factory.declarations.SubFactory object at 0x1e15610>>)
+ SubFactory: Instantiating tests.test_using.TestModelFactory(__containers=(<LazyStub for tests.test_using.TestModel2Factory>,), one=4), create=True
+ BaseFactory: Preparing tests.test_using.TestModelFactory(extra={'__containers': (<LazyStub for tests.test_using.TestModel2Factory>,), 'one': 4})
+ LazyStub: Computing values for tests.test_using.TestModelFactory(one=4)
+ LazyStub: Computed values, got tests.test_using.TestModelFactory(one=4)
+ BaseFactory: Generating tests.test_using.TestModelFactory(one=4)
+ LazyStub: Computed values, got tests.test_using.TestModel2Factory(two=<tests.test_using.TestModel object at 0x1e15410>)
+ BaseFactory: Generating tests.test_using.TestModel2Factory(two=<tests.test_using.TestModel object at 0x1e15410>)
+
+
+ORM Support
+"""""""""""
+
+factory_boy has specific support for a few ORMs, through specific :class:`~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`
+
+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.
+
+All pull request should pass the test suite, which can be launched simply with:
+
+.. code-block:: sh
+
+ $ python setup.py test
+
+
+.. note::
+
+ Running test requires the unittest2 (standard in Python 2.7+) and mock libraries.
+
+
+In order to test coverage, please use:
+
+.. code-block:: sh
+
+ $ pip install coverage
+ $ coverage erase; coverage run --branch setup.py test; coverage report
+
+
+Contents, indices and tables
+----------------------------
+
+.. toctree::
+ :maxdepth: 2
+
+ introduction
+ reference
+ orms
+ recipes
+ fuzzy
+ examples
+ internals
+ changelog
+ ideas
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
+
diff --git a/dev_requirements.txt b/dev_requirements.txt
new file mode 100644
index 0000000..e828644
--- /dev/null
+++ b/dev_requirements.txt
@@ -0,0 +1,5 @@
+coverage
+Django
+Pillow
+sqlalchemy
+mongoengine \ No newline at end of file
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 173c40f..4917578 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,24 +1,139 @@
ChangeLog
=========
+
+.. _v2.3.1:
+
+2.3.1 (2014-01-22)
+------------------
+
+*Bugfix:*
+
+ - Fix badly written assert containing state-changing code, spotted by `chsigi <https://github.com/chsigi>`_ (:issue:`126`)
+ - Don't crash when handling objects whose __repr__ is non-pure-ascii bytes on Py2,
+ discovered by `mbertheau <https://github.com/mbertheau>`_ (:issue:`123`) and `strycore <https://github.com/strycore>`_ (:issue:`127`)
+
+.. _v2.3.0:
+
+2.3.0 (2013-12-25)
+------------------
+
+*New:*
+
+ - Add :class:`~factory.fuzzy.FuzzyText`, thanks to `jdufresne <https://github.com/jdufresne>`_ (:issue:`97`)
+ - Add :class:`~factory.fuzzy.FuzzyDecimal`, thanks to `thedrow <https://github.com/thedrow>`_ (:issue:`94`)
+ - Add support for :class:`~mongoengine.EmbeddedDocument`, thanks to `imiric <https://github.com/imiric>`_ (:issue:`100`)
+
+.. _v2.2.1:
+
+2.2.1 (2013-09-24)
+------------------
+
+*Bugfix:*
+
+ - Fixed sequence counter for :class:`~factory.django.DjangoModelFactory` when a factory
+ inherits from another factory relating to an abstract model.
+
+.. _v2.2.0:
+
+2.2.0 (2013-09-24)
+------------------
+
+*Bugfix:*
+
+ - Removed duplicated :class:`~factory.alchemy.SQLAlchemyModelFactory` lurking in :mod:`factory`
+ (:issue:`83`)
+ - Properly handle sequences within object inheritance chains.
+ If FactoryA inherits from FactoryB, and their associated classes share the same link,
+ sequence counters will be shared (:issue:`93`)
+ - Properly handle nested :class:`~factory.SubFactory` overrides
+
+*New:*
+
+ - The :class:`~factory.django.DjangoModelFactory` now supports the ``FACTORY_FOR = 'myapp.MyModel'``
+ syntax, making it easier to shove all factories in a single module (:issue:`66`).
+ - Add :meth:`factory.debug()` helper for easier backtrace analysis
+ - Adding factory support for mongoengine with :class:`~factory.mongoengine.MongoEngineFactory`.
+
+.. _v2.1.2:
+
+2.1.2 (2013-08-14)
+------------------
+
+*New:*
+
+ - The :class:`~factory.Factory.ABSTRACT_FACTORY` keyword is now optional, and automatically set
+ to ``True`` if neither the :class:`~factory.Factory` subclass nor its parent declare the
+ :class:`~factory.Factory.FACTORY_FOR` attribute (:issue:`74`)
+
+
+.. _v2.1.1:
+
+2.1.1 (2013-07-02)
+------------------
+
+*Bugfix:*
+
+ - Properly retrieve the ``color`` keyword argument passed to :class:`~factory.django.ImageField`
+
+.. _v2.1.0:
+
+2.1.0 (2013-06-26)
+------------------
+
+*New:*
+
+ - Add :class:`~factory.fuzzy.FuzzyDate` thanks to `saulshanabrook <https://github.com/saulshanabrook>`_
+ - Add :class:`~factory.fuzzy.FuzzyDateTime` and :class:`~factory.fuzzy.FuzzyNaiveDateTime`.
+ - Add a :attr:`~factory.containers.LazyStub.factory_parent` attribute to the
+ :class:`~factory.containers.LazyStub` passed to :class:`~factory.LazyAttribute`, in order to access
+ fields defined in wrapping factories.
+ - Move :class:`~factory.django.DjangoModelFactory` and :class:`~factory.mogo.MogoFactory`
+ to their own modules (:mod:`factory.django` and :mod:`factory.mogo`)
+ - Add the :meth:`~factory.Factory.reset_sequence` classmethod to :class:`~factory.Factory`
+ to ease resetting the sequence counter for a given factory.
+ - Add debug messages to ``factory`` logger.
+ - Add a :meth:`~factory.Iterator.reset` method to :class:`~factory.Iterator` (:issue:`63`)
+ - Add support for the SQLAlchemy ORM through :class:`~factory.alchemy.SQLAlchemyModelFactory`
+ (:issue:`64`, thanks to `Romain Commandé <https://github.com/rcommande>`_)
+ - Add :class:`factory.django.FileField` and :class:`factory.django.ImageField` hooks for
+ related Django model fields (:issue:`52`)
+
+*Bugfix*
+
+ - Properly handle non-integer pks in :class:`~factory.django.DjangoModelFactory` (:issue:`57`).
+ - Disable :class:`~factory.RelatedFactory` generation when a specific value was
+ passed (:issue:`62`, thanks to `Gabe Koscky <https://github.com/dhekke>`_)
+
+*Deprecation:*
+
+ - Rename :class:`~factory.RelatedFactory`'s ``name`` argument to ``factory_related_name`` (See :issue:`58`)
+
+
+.. _v2.0.2:
+
2.0.2 (2013-04-16)
------------------
*New:*
- - When :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is
+ - When :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is
empty, use ``Model.objects.create()`` instead of ``Model.objects.get_or_create``.
+.. _v2.0.1:
+
2.0.1 (2013-04-16)
------------------
*New:*
- Don't push ``defaults`` to ``get_or_create`` when
- :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set.
+ :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set.
+.. _v2.0.0:
+
2.0.0 (2013-04-15)
------------------
@@ -29,11 +144,11 @@ ChangeLog
- The default :attr:`~factory.Sequence.type` for :class:`~factory.Sequence` is now :obj:`int`
- Fields listed in :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` won't be passed to
the associated class' constructor
- - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory`,
- through :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`.
+ - Add support for ``get_or_create`` in :class:`~factory.django.DjangoModelFactory`,
+ through :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`.
- Add support for :mod:`~factory.fuzzy` attribute definitions.
- The :class:`Sequence` counter can be overridden when calling a generating function
- - Add :class:`~factory.Dict` and :class:`~factory.List` declarations (Closes #18).
+ - Add :class:`~factory.Dict` and :class:`~factory.List` declarations (Closes :issue:`18`).
*Removed:*
@@ -46,6 +161,8 @@ ChangeLog
- Remove :meth:`~factory.Factory.set_building_function` / :meth:`~factory.Factory.set_creation_function`
+.. _v1.3.0:
+
1.3.0 (2013-03-11)
------------------
@@ -60,7 +177,7 @@ New
- **Global:**
- Rewrite the whole documentation
- - Provide a dedicated :class:`~factory.MogoFactory` subclass of :class:`~factory.Factory`
+ - Provide a dedicated :class:`~factory.mogo.MogoFactory` subclass of :class:`~factory.Factory`
- **The Factory class:**
- Better creation/building customization hooks at :meth:`factory.Factory._build` and :meth:`factory.Factory.create`
@@ -77,7 +194,7 @@ New
its :attr:`~factory.Iterator.cycle` argument to ``False``
- Allow overriding default arguments in a :class:`~factory.PostGenerationMethodCall`
when generating an instance of the factory
- - An object created by a :class:`~factory.DjangoModelFactory` will be saved
+ - An object created by a :class:`~factory.django.DjangoModelFactory` will be saved
again after :class:`~factory.PostGeneration` hooks execution
@@ -112,7 +229,7 @@ In order to upgrade client code, apply the following rules:
:class:`~factory.Factory`, instead of relying on automagic associated class
discovery
- When using factory_boy for Django models, have each factory inherit from
- :class:`~factory.DjangoModelFactory`
+ :class:`~factory.django.DjangoModelFactory`
- Replace ``factory.CircularSubFactory('some.module', 'Symbol')`` with
``factory.SubFactory('some.module.Symbol')``
- Replace ``factory.InfiniteIterator(iterable)`` with ``factory.Iterator(iterable)``
@@ -124,6 +241,8 @@ In order to upgrade client code, apply the following rules:
+.. _v1.2.0:
+
1.2.0 (2012-09-08)
------------------
@@ -131,6 +250,9 @@ In order to upgrade client code, apply the following rules:
- Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories
+
+.. _v1.1.5:
+
1.1.5 (2012-07-09)
------------------
@@ -138,6 +260,9 @@ In order to upgrade client code, apply the following rules:
- Fix :class:`~factory.PostGenerationDeclaration` and derived classes.
+
+.. _v1.1.4:
+
1.1.4 (2012-06-19)
------------------
@@ -149,6 +274,9 @@ In order to upgrade client code, apply the following rules:
- Introduce :class:`~factory.PostGeneration` and
:class:`~factory.RelatedFactory`
+
+.. _v1.1.3:
+
1.1.3 (2012-03-09)
------------------
@@ -156,6 +284,9 @@ In order to upgrade client code, apply the following rules:
- Fix packaging rules
+
+.. _v1.1.2:
+
1.1.2 (2012-02-25)
------------------
@@ -165,6 +296,9 @@ In order to upgrade client code, apply the following rules:
- Provide :func:`~factory.Factory.generate` and :func:`~factory.Factory.simple_generate`, that allow specifying the instantiation strategy directly.
Also provides :func:`~factory.Factory.generate_batch` and :func:`~factory.Factory.simple_generate_batch`.
+
+.. _v1.1.1:
+
1.1.1 (2012-02-24)
------------------
@@ -172,6 +306,9 @@ In order to upgrade client code, apply the following rules:
- Add :func:`~factory.Factory.build_batch`, :func:`~factory.Factory.create_batch` and :func:`~factory.Factory.stub_batch`, to instantiate factories in batch
+
+.. _v1.1.0:
+
1.1.0 (2012-02-24)
------------------
@@ -190,6 +327,9 @@ In order to upgrade client code, apply the following rules:
- Auto-discovery of :attr:`~factory.Factory.FACTORY_FOR` based on class name is now deprecated
+
+.. _v1.0.4:
+
1.0.4 (2011-12-21)
------------------
@@ -201,7 +341,7 @@ In order to upgrade client code, apply the following rules:
- Introduce :data:`~factory.MOGO_BUILD` build function
- Add support for inheriting from multiple :class:`~factory.Factory`
- Base :class:`~factory.Factory` classes can now be declared :attr:`abstract <factory.Factory.ABSTRACT_FACTORY>`.
- - Provide :class:`~factory.DjangoModelFactory`, whose :class:`~factory.Sequence` counter starts at the next free database id
+ - Provide :class:`~factory.django.DjangoModelFactory`, whose :class:`~factory.Sequence` counter starts at the next free database id
- Introduce :class:`~factory.SelfAttribute`, a shortcut for ``factory.LazyAttribute(lambda o: o.foo.bar.baz``.
*Bugfix:*
@@ -210,6 +350,9 @@ In order to upgrade client code, apply the following rules:
- Share sequence counter between parent and subclasses
- Fix :class:`~factory.SubFactory` / :class:`~factory.Sequence` interferences
+
+.. _v1.0.2:
+
1.0.2 (2011-05-16)
------------------
@@ -217,6 +360,9 @@ In order to upgrade client code, apply the following rules:
- Introduce :class:`~factory.SubFactory`
+
+.. _v1.0.1:
+
1.0.1 (2011-05-13)
------------------
@@ -229,6 +375,9 @@ In order to upgrade client code, apply the following rules:
- Fix concurrency between :class:`~factory.LazyAttribute` and :class:`~factory.Sequence`
+
+.. _v1.0.0:
+
1.0.0 (2010-08-22)
------------------
diff --git a/docs/conf.py b/docs/conf.py
index 0ccaf29..4f76d45 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -26,7 +26,16 @@ sys.path.insert(0, os.path.dirname(os.path.abspath('.')))
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode']
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.extlinks',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.viewcode',
+]
+
+extlinks = {
+ 'issue': ('https://github.com/rbarrois/factory_boy/issues/%s', 'issue #'),
+}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -237,4 +246,8 @@ intersphinx_mapping = {
'http://docs.djangoproject.com/en/dev/',
'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',
+ ),
}
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
index f1f4085..b94dfa5 100644
--- a/docs/fuzzy.rst
+++ b/docs/fuzzy.rst
@@ -24,6 +24,35 @@ FuzzyAttribute
The callable that generates random values
+FuzzyText
+---------
+
+
+.. class:: FuzzyText(length=12, chars=string.ascii_letters, prefix='')
+
+ The :class:`FuzzyText` fuzzer yields random strings beginning with
+ the given :attr:`prefix`, followed by :attr:`length` charactes chosen
+ from the :attr:`chars` character set,
+ and ending with the given :attr:`suffix`.
+
+ .. attribute:: length
+
+ int, the length of the random part
+
+ .. attribute:: prefix
+
+ text, an optional prefix to prepend to the random part
+
+ .. attribute:: suffix
+
+ text, an optional suffix to append to the random part
+
+ .. attribute:: chars
+
+ char iterable, the chars to choose from; defaults to the list of ascii
+ letters and numbers.
+
+
FuzzyChoice
-----------
@@ -69,6 +98,197 @@ FuzzyInteger
int, the inclusive higher bound of generated integers
+FuzzyDecimal
+------------
+
+.. class:: FuzzyDecimal(low[, high])
+
+ The :class:`FuzzyDecimal` fuzzer generates random integers within a given
+ inclusive range.
+
+ The :attr:`low` bound may be omitted, in which case it defaults to 0:
+
+ .. code-block:: pycon
+
+ >>> FuzzyDecimal(0.5, 42.7)
+ >>> fi.low, fi.high
+ 0.5, 42.7
+
+ >>> fi = FuzzyDecimal(42.7)
+ >>> fi.low, fi.high
+ 0.0, 42.7
+
+ >>> fi = FuzzyDecimal(0.5, 42.7, 3)
+ >>> fi.low, fi.high, fi.precision
+ 0.5, 42.7, 3
+
+ .. attribute:: low
+
+ decimal, the inclusive lower bound of generated decimals
+
+ .. attribute:: high
+
+ decimal, the inclusive higher bound of generated decimals
+
+ .. attribute:: precision
+ int, the number of digits to generate after the dot. The default is 2 digits.
+
+
+FuzzyDate
+---------
+
+.. class:: FuzzyDate(start_date[, end_date])
+
+ The :class:`FuzzyDate` fuzzer generates random dates within a given
+ inclusive range.
+
+ The :attr:`end_date` bound may be omitted, in which case it defaults to the current date:
+
+ .. code-block:: pycon
+
+ >>> fd = FuzzyDate(datetime.date(2008, 1, 1))
+ >>> fd.start_date, fd.end_date
+ datetime.date(2008, 1, 1), datetime.date(2013, 4, 16)
+
+ .. attribute:: start_date
+
+ :class:`datetime.date`, the inclusive lower bound of generated dates
+
+ .. attribute:: end_date
+
+ :class:`datetime.date`, the inclusive higher bound of generated dates
+
+
+FuzzyDateTime
+-------------
+
+.. class:: FuzzyDateTime(start_dt[, end_dt], tz=UTC, force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None)
+
+ The :class:`FuzzyDateTime` fuzzer generates random timezone-aware datetime within a given
+ inclusive range.
+
+ The :attr:`end_dt` bound may be omitted, in which case it defaults to ``datetime.datetime.now()``
+ localized into the UTC timezone.
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=UTC))
+ >>> fdt.start_dt, fdt.end_dt
+ datetime.datetime(2008, 1, 1, tzinfo=UTC), datetime.datetime(2013, 4, 21, 19, 13, 32, 458487, tzinfo=UTC)
+
+
+ The ``force_XXX`` keyword arguments force the related value of generated datetimes:
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=UTC), datetime.datetime(2009, 1, 1, tzinfo=UTC),
+ ... force_day=3, force_second=42)
+ >>> fdt.evaluate(2, None, False) # Actual code used by ``SomeFactory.build()``
+ datetime.datetime(2008, 5, 3, 12, 13, 42, 124848, tzinfo=UTC)
+
+
+ .. attribute:: start_dt
+
+ :class:`datetime.datetime`, the inclusive lower bound of generated datetimes
+
+ .. attribute:: end_dt
+
+ :class:`datetime.datetime`, the inclusive upper bound of generated datetimes
+
+
+ .. attribute:: force_year
+
+ int or None; if set, forces the :attr:`~datetime.datetime.year` of generated datetime.
+
+ .. attribute:: force_month
+
+ int or None; if set, forces the :attr:`~datetime.datetime.month` of generated datetime.
+
+ .. attribute:: force_day
+
+ int or None; if set, forces the :attr:`~datetime.datetime.day` of generated datetime.
+
+ .. attribute:: force_hour
+
+ int or None; if set, forces the :attr:`~datetime.datetime.hour` of generated datetime.
+
+ .. attribute:: force_minute
+
+ int or None; if set, forces the :attr:`~datetime.datetime.minute` of generated datetime.
+
+ .. attribute:: force_second
+
+ int or None; if set, forces the :attr:`~datetime.datetime.second` of generated datetime.
+
+ .. attribute:: force_microsecond
+
+ int or None; if set, forces the :attr:`~datetime.datetime.microsecond` of generated datetime.
+
+
+FuzzyNaiveDateTime
+------------------
+
+.. class:: FuzzyNaiveDateTime(start_dt[, end_dt], force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None)
+
+ The :class:`FuzzyNaiveDateTime` fuzzer generates random naive datetime within a given
+ inclusive range.
+
+ The :attr:`end_dt` bound may be omitted, in which case it defaults to ``datetime.datetime.now()``:
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyNaiveDateTime(datetime.datetime(2008, 1, 1))
+ >>> fdt.start_dt, fdt.end_dt
+ datetime.datetime(2008, 1, 1), datetime.datetime(2013, 4, 21, 19, 13, 32, 458487)
+
+
+ The ``force_XXX`` keyword arguments force the related value of generated datetimes:
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyNaiveDateTime(datetime.datetime(2008, 1, 1), datetime.datetime(2009, 1, 1),
+ ... force_day=3, force_second=42)
+ >>> fdt.evaluate(2, None, False) # Actual code used by ``SomeFactory.build()``
+ datetime.datetime(2008, 5, 3, 12, 13, 42, 124848)
+
+
+ .. attribute:: start_dt
+
+ :class:`datetime.datetime`, the inclusive lower bound of generated datetimes
+
+ .. attribute:: end_dt
+
+ :class:`datetime.datetime`, the inclusive upper bound of generated datetimes
+
+
+ .. attribute:: force_year
+
+ int or None; if set, forces the :attr:`~datetime.datetime.year` of generated datetime.
+
+ .. attribute:: force_month
+
+ int or None; if set, forces the :attr:`~datetime.datetime.month` of generated datetime.
+
+ .. attribute:: force_day
+
+ int or None; if set, forces the :attr:`~datetime.datetime.day` of generated datetime.
+
+ .. attribute:: force_hour
+
+ int or None; if set, forces the :attr:`~datetime.datetime.hour` of generated datetime.
+
+ .. attribute:: force_minute
+
+ int or None; if set, forces the :attr:`~datetime.datetime.minute` of generated datetime.
+
+ .. attribute:: force_second
+
+ int or None; if set, forces the :attr:`~datetime.datetime.second` of generated datetime.
+
+ .. attribute:: force_microsecond
+
+ int or None; if set, forces the :attr:`~datetime.datetime.microsecond` of generated datetime.
+
Custom fuzzy fields
-------------------
diff --git a/docs/introduction.rst b/docs/introduction.rst
index 8bbb10c..86e2046 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -99,7 +99,7 @@ This is achieved with the :class:`~factory.Sequence` declaration:
>>> UserFactory()
<User: user2>
-.. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator:
+.. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator (note that ``self`` is not added as first parameter):
.. code-block:: python
@@ -107,7 +107,7 @@ This is achieved with the :class:`~factory.Sequence` declaration:
FACTORY_FOR = models.User
@factory.sequence
- def username(self, n):
+ def username(n):
return 'user%d' % n
@@ -140,7 +140,19 @@ taking the object being built and returning the value for the field:
<User: user3 (doe@example.com)>
-.. note:: As for :class:`~factory.Sequence`, a :meth:`~factory.@lazy_attribute` decorator is available.
+.. note:: As for :class:`~factory.Sequence`, a :meth:`~factory.@lazy_attribute` decorator is available:
+
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+
+ username = factory.Sequence(lambda n: 'user%d' % n)
+
+ @factory.lazy_attribute
+ def email(self):
+ return '%s@example.com' % self.username
Inheritance
@@ -227,7 +239,7 @@ All factories support two built-in strategies:
as for a Django model.
Starting from 2.0, :meth:`factory.Factory.create` simply calls ``AssociatedClass(**kwargs)``.
- You should use :class:`~factory.DjangoModelFactory` for Django models.
+ You should use :class:`~factory.django.DjangoModelFactory` for Django models.
When a :class:`~factory.Factory` includes related fields (:class:`~factory.SubFactory`, :class:`~factory.RelatedFactory`),
@@ -253,6 +265,6 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the
<MyClass: X (saved)>
-The default strategy can ba changed by setting the class-level :attr:`~factory.Factory.FACTROY_STRATEGY` attribute.
+The default strategy can be changed by setting the class-level :attr:`~factory.Factory.FACTORY_STRATEGY` attribute.
diff --git a/docs/orms.rst b/docs/orms.rst
index 8e5b6f6..e50e706 100644
--- a/docs/orms.rst
+++ b/docs/orms.rst
@@ -11,6 +11,8 @@ adding dedicated features.
Django
------
+.. currentmodule:: factory.django
+
The first versions of factory_boy were designed specifically for Django,
but the library has now evolved to be framework-independant.
@@ -24,14 +26,16 @@ All factories for a Django :class:`~django.db.models.Model` should use the
:class:`DjangoModelFactory` base class.
-.. class:: DjangoModelFactory(Factory)
+.. class:: DjangoModelFactory(factory.Factory)
Dedicated class for Django :class:`~django.db.models.Model` factories.
This class provides the following features:
- * :func:`~Factory.create()` uses :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>`
- * :func:`~Factory._setup_next_sequence()` selects the next unused primary key value
+ * The :attr:`~factory.Factory.FACTORY_FOR` 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.
@@ -44,8 +48,8 @@ All factories for a Django :class:`~django.db.models.Model` should use the
.. code-block:: python
- class UserFactory(factory.DjangoModelFactory):
- FACTORY_FOR = models.User
+ class UserFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = 'myapp.User' # Equivalent to ``FACTORY_FOR = myapp.models.User``
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
username = 'john'
@@ -68,3 +72,201 @@ All factories for a Django :class:`~django.db.models.Model` should use the
<User: jack>
>>> User.objects.all()
[<User: john>, <User: jack>]
+
+
+.. note:: If a :class:`DjangoModelFactory` relates to an :obj:`~django.db.models.Options.abstract`
+ model, be sure to declare the :class:`DjangoModelFactory` as abstract:
+
+ .. code-block:: python
+
+ class MyAbstractModelFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.MyAbstractModel
+ ABSTRACT_FACTORY = True
+
+ class MyConcreteModelFactory(MyAbstractModelFactory):
+ FACTORY_FOR = models.MyConcreteModel
+
+ Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model.
+
+
+.. class:: FileField
+
+ Custom declarations for :class:`django.db.models.FileField`
+
+ .. method:: __init__(self, from_path='', from_file='', data=b'', filename='example.dat')
+
+ :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
+ :param bytes data: Use the provided bytes as file contents
+ :param str filename: The filename for the FileField
+
+.. note:: If the value ``None`` was passed for the :class:`FileField` field, this will
+ disable field generation:
+
+.. code-block:: python
+
+ class MyFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.MyModel
+
+ the_file = factory.django.FileField(filename='the_file.dat')
+
+.. code-block:: pycon
+
+ >>> MyFactory(the_file__data=b'uhuh').the_file.read()
+ b'uhuh'
+ >>> MyFactory(the_file=None).the_file
+ None
+
+
+.. class:: ImageField
+
+ Custom declarations for :class:`django.db.models.ImageField`
+
+ .. method:: __init__(self, from_path='', from_file='', filename='example.jpg', width=100, height=100, color='green', format='JPEG')
+
+ :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
+ :param str filename: The filename for the ImageField
+ :param int width: The width of the generated image (default: ``100``)
+ :param int height: The height of the generated image (default: ``100``)
+ :param str color: The color of the generated image (default: ``'green'``)
+ :param str format: The image format (as supported by PIL) (default: ``'JPEG'``)
+
+.. note:: If the value ``None`` was passed for the :class:`FileField` field, this will
+ disable field generation:
+
+.. note:: Just as Django's :class:`django.db.models.ImageField` requires the
+ Python Imaging Library, this :class:`ImageField` requires it too.
+
+.. code-block:: python
+
+ class MyFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.MyModel
+
+ the_image = factory.django.ImageField(color='blue')
+
+.. code-block:: pycon
+
+ >>> MyFactory(the_image__width=42).the_image.width
+ 42
+ >>> MyFactory(the_image=None).the_image
+ None
+
+
+Mogo
+----
+
+.. currentmodule:: factory.mogo
+
+factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` class.
+
+`Mogo`_ is a wrapper around the ``pymongo`` library for MongoDB.
+
+.. _Mogo: https://github.com/joshmarshall/mogo
+
+.. class:: MogoFactory(factory.Factory)
+
+ Dedicated class for `Mogo`_ models.
+
+ This class provides the following features:
+
+ * :func:`~factory.Factory.build()` calls a model's ``new()`` method
+ * :func:`~factory.Factory.create()` builds an instance through ``new()`` then
+ saves it.
+
+
+MongoEngine
+-----------
+
+.. currentmodule:: factory.mongoengine
+
+factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngineFactory` class.
+
+`mongoengine`_ is a wrapper around the ``pymongo`` library for MongoDB.
+
+.. _mongoengine: http://mongoengine.org/
+
+.. class:: MongoEngineFactory(factory.Factory)
+
+ Dedicated class for `MongoEngine`_ models.
+
+ This class provides the following features:
+
+ * :func:`~factory.Factory.build()` calls a model's ``__init__`` method
+ * :func:`~factory.Factory.create()` builds an instance through ``__init__`` then
+ saves it.
+
+ .. note:: If the :attr:`associated class <factory.Factory.FACTORY_FOR>` is a :class:`mongoengine.EmbeddedDocument`,
+ the :meth:`~MongoEngineFactory.create` function won't "save" it, since this wouldn't make sense.
+
+ This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document.
+
+
+SQLAlchemy
+----------
+
+.. currentmodule:: factory.alchemy
+
+
+Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class.
+
+To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_SESSION" class attribute.
+
+.. _SQLAlchemy: http://www.sqlalchemy.org/
+
+.. class:: SQLAlchemyModelFactory(factory.Factory)
+
+ Dedicated class for `SQLAlchemy`_ models.
+
+ This class provides the following features:
+
+ * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add`
+ * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value
+
+ .. attribute:: FACTORY_SESSION
+
+ Fields whose SQLAlchemy session object are passed will be used to communicate with the database
+
+A (very) simple exemple:
+
+.. code-block:: python
+
+ from sqlalchemy import Column, Integer, Unicode, create_engine
+ 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)
+ Base = declarative_base()
+
+
+ class User(Base):
+ """ A SQLAlchemy simple model class who represents a user """
+ __tablename__ = 'UserTable'
+
+ id = Column(Integer(), primary_key=True)
+ name = Column(Unicode(20))
+
+ Base.metadata.create_all(engine)
+
+
+ class UserFactory(SQLAlchemyModelFactory):
+ FACTORY_FOR = User
+ FACTORY_SESSION = session # the SQLAlchemy session object
+
+ id = factory.Sequence(lambda n: n)
+ name = factory.Sequence(lambda n: u'User %d' % n)
+
+.. code-block:: pycon
+
+ >>> session.query(User).all()
+ []
+ >>> UserFactory()
+ <User: User 1>
+ >>> session.query(User).all()
+ [<User: User 1>]
diff --git a/docs/recipes.rst b/docs/recipes.rst
index e226732..c1f3700 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -25,7 +25,7 @@ use the :class:`~factory.SubFactory` declaration:
import factory
from . import models
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.User
first_name = factory.Sequence(lambda n: "Agent %03d" % n)
@@ -52,7 +52,7 @@ use a :class:`~factory.RelatedFactory` declaration:
# factories.py
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.User
log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE)
@@ -62,6 +62,65 @@ When a :class:`UserFactory` is instantiated, factory_boy will call
``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``.
+Example: Django's Profile
+"""""""""""""""""""""""""
+
+Django (<1.5) provided a mechanism to attach a ``Profile`` to a ``User`` instance,
+using a :class:`~django.db.models.ForeignKey` from the ``Profile`` to the ``User``.
+
+A typical way to create those profiles was to hook a post-save signal to the ``User`` model.
+
+factory_boy allows to define attributes of such profiles dynamically when creating a ``User``:
+
+.. code-block:: python
+
+ class ProfileFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = my_models.Profile
+
+ title = 'Dr'
+ # We pass in profile=None to prevent UserFactory from creating another profile
+ # (this disables the RelatedFactory)
+ user = factory.SubFactory('app.factories.UserFactory', profile=None)
+
+ class UserFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = auth_models.User
+
+ username = factory.Sequence(lambda n: "user_%d" % n)
+
+ # We pass in 'user' to link the generated Profile to our just-generated User
+ # This will call ProfileFactory(user=our_new_user), thus skipping the SubFactory.
+ profile = factory.RelatedFactory(ProfileFactory, 'user')
+
+ @classmethod
+ def _generate(cls, create, attrs):
+ """Override the default _generate() to disable the post-save signal."""
+
+ # Note: If the signal was defined with a dispatch_uid, include that in both calls.
+ post_save.disconnect(handler_create_user_profile, auth_models.User)
+ user = super(UserFactory, cls)._generate(create, attrs)
+ post_save.connect(handler_create_user_profile, auth_models.User)
+ return user
+
+.. OHAI_VIM:*
+
+
+.. code-block:: pycon
+
+ >>> u = UserFactory(profile__title=u"Lord")
+ >>> u.get_profile().title
+ u"Lord"
+
+Such behaviour can be extended to other situations where a signal interferes with
+factory_boy related factories.
+
+.. note:: When any :class:`~factory.RelatedFactory` or :class:`~factory.post_generation`
+ attribute is defined on the :class:`~factory.django.DjangoModelFactory` subclass,
+ a second ``save()`` is performed *after* the call to ``_create()``.
+
+ Code working with signals should thus override the :meth:`~factory.Factory._generate`
+ method.
+
+
Simple ManyToMany
-----------------
@@ -85,12 +144,12 @@ hook:
# factories.py
- class GroupFactory(factory.DjangoModelFactory):
+ class GroupFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.Group
name = factory.Sequence(lambda n: "Group #%s" % n)
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.User
name = "John Doe"
@@ -140,17 +199,17 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations:
# factories.py
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.User
name = "John Doe"
- class GroupFactory(factory.DjangoModelFactory):
+ class GroupFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.Group
name = "Admins"
- class GroupLevelFactory(factory.DjangoModelFactory):
+ class GroupLevelFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.GroupLevel
user = factory.SubFactory(UserFactory)
@@ -213,20 +272,20 @@ Here, we want:
.. code-block:: python
# factories.py
- class CountryFactory(factory.DjangoModelFactory):
+ class CountryFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.Country
name = factory.Iterator(["France", "Italy", "Spain"])
lang = factory.Iterator(['fr', 'it', 'es'])
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.User
name = "John"
lang = factory.SelfAttribute('country.lang')
country = factory.SubFactory(CountryFactory)
- class CompanyFactory(factory.DjangoModelFactory):
+ class CompanyFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.Company
name = "ACME, Inc."
diff --git a/docs/reference.rst b/docs/reference.rst
index 81aa645..53584a0 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -19,15 +19,18 @@ The :class:`Factory` class
.. attribute:: FACTORY_FOR
- This required attribute describes the class of objects to generate.
- It may only be absent if the factory has been marked abstract through
- :attr:`ABSTRACT_FACTORY`.
+ This optional attribute describes the class of objects to generate.
+
+ If unset, it will be inherited from parent :class:`Factory` subclasses.
.. attribute:: ABSTRACT_FACTORY
This attribute indicates that the :class:`Factory` subclass should not
be used to generate objects, but instead provides some extra defaults.
+ It will be automatically set to ``True`` if neither the :class:`Factory`
+ subclass nor its parents define the :attr:`~Factory.FACTORY_FOR` attribute.
+
.. attribute:: FACTORY_ARG_PARAMETERS
Some factories require non-keyword arguments to their :meth:`~object.__init__`.
@@ -211,7 +214,7 @@ The :class:`Factory` class
.. code-block:: python
class BaseBackendFactory(factory.Factory):
- ABSTRACT_FACTORY = True
+ ABSTRACT_FACTORY = True # Optional
def _create(cls, target_class, *args, **kwargs):
obj = target_class(*args, **kwargs)
@@ -234,6 +237,48 @@ The :class:`Factory` class
values, for instance.
+ **Advanced functions:**
+
+
+ .. classmethod:: reset_sequence(cls, value=None, force=False)
+
+ :arg int value: The value to reset the sequence to
+ :arg bool force: Whether to force-reset the sequence
+
+ Allows to reset the sequence counter for a :class:`~factory.Factory`.
+ The new value can be passed in as the ``value`` argument:
+
+ .. code-block:: pycon
+
+ >>> SomeFactory.reset_sequence(4)
+ >>> SomeFactory._next_sequence
+ 4
+
+ Since subclasses of a non-:attr:`abstract <factory.Factory.ABSTRACT_FACTORY>`
+ :class:`~factory.Factory` share the same sequence counter, special care needs
+ to be taken when resetting the counter of such a subclass.
+
+ By default, :meth:`reset_sequence` will raise a :exc:`ValueError` when
+ called on a subclassed :class:`~factory.Factory` subclass. This can be
+ avoided by passing in the ``force=True`` flag:
+
+ .. code-block:: pycon
+
+ >>> InheritedFactory.reset_sequence()
+ Traceback (most recent call last):
+ File "factory_boy/tests/test_base.py", line 179, in test_reset_sequence_subclass_parent
+ SubTestObjectFactory.reset_sequence()
+ File "factory_boy/factory/base.py", line 250, in reset_sequence
+ "Cannot reset the sequence of a factory subclass. "
+ ValueError: Cannot reset the sequence of a factory subclass. Please call reset_sequence() on the root factory, or call reset_sequence(forward=True).
+
+ >>> InheritedFactory.reset_sequence(force=True)
+ >>>
+
+ This is equivalent to calling :meth:`reset_sequence` on the base
+ factory in the chain.
+
+
.. _strategies:
Strategies
@@ -318,6 +363,38 @@ factory_boy supports two main strategies for generating instances, plus stubs.
with a default strategy set to :data:`STUB_STRATEGY`.
+.. function:: debug(logger='factory', stream=None)
+
+ :param str logger: The name of the logger to enable debug for
+ :param file stream: The stream to send debug output to, defaults to :obj:`sys.stderr`
+
+ Context manager to help debugging factory_boy behavior.
+ It will temporarily put the target logger (e.g ``'factory'``) in debug mode,
+ sending all output to :obj`~sys.stderr`;
+ upon leaving the context, the logging levels are reset.
+
+ A typical use case is to understand what happens during a single factory call:
+
+ .. code-block:: python
+
+ with factory.debug():
+ obj = TestModel2Factory()
+
+ This will yield messages similar to those (artificial indentation):
+
+ .. code-block:: ini
+
+ BaseFactory: Preparing tests.test_using.TestModel2Factory(extra={})
+ LazyStub: Computing values for tests.test_using.TestModel2Factory(two=<OrderedDeclarationWrapper for <factory.declarations.SubFactory object at 0x1e15610>>)
+ SubFactory: Instantiating tests.test_using.TestModelFactory(__containers=(<LazyStub for tests.test_using.TestModel2Factory>,), one=4), create=True
+ BaseFactory: Preparing tests.test_using.TestModelFactory(extra={'__containers': (<LazyStub for tests.test_using.TestModel2Factory>,), 'one': 4})
+ LazyStub: Computing values for tests.test_using.TestModelFactory(one=4)
+ LazyStub: Computed values, got tests.test_using.TestModelFactory(one=4)
+ BaseFactory: Generating tests.test_using.TestModelFactory(one=4)
+ LazyStub: Computed values, got tests.test_using.TestModel2Factory(two=<tests.test_using.TestModel object at 0x1e15410>)
+ BaseFactory: Generating tests.test_using.TestModel2Factory(two=<tests.test_using.TestModel object at 0x1e15410>)
+
+
.. _declarations:
Declarations
@@ -353,6 +430,11 @@ accept the object being built as sole argument, and return a value.
'leo@example.com'
+The object passed to :class:`LazyAttribute` is not an instance of the target class,
+but instead a :class:`~containers.LazyStub`: a temporary container that computes
+the value of all declared fields.
+
+
Decorator
~~~~~~~~~
@@ -586,7 +668,7 @@ handles more complex cases:
SubFactory
""""""""""
-.. class:: SubFactory(sub_factory, **kwargs)
+.. class:: SubFactory(factory, **kwargs)
.. OHAI_VIM**
@@ -601,6 +683,20 @@ The :class:`SubFactory` attribute should be called with:
factory
+.. note::
+
+ When passing an actual :class:`~factory.Factory` for the
+ :attr:`~factory.SubFactory.factory` argument, make sure to pass
+ the class and not instance (i.e no ``()`` after the class):
+
+ .. code-block:: python
+
+ class FooFactory(factory.Factory):
+ FACTORY_FOR = Foo
+
+ bar = factory.SubFactory(BarFactory) # Not BarFactory()
+
+
Definition
~~~~~~~~~~
@@ -786,6 +882,19 @@ Obviously, this "follow parents" hability also handles overriding some attribute
'cn'
+This feature is also available to :class:`LazyAttribute` and :class:`LazyAttributeSequence`,
+through the :attr:`~containers.LazyStub.factory_parent` attribute of the passed-in object:
+
+.. code-block:: python
+
+ class CompanyFactory(factory.Factory):
+ FACTORY_FOR = Company
+ country = factory.SubFactory(CountryFactory)
+ owner = factory.SubFactory(UserFactory,
+ language=factory.LazyAttribute(lambda user: user.factory_parent.country.language),
+ )
+
+
Iterator
""""""""
@@ -810,6 +919,14 @@ Iterator
.. versionadded:: 1.3.0
+ .. method:: reset()
+
+ Reset the internal iterator used by the attribute, so that the next value
+ will be the first value generated by the iterator.
+
+ May be called several times.
+
+
Each call to the factory will receive the next value from the iterable:
.. code-block:: python
@@ -879,6 +996,24 @@ use the :func:`iterator` decorator:
yield line
+Resetting
+~~~~~~~~~
+
+In order to start back at the first value in an :class:`Iterator`,
+simply call the :meth:`~Iterator.reset` method of that attribute
+(accessing it from the bare :class:`~Factory` subclass):
+
+.. code-block:: pycon
+
+ >>> UserFactory().lang
+ 'en'
+ >>> UserFactory().lang
+ 'fr'
+ >>> UserFactory.lang.reset()
+ >>> UserFactory().lang
+ 'en'
+
+
Dict and List
"""""""""""""
@@ -1013,7 +1148,7 @@ as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory.FACTORY_
RelatedFactory
""""""""""""""
-.. class:: RelatedFactory(factory, name='', **kwargs)
+.. class:: RelatedFactory(factory, factory_related_name='', **kwargs)
.. OHAI_VIM**
@@ -1033,13 +1168,27 @@ RelatedFactory
.. attribute:: name
The generated object (where the :class:`RelatedFactory` attribute will
- set) may be passed to the related factory if the :attr:`name` parameter
+ set) may be passed to the related factory if the :attr:`factory_related_name` parameter
is set.
It will be passed as a keyword argument, using the :attr:`name` value as
keyword:
+.. note::
+
+ When passing an actual :class:`~factory.Factory` for the
+ :attr:`~factory.RelatedFactory.factory` argument, make sure to pass
+ the class and not instance (i.e no ``()`` after the class):
+
+ .. code-block:: python
+
+ class FooFactory(factory.Factory):
+ FACTORY_FOR = Foo
+
+ bar = factory.RelatedFactory(BarFactory) # Not BarFactory()
+
+
.. code-block:: python
class CityFactory(factory.Factory):
@@ -1069,6 +1218,22 @@ Extra kwargs may be passed to the related factory, through the usual ``ATTR__SUB
>>> City.objects.get(capital_of=england)
<City: London>
+If a value if passed for the :class:`RelatedFactory` attribute, this disables
+:class:`RelatedFactory` generation:
+
+.. code-block:: pycon
+
+ >>> france = CountryFactory()
+ >>> paris = City.objects.get()
+ >>> paris
+ <City: Paris>
+ >>> reunion = CountryFactory(capital_city=paris)
+ >>> City.objects.count() # No new capital_city generated
+ 1
+ >>> guyane = CountryFactory(capital_city=paris, capital_city__name='Kourou')
+ >>> City.objects.count() # No new capital_city generated, ``name`` ignored.
+ 1
+
PostGeneration
""""""""""""""
@@ -1224,7 +1389,7 @@ factory during instantiation.
.. code-block:: python
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = User
username = 'user'
@@ -1315,12 +1480,12 @@ Lightweight factory declaration
UserFactory = make_factory(models.User,
login='john',
email=factory.LazyAttribute(lambda u: '%s@example.com' % u.login),
- FACTORY_CLASS=factory.DjangoModelFactory,
+ FACTORY_CLASS=factory.django.DjangoModelFactory,
)
# This is equivalent to:
- class UserFactory(factory.DjangoModelFactory):
+ class UserFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = models.User
login = 'john'
diff --git a/factory/__init__.py b/factory/__init__.py
index e1138fa..aa550e8 100644
--- a/factory/__init__.py
+++ b/factory/__init__.py
@@ -20,18 +20,17 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-__version__ = '2.0.2'
+__version__ = '2.3.1'
__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
+
from .base import (
Factory,
BaseDictFactory,
DictFactory,
BaseListFactory,
ListFactory,
- MogoFactory,
StubFactory,
- DjangoModelFactory,
BUILD_STRATEGY,
CREATE_STRATEGY,
@@ -39,6 +38,10 @@ from .base import (
use_strategy,
)
+# Backward compatibility; this should be removed soon.
+from .mogo import MogoFactory
+from .django import DjangoModelFactory
+
from .declarations import (
LazyAttribute,
Iterator,
@@ -55,6 +58,8 @@ from .declarations import (
)
from .helpers import (
+ debug,
+
build,
create,
stub,
diff --git a/factory/alchemy.py b/factory/alchemy.py
new file mode 100644
index 0000000..cec15c9
--- /dev/null
+++ b/factory/alchemy.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 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
+# 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 __future__ import unicode_literals
+from sqlalchemy.sql.functions import max
+
+from . import base
+
+
+class SQLAlchemyModelFactory(base.Factory):
+ """Factory for SQLAlchemy models. """
+
+ ABSTRACT_FACTORY = True
+ FACTORY_SESSION = None
+
+ @classmethod
+ def _setup_next_sequence(cls, *args, **kwargs):
+ """Compute the next available PK, based on the 'pk' database field."""
+ session = cls.FACTORY_SESSION
+ model = cls.FACTORY_FOR
+ pk = getattr(model, model.__mapper__.primary_key[0].name)
+ max_pk = session.query(max(pk)).one()[0]
+ if isinstance(max_pk, int):
+ return max_pk + 1 if max_pk else 1
+ else:
+ return 1
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """Create an instance of the model, and save it to the database."""
+ session = cls.FACTORY_SESSION
+ obj = target_class(*args, **kwargs)
+ session.add(obj)
+ return obj
diff --git a/factory/base.py b/factory/base.py
index 2ff2944..3c6571c 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -20,7 +20,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+import logging
+
from . import containers
+from . import utils
+
+logger = logging.getLogger('factory.generate')
# Strategies
BUILD_STRATEGY = 'build'
@@ -35,6 +40,7 @@ FACTORY_CLASS_DECLARATION = 'FACTORY_FOR'
CLASS_ATTRIBUTE_DECLARATIONS = '_declarations'
CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations'
CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class'
+CLASS_ATTRIBUTE_IS_ABSTRACT = '_abstract_factory'
class FactoryError(Exception):
@@ -96,10 +102,6 @@ class FactoryMetaClass(type):
Returns:
class: the class to associate with this factory
-
- Raises:
- AssociatedClassError: If we were unable to associate this factory
- to a class.
"""
if FACTORY_CLASS_DECLARATION in attrs:
return attrs[FACTORY_CLASS_DECLARATION]
@@ -109,10 +111,8 @@ class FactoryMetaClass(type):
if inherited is not None:
return inherited
- raise AssociatedClassError(
- "Could not determine the class associated with %s. "
- "Use the FACTORY_FOR attribute to specify an associated class." %
- class_name)
+ # Nothing found, return None.
+ return None
@classmethod
def _extract_declarations(mcs, bases, attributes):
@@ -150,7 +150,7 @@ class FactoryMetaClass(type):
return attributes
- def __new__(mcs, class_name, bases, attrs, extra_attrs=None):
+ def __new__(mcs, class_name, bases, attrs):
"""Record attributes as a pattern for later instance construction.
This is called when a new Factory subclass is defined; it will collect
@@ -161,9 +161,6 @@ class FactoryMetaClass(type):
bases (list of class): the parents of the class being created
attrs (str => obj dict): the attributes as defined in the class
definition
- extra_attrs (str => obj dict): extra attributes that should not be
- included in the factory defaults, even if public. This
- argument is only provided by extensions of this metaclass.
Returns:
A new class
@@ -173,44 +170,77 @@ class FactoryMetaClass(type):
return super(FactoryMetaClass, mcs).__new__(
mcs, class_name, bases, attrs)
- is_abstract = attrs.pop('ABSTRACT_FACTORY', False)
extra_attrs = {}
- if not is_abstract:
+ is_abstract = attrs.pop('ABSTRACT_FACTORY', False)
+
+ base = parent_factories[0]
+ inherited_associated_class = base._get_target_class()
+ associated_class = mcs._discover_associated_class(class_name, attrs,
+ inherited_associated_class)
- base = parent_factories[0]
+ # Invoke 'lazy-loading' hooks.
+ associated_class = base._load_target_class(associated_class)
- inherited_associated_class = getattr(base,
- CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
- associated_class = mcs._discover_associated_class(class_name, attrs,
- inherited_associated_class)
+ if associated_class is None:
+ is_abstract = True
+ else:
# If inheriting the factory from a parent, keep a link to it.
# This allows to use the sequence counters from the parents.
- if associated_class == inherited_associated_class:
+ if (inherited_associated_class is not None
+ and issubclass(associated_class, inherited_associated_class)):
attrs['_base_factory'] = base
# The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into
# account when parsing the declared attributes of the new class.
- extra_attrs = {CLASS_ATTRIBUTE_ASSOCIATED_CLASS: associated_class}
+ extra_attrs[CLASS_ATTRIBUTE_ASSOCIATED_CLASS] = associated_class
+
+ extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract
# Extract pre- and post-generation declarations
attributes = mcs._extract_declarations(parent_factories, attrs)
-
- # Add extra args if provided.
- if extra_attrs:
- attributes.update(extra_attrs)
+ attributes.update(extra_attrs)
return super(FactoryMetaClass, mcs).__new__(
mcs, class_name, bases, attributes)
def __str__(cls):
- return '<%s for %s>' % (cls.__name__,
- getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
+ if cls._abstract_factory:
+ return '<%s (abstract)>'
+ else:
+ return '<%s for %s>' % (cls.__name__,
+ getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
# Factory base classes
+
+class _Counter(object):
+ """Simple, naive counter.
+
+ Attributes:
+ for_class (obj): the class this counter related to
+ seq (int): the next value
+ """
+
+ def __init__(self, seq, for_class):
+ self.seq = seq
+ self.for_class = for_class
+
+ def next(self):
+ value = self.seq
+ self.seq += 1
+ return value
+
+ def reset(self, next_value=0):
+ self.seq = next_value
+
+ def __repr__(self):
+ return '<_Counter for %s.%s, next=%d>' % (
+ self.for_class.__module__, self.for_class.__name__, self.seq)
+
+
class BaseFactory(object):
"""Factory base support for sequences, attributes and stubs."""
@@ -223,16 +253,19 @@ class BaseFactory(object):
raise FactoryError('You cannot instantiate BaseFactory')
# ID to use for the next 'declarations.Sequence' attribute.
- _next_sequence = None
+ _counter = None
# Base factory, if this class was inherited from another factory. This is
- # used for sharing the _next_sequence counter among factories for the same
+ # used for sharing the sequence _counter among factories for the same
# class.
_base_factory = None
# Holds the target class, once resolved.
_associated_class = None
+ # Whether this factory is considered "abstract", thus uncallable.
+ _abstract_factory = False
+
# List of arguments that should be passed as *args instead of **kwargs
FACTORY_ARG_PARAMETERS = ()
@@ -240,6 +273,31 @@ class BaseFactory(object):
FACTORY_HIDDEN_ARGS = ()
@classmethod
+ def reset_sequence(cls, value=None, force=False):
+ """Reset the sequence counter.
+
+ Args:
+ value (int or None): the new 'next' sequence value; if None,
+ recompute the next value from _setup_next_sequence().
+ force (bool): whether to force-reset parent sequence counters
+ in a factory inheritance chain.
+ """
+ if cls._base_factory:
+ if force:
+ cls._base_factory.reset_sequence(value=value)
+ else:
+ raise ValueError(
+ "Cannot reset the sequence of a factory subclass. "
+ "Please call reset_sequence() on the root factory, "
+ "or call reset_sequence(force=True)."
+ )
+ else:
+ cls._setup_counter()
+ if value is None:
+ value = cls._setup_next_sequence()
+ cls._counter.reset(value)
+
+ @classmethod
def _setup_next_sequence(cls):
"""Set up an initial sequence value for Sequence attributes.
@@ -249,6 +307,19 @@ class BaseFactory(object):
return 0
@classmethod
+ def _setup_counter(cls):
+ """Ensures cls._counter is set for this class.
+
+ Due to the way inheritance works in Python, we need to ensure that the
+ ``_counter`` attribute has been initialized for *this* Factory subclass,
+ not one of its parents.
+ """
+ if cls._counter is None or cls._counter.for_class != cls:
+ first_seq = cls._setup_next_sequence()
+ cls._counter = _Counter(for_class=cls, seq=first_seq)
+ logger.debug("%r: Setting up next sequence (%d)", cls, first_seq)
+
+ @classmethod
def _generate_next_sequence(cls):
"""Retrieve a new sequence ID.
@@ -259,17 +330,15 @@ class BaseFactory(object):
"""
# Rely upon our parents
- if cls._base_factory:
+ if cls._base_factory and not cls._base_factory._abstract_factory:
+ logger.debug("%r: reusing sequence from %r", cls, cls._base_factory)
return cls._base_factory._generate_next_sequence()
- # Make sure _next_sequence is initialized
- if cls._next_sequence is None:
- cls._next_sequence = cls._setup_next_sequence()
+ # Make sure _counter is initialized
+ cls._setup_counter()
# Pick current value, then increase class counter for the next call.
- next_sequence = cls._next_sequence
- cls._next_sequence += 1
- return next_sequence
+ return cls._counter.next()
@classmethod
def attributes(cls, create=False, extra=None):
@@ -285,7 +354,13 @@ class BaseFactory(object):
force_sequence = None
if extra:
force_sequence = extra.pop('__sequence', None)
- return containers.AttributeBuilder(cls, extra).build(
+ log_ctx = '%s.%s' % (cls.__module__, cls.__name__)
+ logger.debug('BaseFactory: Preparing %s.%s(extra=%r)',
+ cls.__module__,
+ cls.__name__,
+ extra,
+ )
+ return containers.AttributeBuilder(cls, extra, log_ctx=log_ctx).build(
create=create,
force_sequence=force_sequence,
)
@@ -306,6 +381,21 @@ class BaseFactory(object):
return kwargs
@classmethod
+ def _load_target_class(cls, class_definition):
+ """Extension point for loading target classes.
+
+ This can be overridden in framework-specific subclasses to hook into
+ existing model repositories, for instance.
+ """
+ return class_definition
+
+ @classmethod
+ def _get_target_class(cls):
+ """Retrieve the actual, associated target class."""
+ definition = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
+ return cls._load_target_class(definition)
+
+ @classmethod
def _prepare(cls, create, **kwargs):
"""Prepare an object for this factory.
@@ -313,7 +403,7 @@ class BaseFactory(object):
create: bool, whether to create or to build the object
**kwargs: arguments to pass to the creation function
"""
- target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS)
+ target_class = cls._get_target_class()
kwargs = cls._adjust_kwargs(**kwargs)
# Remove 'hidden' arguments.
@@ -323,6 +413,11 @@ class BaseFactory(object):
# Extract *args from **kwargs
args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS)
+ logger.debug('BaseFactory: Generating %s.%s(%s)',
+ cls.__module__,
+ cls.__name__,
+ utils.log_pprint(args, kwargs),
+ )
if create:
return cls._create(target_class, *args, **kwargs)
else:
@@ -336,6 +431,12 @@ class BaseFactory(object):
create (bool): whether to 'build' or 'create' the object
attrs (dict): attributes to use for generating the object
"""
+ if cls._abstract_factory:
+ raise FactoryError(
+ "Cannot generate instances of abstract factory %(f)s; "
+ "Ensure %(f)s.FACTORY_FOR is set and %(f)s.ABSTRACT_FACTORY "
+ "is either not set or False." % dict(f=cls))
+
# Extract declarations used for post-generation
postgen_declarations = getattr(cls,
CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS)
@@ -349,9 +450,8 @@ class BaseFactory(object):
# Handle post-generation attributes
results = {}
for name, decl in sorted(postgen_declarations.items()):
- extracted, extracted_kwargs = postgen_attributes[name]
- results[name] = decl.call(obj, create, extracted,
- **extracted_kwargs)
+ extraction_context = postgen_attributes[name]
+ results[name] = decl.call(obj, create, extraction_context)
cls._after_postgeneration(obj, create, results)
@@ -533,7 +633,7 @@ Factory = FactoryMetaClass('Factory', (BaseFactory,), {
This class has the ability to support multiple ORMs by using custom creation
functions.
""",
- })
+})
# Backwards compatibility
@@ -554,83 +654,6 @@ class StubFactory(Factory):
raise UnsupportedStrategy()
-class DjangoModelFactory(Factory):
- """Factory for Django models.
-
- This makes sure that the 'sequence' field of created objects is a new id.
-
- Possible improvement: define a new 'attribute' type, AutoField, which would
- handle those for non-numerical primary keys.
- """
-
- ABSTRACT_FACTORY = True
- FACTORY_DJANGO_GET_OR_CREATE = ()
-
- @classmethod
- def _get_manager(cls, target_class):
- try:
- return target_class._default_manager # pylint: disable=W0212
- except AttributeError:
- return target_class.objects
-
- @classmethod
- def _setup_next_sequence(cls):
- """Compute the next available PK, based on the 'pk' database field."""
-
- model = cls._associated_class # pylint: disable=E1101
- manager = cls._get_manager(model)
-
- try:
- return 1 + manager.values_list('pk', flat=True
- ).order_by('-pk')[0]
- except IndexError:
- return 1
-
- @classmethod
- def _get_or_create(cls, target_class, *args, **kwargs):
- """Create an instance of the model through objects.get_or_create."""
- manager = cls._get_manager(target_class)
-
- assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, (
- "'defaults' is a reserved keyword for get_or_create "
- "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)"
- % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE))
-
- key_fields = {}
- for field in cls.FACTORY_DJANGO_GET_OR_CREATE:
- key_fields[field] = kwargs.pop(field)
- key_fields['defaults'] = kwargs
-
- obj, _created = manager.get_or_create(*args, **key_fields)
- return obj
-
- @classmethod
- def _create(cls, target_class, *args, **kwargs):
- """Create an instance of the model, and save it to the database."""
- manager = cls._get_manager(target_class)
-
- if cls.FACTORY_DJANGO_GET_OR_CREATE:
- return cls._get_or_create(target_class, *args, **kwargs)
-
- return manager.create(*args, **kwargs)
-
- @classmethod
- def _after_postgeneration(cls, obj, create, results=None):
- """Save again the instance if creating and at least one hook ran."""
- if create and results:
- # Some post-generation hooks ran, and may have modified us.
- obj.save()
-
-
-class MogoFactory(Factory):
- """Factory for mogo objects."""
- ABSTRACT_FACTORY = True
-
- @classmethod
- def _build(cls, target_class, *args, **kwargs):
- return target_class.new(*args, **kwargs)
-
-
class BaseDictFactory(Factory):
"""Factory for dictionary-like classes."""
ABSTRACT_FACTORY = True
diff --git a/factory/compat.py b/factory/compat.py
index 84f31b7..7747b1a 100644
--- a/factory/compat.py
+++ b/factory/compat.py
@@ -23,13 +23,56 @@
"""Compatibility tools"""
+import datetime
+import decimal
import sys
PY2 = (sys.version_info[0] == 2)
-if PY2:
+if PY2: # pragma: no cover
def is_string(obj):
return isinstance(obj, (str, unicode))
-else:
+
+ from StringIO import StringIO as BytesIO
+
+else: # pragma: no cover
def is_string(obj):
return isinstance(obj, str)
+
+ 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
+except AttributeError: # pragma: no cover
+ try:
+ # Fallback to pytz
+ from pytz import UTC
+ except ImportError:
+
+ # Ok, let's write our own.
+ class _UTC(datetime.tzinfo):
+ """The UTC tzinfo."""
+
+ def utcoffset(self, dt):
+ return datetime.timedelta(0)
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return datetime.timedelta(0)
+
+ def localize(self, dt):
+ dt.astimezone(self)
+
+ UTC = _UTC()
diff --git a/factory/containers.py b/factory/containers.py
index ee2ad82..4537e44 100644
--- a/factory/containers.py
+++ b/factory/containers.py
@@ -20,6 +20,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+import logging
+
+logger = logging.getLogger(__name__)
from . import declarations
from . import utils
@@ -49,16 +52,18 @@ class LazyStub(object):
__initialized = False
- def __init__(self, attrs, containers=(), target_class=object):
+ def __init__(self, attrs, containers=(), target_class=object, log_ctx=None):
self.__attrs = attrs
self.__values = {}
self.__pending = []
self.__containers = containers
self.__target_class = target_class
+ self.__log_ctx = log_ctx or '%s.%s' % (target_class.__module__, target_class.__name__)
+ self.factory_parent = containers[0] if containers else None
self.__initialized = True
def __repr__(self):
- return '<LazyStub for %s>' % self.__target_class.__name__
+ return '<LazyStub for %s.%s>' % (self.__target_class.__module__, self.__target_class.__name__)
def __str__(self):
return '<LazyStub for %s with %s>' % (
@@ -71,8 +76,14 @@ class LazyStub(object):
dict: map of attribute name => computed value
"""
res = {}
+ logger.debug("LazyStub: Computing values for %s(%s)",
+ self.__log_ctx, utils.log_pprint(kwargs=self.__attrs),
+ )
for attr in self.__attrs:
res[attr] = getattr(self, attr)
+ logger.debug("LazyStub: Computed values, got %s(%s)",
+ self.__log_ctx, utils.log_pprint(kwargs=res),
+ )
return res
def __getattr__(self, name):
@@ -92,7 +103,8 @@ class LazyStub(object):
if isinstance(val, LazyValue):
self.__pending.append(name)
val = val.evaluate(self, self.__containers)
- assert name == self.__pending.pop()
+ last = self.__pending.pop()
+ assert name == last
self.__values[name] = val
return val
else:
@@ -218,8 +230,8 @@ class AttributeBuilder(object):
overridden default values for the related SubFactory.
"""
- def __init__(self, factory, extra=None, *args, **kwargs):
- super(AttributeBuilder, self).__init__(*args, **kwargs)
+ def __init__(self, factory, extra=None, log_ctx=None, **kwargs):
+ super(AttributeBuilder, self).__init__(**kwargs)
if not extra:
extra = {}
@@ -227,13 +239,15 @@ class AttributeBuilder(object):
self.factory = factory
self._containers = extra.pop('__containers', ())
self._attrs = factory.declarations(extra)
+ self._log_ctx = log_ctx
+ initial_declarations = factory.declarations({})
attrs_with_subfields = [
- k for k, v in self._attrs.items()
+ k for k, v in initial_declarations.items()
if self.has_subfields(v)]
self._subfields = utils.multi_extract_dict(
- attrs_with_subfields, self._attrs)
+ attrs_with_subfields, self._attrs)
def has_subfields(self, value):
return isinstance(value, declarations.ParameteredAttribute)
@@ -258,14 +272,14 @@ class AttributeBuilder(object):
for k, v in self._attrs.items():
if isinstance(v, declarations.OrderedDeclaration):
v = OrderedDeclarationWrapper(v,
- sequence=sequence,
- create=create,
- extra=self._subfields.get(k, {}),
+ sequence=sequence,
+ create=create,
+ extra=self._subfields.get(k, {}),
)
wrapped_attrs[k] = v
stub = LazyStub(wrapped_attrs, containers=self._containers,
- target_class=self.factory)
+ target_class=self.factory, log_ctx=self._log_ctx)
return stub.__fill__()
diff --git a/factory/declarations.py b/factory/declarations.py
index 974b4ac..037a679 100644
--- a/factory/declarations.py
+++ b/factory/declarations.py
@@ -22,11 +22,16 @@
import itertools
+import warnings
+import logging
from . import compat
from . import utils
+logger = logging.getLogger('factory.generate')
+
+
class OrderedDeclaration(object):
"""A factory declaration.
@@ -66,6 +71,7 @@ class LazyAttribute(OrderedDeclaration):
self.function = function
def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ logger.debug("LazyAttribute: Evaluating %r on %r", self.function, obj)
return self.function(obj)
@@ -130,6 +136,8 @@ class SelfAttribute(OrderedDeclaration):
target = containers[self.depth - 2]
else:
target = obj
+
+ logger.debug("SelfAttribute: Picking attribute %r on %r", self.attribute_name, target)
return deepgetattr(target, self.attribute_name, self.default)
def __repr__(self):
@@ -155,16 +163,20 @@ class Iterator(OrderedDeclaration):
self.getter = getter
if cycle:
- self.iterator = itertools.cycle(iterator)
- else:
- self.iterator = iter(iterator)
+ iterator = itertools.cycle(iterator)
+ self.iterator = utils.ResetableIterator(iterator)
def evaluate(self, sequence, obj, create, extra=None, containers=()):
- value = next(self.iterator)
+ logger.debug("Iterator: Fetching next value from %r", self.iterator)
+ value = next(iter(self.iterator))
if self.getter is None:
return value
return self.getter(value)
+ def reset(self):
+ """Reset the internal iterator."""
+ self.iterator.reset()
+
class Sequence(OrderedDeclaration):
"""Specific OrderedDeclaration to use for 'sequenced' fields.
@@ -183,6 +195,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)
return self.function(self.type(sequence))
@@ -196,6 +209,8 @@ 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",
+ self.function, sequence, obj)
return self.function(obj, self.type(sequence))
@@ -300,6 +315,40 @@ class ParameteredAttribute(OrderedDeclaration):
raise NotImplementedError()
+class _FactoryWrapper(object):
+ """Handle a 'factory' arg.
+
+ Such args can be either a Factory subclass, or a fully qualified import
+ path for that subclass (e.g 'myapp.factories.MyFactory').
+ """
+ def __init__(self, factory_or_path):
+ self.factory = None
+ self.module = self.name = ''
+ if isinstance(factory_or_path, type):
+ self.factory = factory_or_path
+ else:
+ if not (compat.is_string(factory_or_path) and '.' in factory_or_path):
+ raise ValueError(
+ "A factory= argument must receive either a class "
+ "or the fully qualified path to a Factory subclass; got "
+ "%r instead." % factory_or_path)
+ self.module, self.name = factory_or_path.rsplit('.', 1)
+
+ def get(self):
+ if self.factory is None:
+ self.factory = utils.import_object(
+ self.module,
+ self.name,
+ )
+ return self.factory
+
+ def __repr__(self):
+ if self.factory is None:
+ return '<_FactoryImport: %s.%s>' % (self.module, self.name)
+ else:
+ return '<_FactoryImport: %s>' % self.factory.__class__
+
+
class SubFactory(ParameteredAttribute):
"""Base class for attributes based upon a sub-factory.
@@ -313,26 +362,11 @@ class SubFactory(ParameteredAttribute):
def __init__(self, factory, **kwargs):
super(SubFactory, self).__init__(**kwargs)
- if isinstance(factory, type):
- self.factory = factory
- self.factory_module = self.factory_name = ''
- else:
- # Must be a string
- if not (compat.is_string(factory) and '.' in factory):
- raise ValueError(
- "The argument of a SubFactory must be either a class "
- "or the fully qualified path to a Factory class; got "
- "%r instead." % factory)
- self.factory = None
- self.factory_module, self.factory_name = factory.rsplit('.', 1)
+ self.factory_wrapper = _FactoryWrapper(factory)
def get_factory(self):
"""Retrieve the wrapped factory.Factory subclass."""
- if self.factory is None:
- # Must be a module path
- self.factory = utils.import_object(
- self.factory_module, self.factory_name)
- return self.factory
+ return self.factory_wrapper.get()
def generate(self, sequence, obj, create, params):
"""Evaluate the current definition and fill its attributes.
@@ -344,6 +378,11 @@ class SubFactory(ParameteredAttribute):
override the wrapped factory's defaults
"""
subfactory = self.get_factory()
+ logger.debug("SubFactory: Instantiating %s.%s(%s), create=%r",
+ subfactory.__module__, subfactory.__name__,
+ utils.log_pprint(kwargs=params),
+ create,
+ )
return subfactory.simple_generate(create, **params)
@@ -355,6 +394,7 @@ class Dict(SubFactory):
def generate(self, sequence, obj, create, params):
dict_factory = self.get_factory()
+ logger.debug("Dict: Building dict(%s)", utils.log_pprint(kwargs=params))
return dict_factory.simple_generate(create,
__sequence=sequence,
**params)
@@ -369,11 +409,30 @@ class List(SubFactory):
def generate(self, sequence, obj, create, params):
list_factory = self.get_factory()
+ logger.debug('List: Building list(%s)',
+ utils.log_pprint(args=[v for _i, v in sorted(params.items())]),
+ )
return list_factory.simple_generate(create,
__sequence=sequence,
**params)
+class ExtractionContext(object):
+ """Private class holding all required context from extraction to postgen."""
+ def __init__(self, value=None, did_extract=False, extra=None, for_field=''):
+ self.value = value
+ self.did_extract = did_extract
+ self.extra = extra or {}
+ self.for_field = for_field
+
+ def __repr__(self):
+ return 'ExtractionContext(%r, %r, %r)' % (
+ self.value,
+ self.did_extract,
+ self.extra,
+ )
+
+
class PostGenerationDeclaration(object):
"""Declarations to be called once the target object has been generated."""
@@ -390,20 +449,24 @@ class PostGenerationDeclaration(object):
(object, dict): a tuple containing the attribute at 'name' (if
provided) and a dict of extracted attributes
"""
- extracted = attrs.pop(name, None)
+ try:
+ extracted = attrs.pop(name)
+ did_extract = True
+ except KeyError:
+ extracted = None
+ did_extract = False
+
kwargs = utils.extract_dict(name, attrs)
- return extracted, kwargs
+ return ExtractionContext(extracted, did_extract, kwargs, name)
- def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover
+ def call(self, obj, create, extraction_context): # pragma: no cover
"""Call this hook; no return value is expected.
Args:
obj (object): the newly generated object
create (bool): whether the object was 'built' or 'created'
- extracted (object): the value given for <name> in the
- object definition, or None if not provided.
- kwargs (dict): declarations extracted from the object
- definition for this hook
+ extraction_context: An ExtractionContext containing values
+ extracted from the containing factory's declaration
"""
raise NotImplementedError()
@@ -414,8 +477,17 @@ class PostGeneration(PostGenerationDeclaration):
super(PostGeneration, self).__init__()
self.function = function
- def call(self, obj, create, extracted=None, **kwargs):
- return self.function(obj, create, extracted, **kwargs)
+ def call(self, obj, create, extraction_context):
+ logger.debug('PostGeneration: Calling %s.%s(%s)',
+ self.function.__module__,
+ self.function.__name__,
+ utils.log_pprint(
+ (obj, create, extraction_context.value),
+ extraction_context.extra,
+ ),
+ )
+ return self.function(obj, create,
+ extraction_context.value, **extraction_context.extra)
class RelatedFactory(PostGenerationDeclaration):
@@ -428,40 +500,48 @@ class RelatedFactory(PostGenerationDeclaration):
calling the related factory
"""
- def __init__(self, factory, name='', **defaults):
+ def __init__(self, factory, factory_related_name='', **defaults):
super(RelatedFactory, self).__init__()
- self.name = name
+ 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
-
- if isinstance(factory, type):
- self.factory = factory
- self.factory_module = self.factory_name = ''
- else:
- # Must be a string
- if not (compat.is_string(factory) and '.' in factory):
- raise ValueError(
- "The argument of a SubFactory must be either a class "
- "or the fully qualified path to a Factory class; got "
- "%r instead." % factory)
- self.factory = None
- self.factory_module, self.factory_name = factory.rsplit('.', 1)
+ self.factory_wrapper = _FactoryWrapper(factory)
def get_factory(self):
"""Retrieve the wrapped factory.Factory subclass."""
- if self.factory is None:
- # Must be a module path
- self.factory = utils.import_object(
- self.factory_module, self.factory_name)
- return self.factory
+ return self.factory_wrapper.get()
+
+ def call(self, obj, create, extraction_context):
+ factory = self.get_factory()
+
+ if extraction_context.did_extract:
+ # The user passed in a custom value
+ logger.debug('RelatedFactory: Using provided %r instead of '
+ 'generating %s.%s.',
+ extraction_context.value,
+ factory.__module__, factory.__name__,
+ )
+ return extraction_context.value
- def call(self, obj, create, extracted=None, **kwargs):
passed_kwargs = dict(self.defaults)
- passed_kwargs.update(kwargs)
+ passed_kwargs.update(extraction_context.extra)
if self.name:
passed_kwargs[self.name] = obj
- factory = self.get_factory()
- factory.simple_generate(create, **passed_kwargs)
+ logger.debug('RelatedFactory: Generating %s.%s(%s)',
+ factory.__module__,
+ factory.__name__,
+ utils.log_pprint((create,), passed_kwargs),
+ )
+ return factory.simple_generate(create, **passed_kwargs)
class PostGenerationMethodCall(PostGenerationDeclaration):
@@ -483,17 +563,22 @@ class PostGenerationMethodCall(PostGenerationDeclaration):
self.method_args = args
self.method_kwargs = kwargs
- def call(self, obj, create, extracted=None, **kwargs):
- if extracted is None:
+ def call(self, obj, create, extraction_context):
+ if not extraction_context.did_extract:
passed_args = self.method_args
elif len(self.method_args) <= 1:
# Max one argument expected
- passed_args = (extracted,)
+ passed_args = (extraction_context.value,)
else:
- passed_args = tuple(extracted)
+ passed_args = tuple(extraction_context.value)
passed_kwargs = dict(self.method_kwargs)
- passed_kwargs.update(kwargs)
+ passed_kwargs.update(extraction_context.extra)
method = getattr(obj, self.method_name)
- method(*passed_args, **passed_kwargs)
+ logger.debug('PostGenerationMethodCall: Calling %r.%s(%s)',
+ obj,
+ self.method_name,
+ utils.log_pprint(passed_args, passed_kwargs),
+ )
+ return method(*passed_args, **passed_kwargs)
diff --git a/factory/django.py b/factory/django.py
new file mode 100644
index 0000000..fee8e52
--- /dev/null
+++ b/factory/django.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 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 __future__ import absolute_import
+from __future__ import unicode_literals
+
+import os
+
+"""factory_boy extensions for use with the Django framework."""
+
+try:
+ from django.core import files as django_files
+except ImportError as e: # pragma: no cover
+ django_files = None
+ import_failure = e
+
+
+from . import base
+from . import declarations
+from .compat import BytesIO, is_string
+
+
+def require_django():
+ """Simple helper to ensure Django is available."""
+ if django_files is None: # pragma: no cover
+ raise import_failure
+
+
+class DjangoModelFactory(base.Factory):
+ """Factory for Django models.
+
+ This makes sure that the 'sequence' field of created objects is a new id.
+
+ Possible improvement: define a new 'attribute' type, AutoField, which would
+ handle those for non-numerical primary keys.
+ """
+
+ ABSTRACT_FACTORY = True # Optional, but explicit.
+ FACTORY_DJANGO_GET_OR_CREATE = ()
+
+ @classmethod
+ def _load_target_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 definition
+
+ @classmethod
+ def _get_manager(cls, target_class):
+ try:
+ return target_class._default_manager # pylint: disable=W0212
+ except AttributeError:
+ return target_class.objects
+
+ @classmethod
+ def _setup_next_sequence(cls):
+ """Compute the next available PK, based on the 'pk' database field."""
+
+ model = cls._get_target_class() # pylint: disable=E1101
+ manager = cls._get_manager(model)
+
+ try:
+ return 1 + manager.values_list('pk', flat=True
+ ).order_by('-pk')[0]
+ except (IndexError, TypeError):
+ # IndexError: No instance exist yet
+ # TypeError: pk isn't an integer type
+ return 1
+
+ @classmethod
+ def _get_or_create(cls, target_class, *args, **kwargs):
+ """Create an instance of the model through objects.get_or_create."""
+ manager = cls._get_manager(target_class)
+
+ assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, (
+ "'defaults' is a reserved keyword for get_or_create "
+ "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)"
+ % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE))
+
+ key_fields = {}
+ for field in cls.FACTORY_DJANGO_GET_OR_CREATE:
+ key_fields[field] = kwargs.pop(field)
+ key_fields['defaults'] = kwargs
+
+ obj, _created = manager.get_or_create(*args, **key_fields)
+ return obj
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """Create an instance of the model, and save it to the database."""
+ manager = cls._get_manager(target_class)
+
+ if cls.FACTORY_DJANGO_GET_OR_CREATE:
+ return cls._get_or_create(target_class, *args, **kwargs)
+
+ return manager.create(*args, **kwargs)
+
+ @classmethod
+ def _after_postgeneration(cls, obj, create, results=None):
+ """Save again the instance if creating and at least one hook ran."""
+ if create and results:
+ # Some post-generation hooks ran, and may have modified us.
+ obj.save()
+
+
+class FileField(declarations.PostGenerationDeclaration):
+ """Helper to fill in django.db.models.FileField from a Factory."""
+
+ DEFAULT_FILENAME = 'example.dat'
+
+ def __init__(self, **defaults):
+ require_django()
+ self.defaults = defaults
+ super(FileField, self).__init__()
+
+ def _make_data(self, params):
+ """Create data for the field."""
+ return params.get('data', b'')
+
+ def _make_content(self, extraction_context):
+ path = ''
+ params = dict(self.defaults)
+ params.update(extraction_context.extra)
+
+ if params.get('from_path') and params.get('from_file'):
+ raise ValueError(
+ "At most one argument from 'from_file' and 'from_path' should "
+ "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'):
+ path = params['from_path']
+ f = open(path, 'rb')
+ content = django_files.File(f, name=path)
+
+ elif params.get('from_file'):
+ f = params['from_file']
+ content = django_files.File(f)
+ path = content.name
+
+ else:
+ data = self._make_data(params)
+ content = django_files.base.ContentFile(data)
+
+ if path:
+ default_filename = os.path.basename(path)
+ else:
+ default_filename = self.DEFAULT_FILENAME
+
+ filename = params.get('filename', default_filename)
+ return filename, content
+
+ def call(self, obj, create, extraction_context):
+ """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
+
+
+class ImageField(FileField):
+ DEFAULT_FILENAME = 'example.jpg'
+
+ def _make_data(self, params):
+ # ImageField (both django's and factory_boy's) require PIL.
+ # Try to import it along one of its known installation paths.
+ try:
+ from PIL import Image
+ except ImportError:
+ import Image
+
+ width = params.get('width', 100)
+ height = params.get('height', width)
+ color = params.get('color', 'blue')
+ image_format = params.get('format', 'JPEG')
+
+ thumb = Image.new('RGB', (width, height), color)
+ thumb_io = BytesIO()
+ thumb.save(thumb_io, format=image_format)
+ return thumb_io.getvalue()
+
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index 186b4a7..34949c5 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -23,9 +23,14 @@
"""Additional declarations for "fuzzy" attribute definitions."""
+from __future__ import unicode_literals
+import decimal
import random
+import string
+import datetime
+from . import compat
from . import declarations
@@ -35,7 +40,7 @@ class BaseFuzzyAttribute(declarations.OrderedDeclaration):
Custom fuzzers should override the `fuzz()` method.
"""
- def fuzz(self):
+ def fuzz(self): # pragma: no cover
raise NotImplementedError()
def evaluate(self, sequence, obj, create, extra=None, containers=()):
@@ -58,6 +63,36 @@ class FuzzyAttribute(BaseFuzzyAttribute):
return self.fuzzer()
+class FuzzyText(BaseFuzzyAttribute):
+ """Random string with a given prefix.
+
+ Generates a random string of the given length from chosen chars.
+ If a prefix or a suffix are supplied, they will be prepended / appended
+ to the generated string.
+
+ Args:
+ prefix (text): An optional prefix to prepend to the random string
+ length (int): the length of the random part
+ suffix (text): An optional suffix to append to the random string
+ chars (str list): the chars to choose from
+
+ Useful for generating unique attributes where the exact value is
+ not important.
+ """
+
+ def __init__(self, prefix='', length=12, suffix='',
+ chars=string.ascii_letters, **kwargs):
+ super(FuzzyText, self).__init__(**kwargs)
+ self.prefix = prefix
+ self.suffix = suffix
+ self.length = length
+ self.chars = tuple(chars) # Unroll iterators
+
+ def fuzz(self):
+ 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."""
@@ -84,3 +119,143 @@ class FuzzyInteger(BaseFuzzyAttribute):
def fuzz(self):
return random.randint(self.low, self.high)
+
+
+class FuzzyDecimal(BaseFuzzyAttribute):
+ """Random decimal within a given range."""
+
+ def __init__(self, low, high=None, precision=2, **kwargs):
+ if high is None:
+ high = low
+ low = 0.0
+
+ self.low = low
+ self.high = high
+ self.precision = precision
+
+ super(FuzzyDecimal, self).__init__(**kwargs)
+
+ def fuzz(self):
+ base = compat.float_to_decimal(random.uniform(self.low, self.high))
+ return base.quantize(decimal.Decimal(10) ** -self.precision)
+
+
+class FuzzyDate(BaseFuzzyAttribute):
+ """Random date within a given date range."""
+
+ def __init__(self, start_date, end_date=None, **kwargs):
+ super(FuzzyDate, self).__init__(**kwargs)
+ if end_date is None:
+ end_date = datetime.date.today()
+
+ if start_date > end_date:
+ raise ValueError(
+ "FuzzyDate boundaries should have start <= end; got %r > %r."
+ % (start_date, end_date))
+
+ self.start_date = start_date.toordinal()
+ self.end_date = end_date.toordinal()
+
+ def fuzz(self):
+ return datetime.date.fromordinal(random.randint(self.start_date, self.end_date))
+
+
+class BaseFuzzyDateTime(BaseFuzzyAttribute):
+ """Base class for fuzzy datetime-related attributes.
+
+ Provides fuzz() computation, forcing year/month/day/hour/...
+ """
+
+ def _check_bounds(self, start_dt, end_dt):
+ if start_dt > end_dt:
+ raise ValueError(
+ """%s boundaries should have start <= end, got %r > %r""" % (
+ self.__class__.__name__, start_dt, end_dt))
+
+ def __init__(self, start_dt, end_dt=None,
+ force_year=None, force_month=None, force_day=None,
+ force_hour=None, force_minute=None, force_second=None,
+ force_microsecond=None, **kwargs):
+ super(BaseFuzzyDateTime, self).__init__(**kwargs)
+
+ if end_dt is None:
+ end_dt = self._now()
+
+ self._check_bounds(start_dt, end_dt)
+
+ self.start_dt = start_dt
+ self.end_dt = end_dt
+ self.force_year = force_year
+ self.force_month = force_month
+ self.force_day = force_day
+ self.force_hour = force_hour
+ self.force_minute = force_minute
+ self.force_second = force_second
+ self.force_microsecond = force_microsecond
+
+ def fuzz(self):
+ delta = self.end_dt - self.start_dt
+ microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))
+
+ offset = random.randint(0, microseconds)
+ result = self.start_dt + datetime.timedelta(microseconds=offset)
+
+ if self.force_year is not None:
+ result = result.replace(year=self.force_year)
+ if self.force_month is not None:
+ result = result.replace(month=self.force_month)
+ if self.force_day is not None:
+ result = result.replace(day=self.force_day)
+ if self.force_hour is not None:
+ result = result.replace(hour=self.force_hour)
+ if self.force_minute is not None:
+ result = result.replace(minute=self.force_minute)
+ if self.force_second is not None:
+ result = result.replace(second=self.force_second)
+ if self.force_microsecond is not None:
+ result = result.replace(microsecond=self.force_microsecond)
+
+ return result
+
+
+class FuzzyNaiveDateTime(BaseFuzzyDateTime):
+ """Random naive datetime within a given range.
+
+ If no upper bound is given, will default to datetime.datetime.utcnow().
+ """
+
+ def _now(self):
+ return datetime.datetime.now()
+
+ def _check_bounds(self, start_dt, end_dt):
+ if start_dt.tzinfo is not None:
+ raise ValueError(
+ "FuzzyNaiveDateTime only handles naive datetimes, got start=%r"
+ % start_dt)
+ if end_dt.tzinfo is not None:
+ raise ValueError(
+ "FuzzyNaiveDateTime only handles naive datetimes, got end=%r"
+ % end_dt)
+ super(FuzzyNaiveDateTime, self)._check_bounds(start_dt, end_dt)
+
+
+class FuzzyDateTime(BaseFuzzyDateTime):
+ """Random timezone-aware datetime within a given range.
+
+ If no upper bound is given, will default to datetime.datetime.now()
+ If no timezone is given, will default to utc.
+ """
+
+ def _now(self):
+ return datetime.datetime.now(tz=compat.UTC)
+
+ def _check_bounds(self, start_dt, end_dt):
+ if start_dt.tzinfo is None:
+ raise ValueError(
+ "FuzzyDateTime only handles aware datetimes, got start=%r"
+ % start_dt)
+ if end_dt.tzinfo is None:
+ raise ValueError(
+ "FuzzyDateTime only handles 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 8f0d161..37b41bf 100644
--- a/factory/helpers.py
+++ b/factory/helpers.py
@@ -23,11 +23,29 @@
"""Simple wrappers around Factory class definition."""
+import contextlib
+import logging
from . import base
from . import declarations
+@contextlib.contextmanager
+def debug(logger='factory', stream=None):
+ logger_obj = logging.getLogger(logger)
+ old_level = logger_obj.level
+
+ handler = logging.StreamHandler(stream)
+ handler.setLevel(logging.DEBUG)
+ logger_obj.addHandler(handler)
+ logger_obj.setLevel(logging.DEBUG)
+
+ yield
+
+ logger_obj.setLevel(old_level)
+ logger_obj.removeHandler(handler)
+
+
def make_factory(klass, **kwargs):
"""Create a new, simple factory for the given class."""
factory_name = '%sFactory' % klass.__name__
diff --git a/factory/mogo.py b/factory/mogo.py
new file mode 100644
index 0000000..48d9677
--- /dev/null
+++ b/factory/mogo.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 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 __future__ import unicode_literals
+
+
+"""factory_boy extensions for use with the mogo library (pymongo wrapper)."""
+
+
+from . import base
+
+
+class MogoFactory(base.Factory):
+ """Factory for mogo objects."""
+ ABSTRACT_FACTORY = True
+
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ return target_class.new(*args, **kwargs)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ instance = target_class.new(*args, **kwargs)
+ instance.save()
+ return instance
diff --git a/factory/mongoengine.py b/factory/mongoengine.py
new file mode 100644
index 0000000..462f5f2
--- /dev/null
+++ b/factory/mongoengine.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 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 __future__ import unicode_literals
+
+
+"""factory_boy extensions for use with the mongoengine library (pymongo wrapper)."""
+
+
+from . import base
+
+
+class MongoEngineFactory(base.Factory):
+ """Factory for mongoengine objects."""
+ ABSTRACT_FACTORY = True
+
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ return target_class(*args, **kwargs)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ instance = target_class(*args, **kwargs)
+ if instance._is_document:
+ instance.save()
+ return instance
diff --git a/factory/utils.py b/factory/utils.py
index fb8cfef..276977a 100644
--- a/factory/utils.py
+++ b/factory/utils.py
@@ -20,6 +20,9 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+from __future__ import unicode_literals
+
+import collections
#: String for splitting an attribute name into a
#: (subfactory_name, subfactory_field) tuple.
@@ -94,3 +97,47 @@ def import_object(module_name, attribute_name):
module = __import__(module_name, {}, {}, [attribute_name], 0)
return getattr(module, attribute_name)
+
+def _safe_repr(obj):
+ try:
+ obj_repr = repr(obj)
+ except UnicodeError:
+ return '<bad_repr object at %s>' % id(obj)
+
+ try: # Convert to "text type" (= unicode)
+ return '%s' % obj_repr
+ except UnicodeError: # non-ascii bytes repr on Py2
+ 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 ResetableIterator(object):
+ """An iterator wrapper that can be 'reset()' to its start."""
+ def __init__(self, iterator, **kwargs):
+ super(ResetableIterator, self).__init__(**kwargs)
+ self.iterator = iter(iterator)
+ self.past_elements = collections.deque()
+ self.next_elements = collections.deque()
+
+ def __iter__(self):
+ while True:
+ if self.next_elements:
+ yield self.next_elements.popleft()
+ else:
+ value = next(self.iterator)
+ self.past_elements.append(value)
+ yield value
+
+ def reset(self):
+ self.next_elements.clear()
+ self.next_elements.extend(self.past_elements)
diff --git a/setup.py b/setup.py
index 050e43b..54e4caa 100755
--- a/setup.py
+++ b/setup.py
@@ -1,73 +1,40 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
+import codecs
import os
import re
import sys
-from distutils.core import setup
-from distutils import cmd
-root = os.path.abspath(os.path.dirname(__file__))
+from setuptools import setup
-def get_version(*module_dir_components):
- version_re = re.compile(r"^__version__ = ['\"](.*)['\"]$")
- module_root = os.path.join(root, *module_dir_components)
- module_init = os.path.join(module_root, '__init__.py')
- with open(module_init, 'r') as f:
+root_dir = os.path.abspath(os.path.dirname(__file__))
+
+
+def get_version(package_name):
+ version_re = re.compile(r"^__version__ = [\"']([\w_.-]+)[\"']$")
+ package_components = package_name.split('.')
+ init_path = os.path.join(root_dir, *(package_components + ['__init__.py']))
+ with codecs.open(init_path, 'r', 'utf-8') as f:
for line in f:
match = version_re.match(line[:-1])
if match:
return match.groups()[0]
return '0.1.0'
-VERSION = get_version('factory')
-
-
-class test(cmd.Command):
- """Run the tests for this package."""
- command_name = 'test'
- description = 'run the tests associated with the package'
-
- user_options = [
- ('test-suite=', None, "A test suite to run (defaults to 'tests')"),
- ]
-
- def initialize_options(self):
- self.test_runner = None
- self.test_suite = None
- def finalize_options(self):
- self.ensure_string('test_suite', 'tests')
+if sys.version_info[0:2] < (2, 7): # pragma: no cover
+ test_loader = 'unittest2:TestLoader'
+else:
+ test_loader = 'unittest:TestLoader'
- def run(self):
- """Run the test suite."""
- try:
- import unittest2 as unittest
- except ImportError:
- import unittest
- if self.verbose:
- verbosity=1
- else:
- verbosity=0
-
- loader = unittest.TestLoader()
- suite = unittest.TestSuite()
-
- if self.test_suite == 'tests':
- for test_module in loader.discover('.'):
- suite.addTest(test_module)
- else:
- suite.addTest(loader.loadTestsFromName(self.test_suite))
-
- result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
- if not result.wasSuccessful():
- sys.exit(1)
+PACKAGE = 'factory'
setup(
name='factory_boy',
- version=VERSION,
+ version=get_version(PACKAGE),
description="A verstile test fixtures replacement based on thoughtbot's factory_girl for Ruby.",
author='Mark Sandstrom',
author_email='mark@deliciouslynerdy.com',
@@ -77,22 +44,29 @@ setup(
keywords=['factory_boy', 'factory', 'fixtures'],
packages=['factory'],
license='MIT',
+ setup_requires=[
+ 'setuptools>=0.8',
+ ],
+ tests_require=[
+ 'mock',
+ ],
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'Framework :: Django',
- 'License :: OSI Approved :: MIT License',
- 'Operating System :: OS Independent',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.2',
- 'Programming Language :: Python :: 3.3',
- 'Programming Language :: Python :: Implementation :: PyPy',
- 'Topic :: Software Development :: Testing',
- 'Topic :: Software Development :: Libraries :: Python Modules'
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Framework :: Django",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.2",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Software Development :: Testing",
+ "Topic :: Software Development :: Libraries :: Python Modules"
],
- cmdclass={'test': test},
+ test_suite='tests',
+ test_loader=test_loader,
)
diff --git a/tests/__init__.py b/tests/__init__.py
index 3c620d6..5b6fc55 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -4,6 +4,10 @@
from .test_base import *
from .test_containers import *
from .test_declarations import *
+from .test_django import *
from .test_fuzzy import *
+from .test_helpers import *
from .test_using import *
from .test_utils import *
+from .test_alchemy import *
+from .test_mongoengine import *
diff --git a/tests/alchemyapp/__init__.py b/tests/alchemyapp/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/alchemyapp/__init__.py
diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py
new file mode 100644
index 0000000..e0193d4
--- /dev/null
+++ b/tests/alchemyapp/models.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 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
+# 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.
+
+
+"""Helpers for testing SQLAlchemy apps."""
+
+from sqlalchemy import Column, Integer, Unicode, create_engine
+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)
+Base = declarative_base()
+
+
+class StandardModel(Base):
+ __tablename__ = 'StandardModelTable'
+
+ id = Column(Integer(), primary_key=True)
+ foo = Column(Unicode(20))
+
+
+class NonIntegerPk(Base):
+ __tablename__ = 'NonIntegerPk'
+
+ id = Column(Unicode(20), primary_key=True)
+
+Base.metadata.create_all(engine)
diff --git a/tests/alter_time.py b/tests/alter_time.py
new file mode 100644
index 0000000..db0a611
--- /dev/null
+++ b/tests/alter_time.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# This code is in the public domain
+# Author: Raphaël Barrois
+
+
+from __future__ import print_function
+
+import datetime
+import mock
+
+
+real_datetime_class = datetime.datetime
+
+def mock_datetime_now(target, datetime_module):
+ """Override ``datetime.datetime.now()`` with a custom target value.
+
+ This creates a new datetime.datetime class, and alters its now()/utcnow()
+ methods.
+
+ Returns:
+ A mock.patch context, can be used as a decorator or in a with.
+ """
+
+ # See http://bugs.python.org/msg68532
+ # And http://docs.python.org/reference/datamodel.html#customizing-instance-and-subclass-checks
+ class DatetimeSubclassMeta(type):
+ """We need to customize the __instancecheck__ method for isinstance().
+
+ This must be performed at a metaclass level.
+ """
+
+ @classmethod
+ def __instancecheck__(mcs, obj):
+ return isinstance(obj, real_datetime_class)
+
+ class BaseMockedDatetime(real_datetime_class):
+ @classmethod
+ def now(cls, tz=None):
+ return target.replace(tzinfo=tz)
+
+ @classmethod
+ def utcnow(cls):
+ return target
+
+ # Python2 & Python3-compatible metaclass
+ MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {})
+
+ return mock.patch.object(datetime_module, 'datetime', MockedDatetime)
+
+real_date_class = datetime.date
+
+def mock_date_today(target, datetime_module):
+ """Override ``datetime.date.today()`` with a custom target value.
+
+ This creates a new datetime.date class, and alters its today() method.
+
+ Returns:
+ A mock.patch context, can be used as a decorator or in a with.
+ """
+
+ # See http://bugs.python.org/msg68532
+ # And http://docs.python.org/reference/datamodel.html#customizing-instance-and-subclass-checks
+ class DateSubclassMeta(type):
+ """We need to customize the __instancecheck__ method for isinstance().
+
+ This must be performed at a metaclass level.
+ """
+
+ @classmethod
+ def __instancecheck__(mcs, obj):
+ return isinstance(obj, real_date_class)
+
+ class BaseMockedDate(real_date_class):
+ @classmethod
+ def today(cls):
+ return target
+
+ # Python2 & Python3-compatible metaclass
+ MockedDate = DateSubclassMeta('date', (BaseMockedDate,), {})
+
+ return mock.patch.object(datetime_module, 'date', MockedDate)
+
+
+def main(): # pragma: no cover
+ """Run a couple of tests"""
+ target_dt = real_datetime_class(2009, 1, 1)
+ target_date = real_date_class(2009, 1, 1)
+
+ print("Entering mock")
+ with mock_datetime_now(target_dt, datetime):
+ print("- now ->", datetime.datetime.now())
+ print("- isinstance(now, dt) ->", isinstance(datetime.datetime.now(), datetime.datetime))
+ print("- isinstance(target, dt) ->", isinstance(target_dt, datetime.datetime))
+
+ with mock_date_today(target_date, datetime):
+ print("- today ->", datetime.date.today())
+ print("- isinstance(now, date) ->", isinstance(datetime.date.today(), datetime.date))
+ print("- isinstance(target, date) ->", isinstance(target_date, datetime.date))
+
+
+ print("Outside mock")
+ print("- now ->", datetime.datetime.now())
+ print("- isinstance(now, dt) ->", isinstance(datetime.datetime.now(), datetime.datetime))
+ print("- isinstance(target, dt) ->", isinstance(target_dt, datetime.datetime))
+
+ print("- today ->", datetime.date.today())
+ print("- isinstance(now, date) ->", isinstance(datetime.date.today(), datetime.date))
+ print("- isinstance(target, date) ->", isinstance(target_date, datetime.date))
+
+
+if __name__ == '__main__': # pragma: no cover
+ main()
diff --git a/tests/compat.py b/tests/compat.py
index 6a1eb80..ff96f13 100644
--- a/tests/compat.py
+++ b/tests/compat.py
@@ -25,13 +25,18 @@ import sys
is_python2 = (sys.version_info[0] == 2)
-try:
+if sys.version_info[0:2] < (2, 7): # pragma: no cover
import unittest2 as unittest
-except ImportError:
+else: # pragma: no cover
import unittest
-if sys.version_info[0:2] < (3, 3):
+if sys.version_info[0] == 2: # pragma: no cover
+ import StringIO as io
+else: # pragma: no cover
+ import io
+
+if sys.version_info[0:2] < (3, 3): # pragma: no cover
import mock
-else:
+else: # pragma: no cover
from unittest import mock
diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py
index 7f00f12..e584ed1 100644
--- a/tests/cyclic/foo.py
+++ b/tests/cyclic/foo.py
@@ -23,7 +23,7 @@
import factory
-from cyclic import bar
+from . import bar as bar_mod
class Foo(object):
def __init__(self, bar, x):
@@ -35,4 +35,4 @@ class FooFactory(factory.Factory):
FACTORY_FOR = Foo
x = 42
- bar = factory.SubFactory(bar.BarFactory)
+ bar = factory.SubFactory(bar_mod.BarFactory)
diff --git a/tests/djapp/__init__.py b/tests/djapp/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/djapp/__init__.py
diff --git a/tests/djapp/models.py b/tests/djapp/models.py
new file mode 100644
index 0000000..e98279d
--- /dev/null
+++ b/tests/djapp/models.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011-2013 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.
+
+
+"""Helpers for testing django apps."""
+
+import os.path
+
+try:
+ from PIL import Image
+except ImportError:
+ try:
+ import Image
+ except ImportError:
+ Image = None
+
+from django.conf import settings
+from django.db import models
+
+class StandardModel(models.Model):
+ foo = models.CharField(max_length=20)
+
+
+class NonIntegerPk(models.Model):
+ foo = models.CharField(max_length=20, primary_key=True)
+ bar = models.CharField(max_length=20, blank=True)
+
+
+class AbstractBase(models.Model):
+ foo = models.CharField(max_length=20)
+
+ class Meta:
+ abstract = True
+
+
+class ConcreteSon(AbstractBase):
+ pass
+
+
+class StandardSon(StandardModel):
+ pass
+
+
+WITHFILE_UPLOAD_TO = 'django'
+WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO)
+
+class WithFile(models.Model):
+ afile = models.FileField(upload_to=WITHFILE_UPLOAD_TO)
+
+
+if Image is not None: # PIL is available
+
+ class WithImage(models.Model):
+ animage = models.ImageField(upload_to=WITHFILE_UPLOAD_TO)
+
+else:
+ class WithImage(models.Model):
+ pass
diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py
new file mode 100644
index 0000000..c1b79b0
--- /dev/null
+++ b/tests/djapp/settings.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011-2013 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.
+"""Settings for factory_boy/Django tests."""
+
+import os
+
+FACTORY_ROOT = os.path.join(
+ os.path.abspath(os.path.dirname(__file__)), # /path/to/fboy/tests/djapp/
+ os.pardir, # /path/to/fboy/tests/
+ os.pardir, # /path/to/fboy
+)
+
+MEDIA_ROOT = os.path.join(FACTORY_ROOT, 'tmp_test')
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ },
+}
+
+
+INSTALLED_APPS = [
+ 'tests.djapp'
+]
+
+
+SECRET_KEY = 'testing.'
diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py
new file mode 100644
index 0000000..4255417
--- /dev/null
+++ b/tests/test_alchemy.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 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
+# 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.
+
+"""Tests for factory_boy/SQLAlchemy interactions."""
+
+import factory
+from .compat import unittest
+
+
+try:
+ import sqlalchemy
+except ImportError:
+ sqlalchemy = None
+
+if sqlalchemy:
+ from factory.alchemy import SQLAlchemyModelFactory
+ from .alchemyapp import models
+else:
+
+ class Fake(object):
+ FACTORY_SESSION = None
+
+ models = Fake()
+ models.StandardModel = Fake()
+ models.NonIntegerPk = Fake()
+ models.session = Fake()
+ SQLAlchemyModelFactory = Fake
+
+
+class StandardFactory(SQLAlchemyModelFactory):
+ FACTORY_FOR = models.StandardModel
+ FACTORY_SESSION = models.session
+
+ id = factory.Sequence(lambda n: n)
+ foo = factory.Sequence(lambda n: 'foo%d' % n)
+
+
+class NonIntegerPkFactory(SQLAlchemyModelFactory):
+ FACTORY_FOR = models.NonIntegerPk
+ FACTORY_SESSION = models.session
+
+ id = factory.Sequence(lambda n: 'foo%d' % n)
+
+
+@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.")
+class SQLAlchemyPkSequenceTestCase(unittest.TestCase):
+
+ def setUp(self):
+ super(SQLAlchemyPkSequenceTestCase, self).setUp()
+ StandardFactory.reset_sequence(1)
+ NonIntegerPkFactory.FACTORY_SESSION.rollback()
+
+ def test_pk_first(self):
+ std = StandardFactory.build()
+ self.assertEqual('foo1', std.foo)
+
+ def test_pk_many(self):
+ std1 = StandardFactory.build()
+ std2 = StandardFactory.build()
+ self.assertEqual('foo1', std1.foo)
+ self.assertEqual('foo2', std2.foo)
+
+ def test_pk_creation(self):
+ std1 = StandardFactory.create()
+ self.assertEqual('foo1', std1.foo)
+ self.assertEqual(1, std1.id)
+
+ StandardFactory.reset_sequence()
+ std2 = StandardFactory.create()
+ self.assertEqual('foo2', std2.foo)
+ self.assertEqual(2, 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(10, std1.id)
+
+ StandardFactory.reset_sequence()
+ std2 = StandardFactory.create()
+ self.assertEqual('foo11', std2.foo)
+ self.assertEqual(11, std2.id)
+
+
+@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.")
+class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase):
+ def setUp(self):
+ super(SQLAlchemyNonIntegerPkTestCase, self).setUp()
+ NonIntegerPkFactory.reset_sequence()
+ NonIntegerPkFactory.FACTORY_SESSION.rollback()
+
+ def test_first(self):
+ nonint = NonIntegerPkFactory.build()
+ self.assertEqual('foo1', nonint.id)
+
+ def test_many(self):
+ nonint1 = NonIntegerPkFactory.build()
+ nonint2 = NonIntegerPkFactory.build()
+
+ self.assertEqual('foo1', nonint1.id)
+ self.assertEqual('foo2', nonint2.id)
+
+ def test_creation(self):
+ nonint1 = NonIntegerPkFactory.create()
+ self.assertEqual('foo1', nonint1.id)
+
+ NonIntegerPkFactory.reset_sequence()
+ nonint2 = NonIntegerPkFactory.build()
+ self.assertEqual('foo1', nonint2.id)
+
+ def test_force_pk(self):
+ nonint1 = NonIntegerPkFactory.create(id='foo10')
+ self.assertEqual('foo10', nonint1.id)
+
+ NonIntegerPkFactory.reset_sequence()
+ nonint2 = NonIntegerPkFactory.create()
+ self.assertEqual('foo1', nonint2.id)
diff --git a/tests/test_base.py b/tests/test_base.py
index 73e59fa..8cea6fc 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -34,6 +34,7 @@ class TestObject(object):
self.three = three
self.four = four
+
class FakeDjangoModel(object):
@classmethod
def create(cls, **kwargs):
@@ -46,6 +47,7 @@ class FakeDjangoModel(object):
setattr(self, name, value)
self.id = None
+
class FakeModelFactory(base.Factory):
ABSTRACT_FACTORY = True
@@ -71,6 +73,20 @@ class AbstractFactoryTestCase(unittest.TestCase):
# Passed
+ def test_factory_for_and_abstract_factory_optional(self):
+ """Ensure that ABSTRACT_FACTORY is optional."""
+ class TestObjectFactory(base.Factory):
+ pass
+
+ # passed
+
+ def test_abstract_factory_cannot_be_called(self):
+ class TestObjectFactory(base.Factory):
+ pass
+
+ self.assertRaises(base.FactoryError, TestObjectFactory.build)
+ self.assertRaises(base.FactoryError, TestObjectFactory.create)
+
class FactoryTestCase(unittest.TestCase):
def test_factory_for(self):
@@ -116,6 +132,84 @@ class FactoryTestCase(unittest.TestCase):
self.assertEqual(4, len(ones))
+class FactorySequenceTestCase(unittest.TestCase):
+ def setUp(self):
+ super(FactorySequenceTestCase, self).setUp()
+
+ class TestObjectFactory(base.Factory):
+ FACTORY_FOR = TestObject
+ one = declarations.Sequence(lambda n: n)
+
+ self.TestObjectFactory = TestObjectFactory
+
+ def test_reset_sequence(self):
+ o1 = self.TestObjectFactory()
+ self.assertEqual(0, o1.one)
+
+ o2 = self.TestObjectFactory()
+ self.assertEqual(1, o2.one)
+
+ self.TestObjectFactory.reset_sequence()
+ o3 = self.TestObjectFactory()
+ self.assertEqual(0, o3.one)
+
+ def test_reset_sequence_with_value(self):
+ o1 = self.TestObjectFactory()
+ self.assertEqual(0, o1.one)
+
+ o2 = self.TestObjectFactory()
+ self.assertEqual(1, o2.one)
+
+ self.TestObjectFactory.reset_sequence(42)
+ o3 = self.TestObjectFactory()
+ self.assertEqual(42, o3.one)
+
+ def test_reset_sequence_subclass_fails(self):
+ """Tests that the sequence of a 'slave' factory cannot be reseted."""
+ class SubTestObjectFactory(self.TestObjectFactory):
+ pass
+
+ self.assertRaises(ValueError, SubTestObjectFactory.reset_sequence)
+
+ def test_reset_sequence_subclass_force(self):
+ """Tests that reset_sequence(force=True) works."""
+ class SubTestObjectFactory(self.TestObjectFactory):
+ pass
+
+ o1 = SubTestObjectFactory()
+ self.assertEqual(0, o1.one)
+
+ o2 = SubTestObjectFactory()
+ self.assertEqual(1, o2.one)
+
+ SubTestObjectFactory.reset_sequence(force=True)
+ o3 = SubTestObjectFactory()
+ self.assertEqual(0, o3.one)
+
+ # The master sequence counter has been reset
+ o4 = self.TestObjectFactory()
+ self.assertEqual(1, o4.one)
+
+ def test_reset_sequence_subclass_parent(self):
+ """Tests that the sequence of a 'slave' factory cannot be reseted."""
+ class SubTestObjectFactory(self.TestObjectFactory):
+ pass
+
+ o1 = SubTestObjectFactory()
+ self.assertEqual(0, o1.one)
+
+ o2 = SubTestObjectFactory()
+ self.assertEqual(1, o2.one)
+
+ self.TestObjectFactory.reset_sequence()
+ o3 = SubTestObjectFactory()
+ self.assertEqual(0, o3.one)
+
+ o4 = self.TestObjectFactory()
+ self.assertEqual(1, o4.one)
+
+
+
class FactoryDefaultStrategyTestCase(unittest.TestCase):
def setUp(self):
self.default_strategy = base.Factory.FACTORY_STRATEGY
@@ -238,12 +332,10 @@ class FactoryCreationTestCase(unittest.TestCase):
# Errors
def test_no_associated_class(self):
- try:
- class Test(base.Factory):
- pass
- self.fail()
- except base.Factory.AssociatedClassError as e:
- self.assertTrue('autodiscovery' not in str(e))
+ class Test(base.Factory):
+ pass
+
+ self.assertTrue(Test._abstract_factory)
class PostGenerationParsingTestCase(unittest.TestCase):
@@ -268,5 +360,5 @@ class PostGenerationParsingTestCase(unittest.TestCase):
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
unittest.main()
diff --git a/tests/test_containers.py b/tests/test_containers.py
index 70ed885..8b78dc7 100644
--- a/tests/test_containers.py
+++ b/tests/test_containers.py
@@ -68,6 +68,16 @@ class LazyStubTestCase(unittest.TestCase):
containers=())
self.assertEqual(2, stub.one)
+ def test_access_parent(self):
+ """Test simple access to a stub' parent."""
+ o1 = containers.LazyStub({'rank': 1})
+ o2 = containers.LazyStub({'rank': 2}, (o1,))
+ stub = containers.LazyStub({'rank': 3}, (o2, o1))
+
+ self.assertEqual(3, stub.rank)
+ self.assertEqual(2, stub.factory_parent.rank)
+ self.assertEqual(1, stub.factory_parent.factory_parent.rank)
+
def test_cyclic_definition(self):
class LazyAttr(containers.LazyValue):
def __init__(self, attrname):
@@ -349,5 +359,5 @@ class AttributeBuilderTestCase(unittest.TestCase):
pass
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
unittest.main()
diff --git a/tests/test_declarations.py b/tests/test_declarations.py
index 4c08dfa..86bc8b5 100644
--- a/tests/test_declarations.py
+++ b/tests/test_declarations.py
@@ -107,6 +107,27 @@ class IteratorTestCase(unittest.TestCase):
self.assertEqual(2, it.evaluate(1, None, False))
self.assertRaises(StopIteration, it.evaluate, 2, None, False)
+ def test_reset_cycle(self):
+ it = declarations.Iterator([1, 2])
+ self.assertEqual(1, it.evaluate(0, None, False))
+ self.assertEqual(2, it.evaluate(1, None, False))
+ self.assertEqual(1, it.evaluate(2, None, False))
+ self.assertEqual(2, it.evaluate(3, None, False))
+ self.assertEqual(1, it.evaluate(4, None, False))
+ it.reset()
+ self.assertEqual(1, it.evaluate(5, None, False))
+ self.assertEqual(2, it.evaluate(6, None, False))
+
+ def test_reset_no_cycling(self):
+ it = declarations.Iterator([1, 2], cycle=False)
+ self.assertEqual(1, it.evaluate(0, None, False))
+ self.assertEqual(2, it.evaluate(1, None, False))
+ self.assertRaises(StopIteration, it.evaluate, 2, None, False)
+ it.reset()
+ self.assertEqual(1, it.evaluate(0, None, False))
+ self.assertEqual(2, it.evaluate(1, None, False))
+ self.assertRaises(StopIteration, it.evaluate, 2, None, False)
+
def test_getter(self):
it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1])
self.assertEqual(2, it.evaluate(0, None, False))
@@ -119,9 +140,11 @@ class PostGenerationDeclarationTestCase(unittest.TestCase):
def test_extract_no_prefix(self):
decl = declarations.PostGenerationDeclaration()
- extracted, kwargs = decl.extract('foo', {'foo': 13, 'foo__bar': 42})
- self.assertEqual(extracted, 13)
- self.assertEqual(kwargs, {'bar': 42})
+ context = decl.extract('foo',
+ {'foo': 13, 'foo__bar': 42})
+ self.assertTrue(context.did_extract)
+ self.assertEqual(context.value, 13)
+ self.assertEqual(context.extra, {'bar': 42})
def test_decorator_simple(self):
call_params = []
@@ -130,45 +153,53 @@ class PostGenerationDeclarationTestCase(unittest.TestCase):
call_params.append(args)
call_params.append(kwargs)
- extracted, kwargs = foo.extract('foo',
+ context = foo.extract('foo',
{'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1})
- self.assertEqual(13, extracted)
- self.assertEqual({'bar': 42}, kwargs)
+ self.assertTrue(context.did_extract)
+ self.assertEqual(13, context.value)
+ self.assertEqual({'bar': 42}, context.extra)
# No value returned.
- foo.call(None, False, extracted, **kwargs)
+ foo.call(None, False, context)
self.assertEqual(2, len(call_params))
self.assertEqual((None, False, 13), call_params[0])
self.assertEqual({'bar': 42}, call_params[1])
-class SubFactoryTestCase(unittest.TestCase):
+class FactoryWrapperTestCase(unittest.TestCase):
+ def test_invalid_path(self):
+ self.assertRaises(ValueError, declarations._FactoryWrapper, 'UnqualifiedSymbol')
+ self.assertRaises(ValueError, declarations._FactoryWrapper, 42)
+
+ def test_class(self):
+ w = declarations._FactoryWrapper(datetime.date)
+ self.assertEqual(datetime.date, w.get())
- def test_arg(self):
- self.assertRaises(ValueError, declarations.SubFactory, 'UnqualifiedSymbol')
+ def test_path(self):
+ w = declarations._FactoryWrapper('datetime.date')
+ self.assertEqual(datetime.date, w.get())
def test_lazyness(self):
- f = declarations.SubFactory('factory.declarations.Sequence', x=3)
+ f = declarations._FactoryWrapper('factory.declarations.Sequence')
self.assertEqual(None, f.factory)
- self.assertEqual({'x': 3}, f.defaults)
-
- factory_class = f.get_factory()
+ factory_class = f.get()
self.assertEqual(declarations.Sequence, factory_class)
def test_cache(self):
+ """Ensure that _FactoryWrapper tries to import only once."""
orig_date = datetime.date
- f = declarations.SubFactory('datetime.date')
- self.assertEqual(None, f.factory)
+ w = declarations._FactoryWrapper('datetime.date')
+ self.assertEqual(None, w.factory)
- factory_class = f.get_factory()
+ factory_class = w.get()
self.assertEqual(orig_date, factory_class)
try:
# Modify original value
datetime.date = None
# Repeat import
- factory_class = f.get_factory()
+ factory_class = w.get()
self.assertEqual(orig_date, factory_class)
finally:
@@ -178,106 +209,92 @@ class SubFactoryTestCase(unittest.TestCase):
class RelatedFactoryTestCase(unittest.TestCase):
- def test_arg(self):
- self.assertRaises(ValueError, declarations.RelatedFactory, 'UnqualifiedSymbol')
-
- def test_lazyness(self):
- f = declarations.RelatedFactory('factory.declarations.Sequence', x=3)
- self.assertEqual(None, f.factory)
-
- self.assertEqual({'x': 3}, f.defaults)
+ def test_deprecate_name(self):
+ with warnings.catch_warnings(record=True) as w:
- factory_class = f.get_factory()
- self.assertEqual(declarations.Sequence, factory_class)
+ warnings.simplefilter('always')
+ f = declarations.RelatedFactory('datetime.date', name='blah')
- def test_cache(self):
- """Ensure that RelatedFactory tries to import only once."""
- orig_date = datetime.date
- f = declarations.RelatedFactory('datetime.date')
- self.assertEqual(None, f.factory)
-
- factory_class = f.get_factory()
- self.assertEqual(orig_date, factory_class)
-
- try:
- # Modify original value
- datetime.date = None
- # Repeat import
- factory_class = f.get_factory()
- self.assertEqual(orig_date, factory_class)
-
- finally:
- # IMPORTANT: restore attribute.
- datetime.date = orig_date
+ 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()
+ def ctx(self, value=None, force_value=False, extra=None):
+ return declarations.ExtractionContext(
+ value,
+ bool(value) or force_value,
+ extra,
+ )
+
def test_simplest_setup_and_call(self):
decl = declarations.PostGenerationMethodCall('method')
- decl.call(self.obj, False)
+ decl.call(self.obj, False, self.ctx())
self.obj.method.assert_called_once_with()
def test_call_with_method_args(self):
decl = declarations.PostGenerationMethodCall(
'method', 'data')
- decl.call(self.obj, False)
+ decl.call(self.obj, False, self.ctx())
self.obj.method.assert_called_once_with('data')
def test_call_with_passed_extracted_string(self):
decl = declarations.PostGenerationMethodCall(
'method')
- decl.call(self.obj, False, 'data')
+ decl.call(self.obj, False, self.ctx('data'))
self.obj.method.assert_called_once_with('data')
def test_call_with_passed_extracted_int(self):
decl = declarations.PostGenerationMethodCall('method')
- decl.call(self.obj, False, 1)
+ decl.call(self.obj, False, self.ctx(1))
self.obj.method.assert_called_once_with(1)
def test_call_with_passed_extracted_iterable(self):
decl = declarations.PostGenerationMethodCall('method')
- decl.call(self.obj, False, (1, 2, 3))
+ decl.call(self.obj, False, self.ctx((1, 2, 3)))
self.obj.method.assert_called_once_with((1, 2, 3))
def test_call_with_method_kwargs(self):
decl = declarations.PostGenerationMethodCall(
'method', data='data')
- decl.call(self.obj, False)
+ decl.call(self.obj, False, self.ctx())
self.obj.method.assert_called_once_with(data='data')
def test_call_with_passed_kwargs(self):
decl = declarations.PostGenerationMethodCall('method')
- decl.call(self.obj, False, data='other')
+ decl.call(self.obj, False, self.ctx(extra={'data': 'other'}))
self.obj.method.assert_called_once_with(data='other')
def test_multi_call_with_multi_method_args(self):
decl = declarations.PostGenerationMethodCall(
'method', 'arg1', 'arg2')
- decl.call(self.obj, False)
+ decl.call(self.obj, False, self.ctx())
self.obj.method.assert_called_once_with('arg1', 'arg2')
def test_multi_call_with_passed_multiple_args(self):
decl = declarations.PostGenerationMethodCall(
'method', 'arg1', 'arg2')
- decl.call(self.obj, False, ('param1', 'param2', 'param3'))
+ decl.call(self.obj, False, self.ctx(('param1', 'param2', 'param3')))
self.obj.method.assert_called_once_with('param1', 'param2', 'param3')
def test_multi_call_with_passed_tuple(self):
decl = declarations.PostGenerationMethodCall(
'method', 'arg1', 'arg2')
- decl.call(self.obj, False, (('param1', 'param2'),))
+ decl.call(self.obj, False, self.ctx((('param1', 'param2'),)))
self.obj.method.assert_called_once_with(('param1', 'param2'))
def test_multi_call_with_kwargs(self):
decl = declarations.PostGenerationMethodCall(
'method', 'arg1', 'arg2')
- decl.call(self.obj, False, x=2)
+ decl.call(self.obj, False, self.ctx(extra={'x': 2}))
self.obj.method.assert_called_once_with('arg1', 'arg2', x=2)
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
unittest.main()
diff --git a/tests/test_django.py b/tests/test_django.py
new file mode 100644
index 0000000..e4bbc2b
--- /dev/null
+++ b/tests/test_django.py
@@ -0,0 +1,515 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011-2013 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.
+"""Tests for factory_boy/Django interactions."""
+
+import os
+
+import factory
+import factory.django
+
+
+try:
+ import django
+except ImportError: # pragma: no cover
+ django = None
+
+try:
+ from PIL import Image
+except ImportError: # pragma: no cover
+ # Try PIL alternate name
+ try:
+ import Image
+ except ImportError:
+ # OK, not installed
+ Image = None
+
+
+from .compat import is_python2, unittest
+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 .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
+
+
+test_state = {}
+
+
+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_state = runner.setup_databases()
+ test_state.update({
+ 'runner': runner,
+ 'runner_state': runner_state,
+ })
+
+
+def tearDownModule():
+ if django is None: # pragma: no cover
+ return
+ runner = test_state['runner']
+ runner_state = test_state['runner_state']
+ runner.teardown_databases(runner_state)
+ django_test_utils.teardown_test_environment()
+
+
+class StandardFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.StandardModel
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+
+
+class StandardFactoryWithPKField(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.StandardModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('pk',)
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+ pk = None
+
+
+class NonIntegerPkFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.NonIntegerPk
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+ bar = ''
+
+
+class AbstractBaseFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.AbstractBase
+ ABSTRACT_FACTORY = True
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+
+
+class ConcreteSonFactory(AbstractBaseFactory):
+ FACTORY_FOR = models.ConcreteSon
+
+
+class WithFileFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.WithFile
+
+ if django is not None:
+ afile = factory.django.FileField()
+
+
+class WithImageFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.WithImage
+
+ if django is not None:
+ animage = factory.django.ImageField()
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoPkSequenceTestCase(django_test.TestCase):
+ def setUp(self):
+ super(DjangoPkSequenceTestCase, self).setUp()
+ StandardFactory.reset_sequence()
+
+ def test_pk_first(self):
+ std = StandardFactory.build()
+ self.assertEqual('foo1', std.foo)
+
+ def test_pk_many(self):
+ std1 = StandardFactory.build()
+ std2 = StandardFactory.build()
+ self.assertEqual('foo1', std1.foo)
+ self.assertEqual('foo2', std2.foo)
+
+ def test_pk_creation(self):
+ std1 = StandardFactory.create()
+ self.assertEqual('foo1', std1.foo)
+ self.assertEqual(1, std1.pk)
+
+ StandardFactory.reset_sequence()
+ std2 = StandardFactory.create()
+ self.assertEqual('foo2', 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(10, std1.pk)
+
+ StandardFactory.reset_sequence()
+ std2 = StandardFactory.create()
+ self.assertEqual('foo11', std2.foo)
+ self.assertEqual(11, std2.pk)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoPkForceTestCase(django_test.TestCase):
+ def setUp(self):
+ super(DjangoPkForceTestCase, self).setUp()
+ StandardFactoryWithPKField.reset_sequence()
+
+ def test_no_pk(self):
+ std = StandardFactoryWithPKField()
+ self.assertIsNotNone(std.pk)
+ self.assertEqual('foo1', std.foo)
+
+ def test_force_pk(self):
+ std = StandardFactoryWithPKField(pk=42)
+ self.assertIsNotNone(std.pk)
+ self.assertEqual('foo1', std.foo)
+
+ def test_reuse_pk(self):
+ std1 = StandardFactoryWithPKField(foo='bar')
+ self.assertIsNotNone(std1.pk)
+
+ std2 = StandardFactoryWithPKField(pk=std1.pk, foo='blah')
+ self.assertEqual(std1.pk, std2.pk)
+ self.assertEqual('bar', std2.foo)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoModelLoadingTestCase(django_test.TestCase):
+ """Tests FACTORY_FOR = 'app.Model' pattern."""
+
+ def test_loading(self):
+ class ExampleFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = 'djapp.StandardModel'
+
+ self.assertEqual(models.StandardModel, ExampleFactory._get_target_class())
+
+ def test_building(self):
+ class ExampleFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = 'djapp.StandardModel'
+
+ e = ExampleFactory.build()
+ self.assertEqual(models.StandardModel, e.__class__)
+
+ def test_inherited_loading(self):
+ """Proper loading of a model within 'child' factories.
+
+ See https://github.com/rbarrois/factory_boy/issues/109.
+ """
+ class ExampleFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = 'djapp.StandardModel'
+
+ class Example2Factory(ExampleFactory):
+ pass
+
+ e = Example2Factory.build()
+ self.assertEqual(models.StandardModel, e.__class__)
+
+ def test_inherited_loading_and_sequence(self):
+ """Proper loading of a model within 'child' factories.
+
+ See https://github.com/rbarrois/factory_boy/issues/109.
+ """
+ class ExampleFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = 'djapp.StandardModel'
+
+ foo = factory.Sequence(lambda n: n)
+
+ class Example2Factory(ExampleFactory):
+ FACTORY_FOR = 'djapp.StandardSon'
+
+ self.assertEqual(models.StandardSon, Example2Factory._get_target_class())
+
+ e1 = ExampleFactory.build()
+ e2 = Example2Factory.build()
+ e3 = ExampleFactory.build()
+ 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)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoNonIntegerPkTestCase(django_test.TestCase):
+ def setUp(self):
+ super(DjangoNonIntegerPkTestCase, self).setUp()
+ NonIntegerPkFactory.reset_sequence()
+
+ def test_first(self):
+ nonint = NonIntegerPkFactory.build()
+ self.assertEqual('foo1', nonint.foo)
+
+ def test_many(self):
+ nonint1 = NonIntegerPkFactory.build()
+ nonint2 = NonIntegerPkFactory.build()
+
+ self.assertEqual('foo1', nonint1.foo)
+ self.assertEqual('foo2', nonint2.foo)
+
+ def test_creation(self):
+ nonint1 = NonIntegerPkFactory.create()
+ self.assertEqual('foo1', nonint1.foo)
+ self.assertEqual('foo1', nonint1.pk)
+
+ NonIntegerPkFactory.reset_sequence()
+ nonint2 = NonIntegerPkFactory.build()
+ self.assertEqual('foo1', nonint2.foo)
+
+ def test_force_pk(self):
+ nonint1 = NonIntegerPkFactory.create(pk='foo10')
+ self.assertEqual('foo10', nonint1.foo)
+ self.assertEqual('foo10', nonint1.pk)
+
+ NonIntegerPkFactory.reset_sequence()
+ nonint2 = NonIntegerPkFactory.create()
+ self.assertEqual('foo1', nonint2.foo)
+ self.assertEqual('foo1', nonint2.pk)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoAbstractBaseSequenceTestCase(django_test.TestCase):
+ def test_auto_sequence(self):
+ with factory.debug():
+ obj = ConcreteSonFactory()
+ self.assertEqual(1, obj.pk)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+class DjangoFileFieldTestCase(unittest.TestCase):
+
+ def tearDown(self):
+ super(DjangoFileFieldTestCase, self).tearDown()
+ for path in os.listdir(models.WITHFILE_UPLOAD_DIR):
+ # Remove temporary files written during tests.
+ os.unlink(os.path.join(models.WITHFILE_UPLOAD_DIR, path))
+
+ def test_default_build(self):
+ o = WithFileFactory.build()
+ self.assertIsNone(o.pk)
+ self.assertEqual(b'', o.afile.read())
+ self.assertEqual('django/example.dat', o.afile.name)
+
+ def test_default_create(self):
+ o = WithFileFactory.create()
+ self.assertIsNotNone(o.pk)
+ self.assertEqual(b'', o.afile.read())
+ self.assertEqual('django/example.dat', o.afile.name)
+
+ def test_with_content(self):
+ o = WithFileFactory.build(afile__data='foo')
+ self.assertIsNone(o.pk)
+ 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)
+ 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)
+ self.assertEqual(b'example_data\n', o.afile.read())
+ self.assertEqual('django/example.data', o.afile.name)
+
+ def test_with_file_empty_path(self):
+ with open(testdata.TESTFILE_PATH, 'rb') as f:
+ o = WithFileFactory.build(
+ afile__from_file=f,
+ afile__from_path=''
+ )
+ self.assertIsNone(o.pk)
+ self.assertEqual(b'example_data\n', o.afile.read())
+ self.assertEqual('django/example.data', o.afile.name)
+
+ def test_with_path_empty_file(self):
+ o = WithFileFactory.build(
+ afile__from_path=testdata.TESTFILE_PATH,
+ afile__from_file=None,
+ )
+ self.assertIsNone(o.pk)
+ self.assertEqual(b'example_data\n', o.afile.read())
+ self.assertEqual('django/example.data', o.afile.name)
+
+ def test_error_both_file_and_path(self):
+ self.assertRaises(ValueError, WithFileFactory.build,
+ afile__from_file='fakefile',
+ afile__from_path=testdata.TESTFILE_PATH,
+ )
+
+ def test_override_filename_with_path(self):
+ o = WithFileFactory.build(
+ afile__from_path=testdata.TESTFILE_PATH,
+ afile__filename='example.foo',
+ )
+ self.assertIsNone(o.pk)
+ 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)
+
+ o2 = WithFileFactory.build(afile=o1.afile)
+ self.assertIsNone(o2.pk)
+ self.assertEqual(b'example_data\n', o2.afile.read())
+ self.assertEqual('django/example_1.data', o2.afile.name)
+
+ def test_no_file(self):
+ o = WithFileFactory.build(afile=None)
+ self.assertIsNone(o.pk)
+ self.assertFalse(o.afile)
+
+
+@unittest.skipIf(django is None, "Django not installed.")
+@unittest.skipIf(Image is None, "PIL not installed.")
+class DjangoImageFieldTestCase(unittest.TestCase):
+
+ def tearDown(self):
+ super(DjangoImageFieldTestCase, self).tearDown()
+ for path in os.listdir(models.WITHFILE_UPLOAD_DIR):
+ # Remove temporary files written during tests.
+ os.unlink(os.path.join(models.WITHFILE_UPLOAD_DIR, path))
+
+ def test_default_build(self):
+ o = WithImageFactory.build()
+ self.assertIsNone(o.pk)
+ self.assertEqual(100, o.animage.width)
+ self.assertEqual(100, o.animage.height)
+ self.assertEqual('django/example.jpg', o.animage.name)
+
+ def test_default_create(self):
+ o = WithImageFactory.create()
+ self.assertIsNotNone(o.pk)
+ self.assertEqual(100, o.animage.width)
+ self.assertEqual(100, o.animage.height)
+ self.assertEqual('django/example.jpg', o.animage.name)
+
+ def test_with_content(self):
+ o = WithImageFactory.build(animage__width=13, animage__color='red')
+ self.assertIsNone(o.pk)
+ self.assertEqual(13, o.animage.width)
+ self.assertEqual(13, o.animage.height)
+ self.assertEqual('django/example.jpg', o.animage.name)
+
+ i = Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name))
+ colors = i.getcolors()
+ # 169 pixels with rgb(254, 0, 0)
+ self.assertEqual([(169, (254, 0, 0))], colors)
+ self.assertEqual('JPEG', i.format)
+
+ def test_gif(self):
+ o = WithImageFactory.build(animage__width=13, animage__color='blue', animage__format='GIF')
+ self.assertIsNone(o.pk)
+ self.assertEqual(13, o.animage.width)
+ self.assertEqual(13, o.animage.height)
+ self.assertEqual('django/example.jpg', o.animage.name)
+
+ i = Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name))
+ colors = i.getcolors()
+ # 169 pixels with color 190 from the GIF palette
+ self.assertEqual([(169, 190)], colors)
+ self.assertEqual('GIF', i.format)
+
+ def test_with_file(self):
+ with open(testdata.TESTIMAGE_PATH, 'rb') as f:
+ o = WithImageFactory.build(animage__from_file=f)
+ self.assertIsNone(o.pk)
+ # 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)
+
+ def test_with_path(self):
+ o = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH)
+ self.assertIsNone(o.pk)
+ # 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)
+
+ def test_with_file_empty_path(self):
+ with open(testdata.TESTIMAGE_PATH, 'rb') as f:
+ o = WithImageFactory.build(
+ animage__from_file=f,
+ animage__from_path=''
+ )
+ self.assertIsNone(o.pk)
+ # 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)
+
+ def test_with_path_empty_file(self):
+ o = WithImageFactory.build(
+ animage__from_path=testdata.TESTIMAGE_PATH,
+ animage__from_file=None,
+ )
+ self.assertIsNone(o.pk)
+ # 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)
+
+ def test_error_both_file_and_path(self):
+ self.assertRaises(ValueError, WithImageFactory.build,
+ animage__from_file='fakefile',
+ animage__from_path=testdata.TESTIMAGE_PATH,
+ )
+
+ def test_override_filename_with_path(self):
+ o = WithImageFactory.build(
+ animage__from_path=testdata.TESTIMAGE_PATH,
+ animage__filename='example.foo',
+ )
+ self.assertIsNone(o.pk)
+ # 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)
+
+ o2 = WithImageFactory.build(animage=o1.animage)
+ self.assertIsNone(o2.pk)
+ # 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)
+
+ def test_no_file(self):
+ o = WithImageFactory.build(animage=None)
+ self.assertIsNone(o.pk)
+ self.assertFalse(o.animage)
+
+
+if __name__ == '__main__': # pragma: no cover
+ unittest.main()
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index 70a2095..d6f33bb 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -21,9 +21,14 @@
# THE SOFTWARE.
+import datetime
+import decimal
+
+from factory import compat
from factory import fuzzy
from .compat import mock, unittest
+from . import utils
class FuzzyAttributeTestCase(unittest.TestCase):
@@ -59,7 +64,7 @@ class FuzzyChoiceTestCase(unittest.TestCase):
def options():
for i in range(3):
yield i
-
+
d = fuzzy.FuzzyChoice(options())
res = d.evaluate(2, None, False)
@@ -102,3 +107,390 @@ class FuzzyIntegerTestCase(unittest.TestCase):
res = fuzz.evaluate(2, None, False)
self.assertEqual(8, res)
+
+
+class FuzzyDecimalTestCase(unittest.TestCase):
+ def test_definition(self):
+ """Tests all ways of defining a FuzzyDecimal."""
+ fuzz = fuzzy.FuzzyDecimal(2.0, 3.0)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertTrue(decimal.Decimal('2.0') <= res <= decimal.Decimal('3.0'),
+ "value %d is not between 2.0 and 3.0" % res)
+
+ fuzz = fuzzy.FuzzyDecimal(4.0)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertTrue(decimal.Decimal('0.0') <= res <= decimal.Decimal('4.0'),
+ "value %d is not between 0.0 and 4.0" % res)
+
+ fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertTrue(decimal.Decimal('0.54') <= res <= decimal.Decimal('4.0'),
+ "value %d is not between 0.54 and 4.0" % res)
+ self.assertTrue(res.as_tuple().exponent, -5)
+
+ def test_biased(self):
+ fake_uniform = lambda low, high: low + high
+
+ fuzz = fuzzy.FuzzyDecimal(2.0, 8.0)
+
+ with mock.patch('random.uniform', fake_uniform):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(decimal.Decimal('10.0'), res)
+
+ def test_biased_high_only(self):
+ fake_uniform = lambda low, high: low + high
+
+ fuzz = fuzzy.FuzzyDecimal(8.0)
+
+ with mock.patch('random.uniform', fake_uniform):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(decimal.Decimal('8.0'), res)
+
+ def test_precision(self):
+ fake_uniform = lambda low, high: low + high + 0.001
+
+ fuzz = fuzzy.FuzzyDecimal(8.0, precision=3)
+
+ with mock.patch('random.uniform', fake_uniform):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res)
+
+
+class FuzzyDateTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # Setup useful constants
+ cls.jan1 = datetime.date(2013, 1, 1)
+ cls.jan3 = datetime.date(2013, 1, 3)
+ cls.jan31 = datetime.date(2013, 1, 31)
+
+ def test_accurate_definition(self):
+ """Tests all ways of defining a FuzzyDate."""
+ fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan31)
+
+ def test_partial_definition(self):
+ """Test defining a FuzzyDate without passing an end date."""
+ with utils.mocked_date_today(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyDate(self.jan1)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan3)
+
+ def test_invalid_definition(self):
+ self.assertRaises(ValueError, fuzzy.FuzzyDate,
+ self.jan31, self.jan1)
+
+ def test_invalid_partial_definition(self):
+ with utils.mocked_date_today(self.jan1, fuzzy):
+ self.assertRaises(ValueError, fuzzy.FuzzyDate,
+ self.jan31)
+
+ def test_biased(self):
+ """Tests a FuzzyDate with a biased random.randint."""
+
+ fake_randint = lambda low, high: (low + high) // 2
+ fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.date(2013, 1, 16), res)
+
+ def test_biased_partial(self):
+ """Tests a FuzzyDate with a biased random and implicit upper bound."""
+ with utils.mocked_date_today(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyDate(self.jan1)
+
+ fake_randint = lambda low, high: (low + high) // 2
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.date(2013, 1, 2), res)
+
+
+class FuzzyNaiveDateTimeTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # Setup useful constants
+ cls.jan1 = datetime.datetime(2013, 1, 1)
+ cls.jan3 = datetime.datetime(2013, 1, 3)
+ cls.jan31 = datetime.datetime(2013, 1, 31)
+
+ def test_accurate_definition(self):
+ """Tests explicit definition of a FuzzyNaiveDateTime."""
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan31)
+
+ def test_partial_definition(self):
+ """Test defining a FuzzyNaiveDateTime without passing an end date."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan3)
+
+ def test_aware_start(self):
+ """Tests that a timezone-aware start datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan1.replace(tzinfo=compat.UTC), self.jan31)
+
+ def test_aware_end(self):
+ """Tests that a timezone-aware end datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan1, self.jan31.replace(tzinfo=compat.UTC))
+
+ def test_force_year(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_year=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.year)
+
+ def test_force_month(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_month=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.month)
+
+ def test_force_day(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_day=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.day)
+
+ def test_force_hour(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_hour=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.hour)
+
+ def test_force_minute(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_minute=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.minute)
+
+ def test_force_second(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_second=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.second)
+
+ def test_force_microsecond(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_microsecond=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.microsecond)
+
+ def test_invalid_definition(self):
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan31, self.jan1)
+
+ def test_invalid_partial_definition(self):
+ with utils.mocked_datetime_now(self.jan1, fuzzy):
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan31)
+
+ def test_biased(self):
+ """Tests a FuzzyDate with a biased random.randint."""
+
+ fake_randint = lambda low, high: (low + high) // 2
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 16), res)
+
+ def test_biased_partial(self):
+ """Tests a FuzzyDate with a biased random and implicit upper bound."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1)
+
+ fake_randint = lambda low, high: (low + high) // 2
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 2), res)
+
+
+class FuzzyDateTimeTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # Setup useful constants
+ cls.jan1 = datetime.datetime(2013, 1, 1, tzinfo=compat.UTC)
+ cls.jan3 = datetime.datetime(2013, 1, 3, tzinfo=compat.UTC)
+ cls.jan31 = datetime.datetime(2013, 1, 31, tzinfo=compat.UTC)
+
+ def test_accurate_definition(self):
+ """Tests explicit definition of a FuzzyDateTime."""
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan31)
+
+ def test_partial_definition(self):
+ """Test defining a FuzzyDateTime without passing an end date."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan3)
+
+ def test_invalid_definition(self):
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan31, self.jan1)
+
+ def test_invalid_partial_definition(self):
+ with utils.mocked_datetime_now(self.jan1, fuzzy):
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan31)
+
+ def test_naive_start(self):
+ """Tests that a timezone-naive start datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan1.replace(tzinfo=None), self.jan31)
+
+ def test_naive_end(self):
+ """Tests that a timezone-naive end datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan1, self.jan31.replace(tzinfo=None))
+
+ def test_force_year(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_year=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.year)
+
+ def test_force_month(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_month=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.month)
+
+ def test_force_day(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_day=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.day)
+
+ def test_force_hour(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_hour=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.hour)
+
+ def test_force_minute(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_minute=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.minute)
+
+ def test_force_second(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_second=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.second)
+
+ def test_force_microsecond(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_microsecond=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.microsecond)
+
+ def test_biased(self):
+ """Tests a FuzzyDate with a biased random.randint."""
+
+ fake_randint = lambda low, high: (low + high) // 2
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res)
+
+ def test_biased_partial(self):
+ """Tests a FuzzyDate with a biased random and implicit upper bound."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1)
+
+ fake_randint = lambda low, high: (low + high) // 2
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res)
+
+
+class FuzzyTextTestCase(unittest.TestCase):
+
+ def test_unbiased(self):
+ chars = ['a', 'b', 'c']
+ fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=12)
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual('pre', res[:3])
+ self.assertEqual('post', res[-4:])
+ self.assertEqual(3 + 12 + 4, len(res))
+
+ for char in res[3:-4]:
+ self.assertIn(char, chars)
+
+ def test_mock(self):
+ fake_choice = lambda chars: chars[0]
+
+ chars = ['a', 'b', 'c']
+ fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4)
+ with mock.patch('random.choice', fake_choice):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual('preaaaapost', res)
+
+ def test_generator(self):
+ def options():
+ yield 'a'
+ yield 'b'
+ yield 'c'
+
+ fuzz = fuzzy.FuzzyText(chars=options(), length=12)
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(12, len(res))
+
+ for char in res:
+ self.assertIn(char, ['a', 'b', 'c'])
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
new file mode 100644
index 0000000..f5a66e5
--- /dev/null
+++ b/tests/test_helpers.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 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 logging
+
+from factory import helpers
+
+from .compat import io, unittest
+
+
+class DebugTest(unittest.TestCase):
+ """Tests for the 'factory.debug()' helper."""
+
+ def test_default_logger(self):
+ stream1 = io.StringIO()
+ stream2 = io.StringIO()
+
+ l = logging.getLogger('factory.test')
+ h = logging.StreamHandler(stream1)
+ h.setLevel(logging.INFO)
+ l.addHandler(h)
+
+ # Non-debug: no text gets out
+ l.debug("Test")
+ self.assertEqual('', stream1.getvalue())
+
+ with helpers.debug(stream=stream2):
+ # Debug: text goes to new stream only
+ l.debug("Test2")
+
+ self.assertEqual('', stream1.getvalue())
+ self.assertEqual("Test2\n", stream2.getvalue())
+
+ def test_alternate_logger(self):
+ stream1 = io.StringIO()
+ stream2 = io.StringIO()
+
+ l1 = logging.getLogger('factory.test')
+ l2 = logging.getLogger('factory.foo')
+ h = logging.StreamHandler(stream1)
+ h.setLevel(logging.DEBUG)
+ l2.addHandler(h)
+
+ # Non-debug: no text gets out
+ l1.debug("Test")
+ self.assertEqual('', stream1.getvalue())
+ l2.debug("Test")
+ self.assertEqual('', stream1.getvalue())
+
+ with helpers.debug('factory.test', stream=stream2):
+ # Debug: text goes to new stream only
+ l1.debug("Test2")
+ l2.debug("Test3")
+
+ self.assertEqual("", stream1.getvalue())
+ self.assertEqual("Test2\n", stream2.getvalue())
+
diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py
new file mode 100644
index 0000000..803607a
--- /dev/null
+++ b/tests/test_mongoengine.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2013 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
+# 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.
+
+"""Tests for factory_boy/MongoEngine interactions."""
+
+import factory
+import os
+from .compat import unittest
+
+
+try:
+ import mongoengine
+except ImportError:
+ mongoengine = None
+
+if mongoengine:
+ from factory.mongoengine import MongoEngineFactory
+
+ class Address(mongoengine.EmbeddedDocument):
+ street = mongoengine.StringField()
+
+ class Person(mongoengine.Document):
+ name = mongoengine.StringField()
+ address = mongoengine.EmbeddedDocumentField(Address)
+
+ class AddressFactory(MongoEngineFactory):
+ FACTORY_FOR = Address
+
+ street = factory.Sequence(lambda n: 'street%d' % n)
+
+ class PersonFactory(MongoEngineFactory):
+ FACTORY_FOR = Person
+
+ name = factory.Sequence(lambda n: 'name%d' % n)
+ address = factory.SubFactory(AddressFactory)
+
+
+@unittest.skipIf(mongoengine is None, "mongoengine not installed.")
+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'))
+
+ @classmethod
+ def setUpClass(cls):
+ cls.db = mongoengine.connect(cls.db_name, host=cls.db_host, port=cls.db_port)
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.db.drop_database(cls.db_name)
+
+ def setUp(self):
+ mongoengine.connect('factory_boy_test')
+
+ def test_build(self):
+ std = PersonFactory.build()
+ self.assertEqual('name0', std.name)
+ self.assertEqual('street0', std.address.street)
+ self.assertIsNone(std.id)
+
+ def test_creation(self):
+ std1 = PersonFactory.create()
+ self.assertEqual('name1', std1.name)
+ self.assertEqual('street1', std1.address.street)
+ self.assertIsNotNone(std1.id)
diff --git a/tests/test_using.py b/tests/test_using.py
index 821fad3..3979cd0 100644
--- a/tests/test_using.py
+++ b/tests/test_using.py
@@ -125,7 +125,7 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(obj.foo, 'bar')
def test_create_custom_base(self):
- obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory)
+ obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.django.DjangoModelFactory)
self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
@@ -141,7 +141,7 @@ class SimpleBuildTestCase(unittest.TestCase):
def test_create_batch_custom_base(self):
objs = factory.create_batch(FakeModel, 4, foo='bar',
- FACTORY_CLASS=factory.DjangoModelFactory)
+ FACTORY_CLASS=factory.django.DjangoModelFactory)
self.assertEqual(4, len(objs))
self.assertEqual(4, len(set(objs)))
@@ -177,7 +177,7 @@ class SimpleBuildTestCase(unittest.TestCase):
def test_generate_create_custom_base(self):
obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar',
- FACTORY_CLASS=factory.DjangoModelFactory)
+ FACTORY_CLASS=factory.django.DjangoModelFactory)
self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
@@ -208,7 +208,7 @@ class SimpleBuildTestCase(unittest.TestCase):
def test_generate_batch_create_custom_base(self):
objs = factory.generate_batch(FakeModel, factory.CREATE_STRATEGY, 20, foo='bar',
- FACTORY_CLASS=factory.DjangoModelFactory)
+ FACTORY_CLASS=factory.django.DjangoModelFactory)
self.assertEqual(20, len(objs))
self.assertEqual(20, len(set(objs)))
@@ -238,7 +238,7 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(obj.foo, 'bar')
def test_simple_generate_create_custom_base(self):
- obj = factory.simple_generate(FakeModel, True, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory)
+ obj = factory.simple_generate(FakeModel, True, foo='bar', FACTORY_CLASS=factory.django.DjangoModelFactory)
self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
@@ -264,7 +264,7 @@ class SimpleBuildTestCase(unittest.TestCase):
def test_simple_generate_batch_create_custom_base(self):
objs = factory.simple_generate_batch(FakeModel, True, 20, foo='bar',
- FACTORY_CLASS=factory.DjangoModelFactory)
+ FACTORY_CLASS=factory.django.DjangoModelFactory)
self.assertEqual(20, len(objs))
self.assertEqual(20, len(set(objs)))
@@ -299,7 +299,7 @@ class UsingFactoryTestCase(unittest.TestCase):
test_object = TestObjectFactory.build()
self.assertEqual(test_object.one, 'one')
- def test_inheritance(self):
+ def test_inheriting_target_class(self):
@factory.use_strategy(factory.BUILD_STRATEGY)
class TestObjectFactory(factory.Factory, TestObject):
FACTORY_FOR = TestObject
@@ -730,6 +730,78 @@ class UsingFactoryTestCase(unittest.TestCase):
test_object_alt = TestObjectFactory.build()
self.assertEqual(None, test_object_alt.three)
+ def test_inheritance_and_sequences(self):
+ """Sequence counters should be kept within an inheritance chain."""
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: n)
+
+ class TestObjectFactory2(TestObjectFactory):
+ FACTORY_FOR = TestObject
+
+ to1a = TestObjectFactory()
+ self.assertEqual(0, to1a.one)
+ to2a = TestObjectFactory2()
+ self.assertEqual(1, to2a.one)
+ to1b = TestObjectFactory()
+ self.assertEqual(2, to1b.one)
+ to2b = TestObjectFactory2()
+ self.assertEqual(3, to2b.one)
+
+ def test_inheritance_sequence_inheriting_objects(self):
+ """Sequence counters are kept with inheritance, incl. misc objects."""
+ class TestObject2(TestObject):
+ pass
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: n)
+
+ class TestObjectFactory2(TestObjectFactory):
+ FACTORY_FOR = TestObject2
+
+ to1a = TestObjectFactory()
+ self.assertEqual(0, to1a.one)
+ to2a = TestObjectFactory2()
+ self.assertEqual(1, to2a.one)
+ to1b = TestObjectFactory()
+ self.assertEqual(2, to1b.one)
+ to2b = TestObjectFactory2()
+ self.assertEqual(3, to2b.one)
+
+ def test_inheritance_sequence_unrelated_objects(self):
+ """Sequence counters are kept with inheritance, unrelated objects.
+
+ See issue https://github.com/rbarrois/factory_boy/issues/93
+
+ Problem: sequence counter is somewhat shared between factories
+ until the "slave" factory has been called.
+ """
+
+ class TestObject2(object):
+ def __init__(self, one):
+ self.one = one
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: n)
+
+ class TestObjectFactory2(TestObjectFactory):
+ FACTORY_FOR = TestObject2
+
+ to1a = TestObjectFactory()
+ self.assertEqual(0, to1a.one)
+ to2a = TestObjectFactory2()
+ self.assertEqual(0, to2a.one)
+ to1b = TestObjectFactory()
+ self.assertEqual(1, to1b.one)
+ to2b = TestObjectFactory2()
+ self.assertEqual(1, to2b.one)
+
+
def test_inheritance_with_inherited_class(self):
class TestObjectFactory(factory.Factory):
FACTORY_FOR = TestObject
@@ -946,9 +1018,9 @@ class SubFactoryTestCase(unittest.TestCase):
class TestModel2Factory(FakeModelFactory):
FACTORY_FOR = TestModel2
two = factory.SubFactory(TestModelFactory,
- one=factory.Sequence(lambda n: 'x%dx' % n),
- two=factory.LazyAttribute(
- lambda o: '%s%s' % (o.one, o.one)))
+ one=factory.Sequence(lambda n: 'x%dx' % n),
+ two=factory.LazyAttribute(lambda o: '%s%s' % (o.one, o.one)),
+ )
test_model = TestModel2Factory(one=42)
self.assertEqual('x0x', test_model.two.one)
@@ -1056,6 +1128,32 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual(outer.wrap.wrapped.two.four, 4)
self.assertEqual(outer.wrap.friend, 5)
+ def test_nested_subfactory_with_override(self):
+ """Tests replacing a SubFactory field with an actual value."""
+
+ # The test class
+ class TestObject(object):
+ def __init__(self, two='one', wrapped=None):
+ self.two = two
+ self.wrapped = wrapped
+
+ # Innermost factory
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ two = 'two'
+
+ # Intermediary factory
+ class WrappingTestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ wrapped = factory.SubFactory(TestObjectFactory)
+ wrapped__two = 'three'
+
+ obj = TestObject(two='four')
+ outer = WrappingTestObjectFactory(wrapped=obj)
+ self.assertEqual(obj, outer.wrapped)
+ self.assertEqual('four', outer.wrapped.two)
+
def test_sub_factory_and_inheritance(self):
"""Test inheriting from a factory with subfactories, overriding."""
class TestObject(object):
@@ -1253,7 +1351,7 @@ class IteratorTestCase(unittest.TestCase):
@factory.iterator
def one():
- for i in range(10, 50):
+ for i in range(10, 50): # pragma: no cover
yield i
objs = TestObjectFactory.build_batch(20)
@@ -1298,7 +1396,7 @@ class BetterFakeModel(object):
class DjangoModelFactoryTestCase(unittest.TestCase):
def test_simple(self):
- class FakeModelFactory(factory.DjangoModelFactory):
+ class FakeModelFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = FakeModel
obj = FakeModelFactory(one=1)
@@ -1312,7 +1410,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
class MyFakeModel(BetterFakeModel):
objects = BetterFakeModelManager({'x': 1}, prev)
- class MyFakeModelFactory(factory.DjangoModelFactory):
+ class MyFakeModelFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = MyFakeModel
FACTORY_DJANGO_GET_OR_CREATE = ('x',)
x = 1
@@ -1333,7 +1431,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
class MyFakeModel(BetterFakeModel):
objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev)
- class MyFakeModelFactory(factory.DjangoModelFactory):
+ class MyFakeModelFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = MyFakeModel
FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z')
x = 1
@@ -1354,7 +1452,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
class MyFakeModel(BetterFakeModel):
objects = BetterFakeModelManager({'x': 1}, prev)
- class MyFakeModelFactory(factory.DjangoModelFactory):
+ class MyFakeModelFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = MyFakeModel
FACTORY_DJANGO_GET_OR_CREATE = ('x',)
x = 1
@@ -1375,7 +1473,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
class MyFakeModel(BetterFakeModel):
objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev)
- class MyFakeModelFactory(factory.DjangoModelFactory):
+ class MyFakeModelFactory(factory.django.DjangoModelFactory):
FACTORY_FOR = MyFakeModel
FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z')
x = 1
@@ -1389,6 +1487,72 @@ class DjangoModelFactoryTestCase(unittest.TestCase):
self.assertEqual(4, obj.z)
self.assertEqual(2, obj.id)
+ def test_sequence(self):
+ class TestModelFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = TestModel
+
+ a = factory.Sequence(lambda n: 'foo_%s' % n)
+
+ o1 = TestModelFactory()
+ o2 = TestModelFactory()
+
+ self.assertEqual('foo_2', o1.a)
+ self.assertEqual('foo_3', o2.a)
+
+ o3 = TestModelFactory.build()
+ o4 = TestModelFactory.build()
+
+ self.assertEqual('foo_4', o3.a)
+ self.assertEqual('foo_5', o4.a)
+
+ def test_no_get_or_create(self):
+ class TestModelFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = TestModel
+
+ a = factory.Sequence(lambda n: 'foo_%s' % n)
+
+ o = TestModelFactory()
+ self.assertEqual(None, o._defaults)
+ self.assertEqual('foo_2', o.a)
+ self.assertEqual(2, o.id)
+
+ def test_get_or_create(self):
+ class TestModelFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = TestModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b')
+
+ a = factory.Sequence(lambda n: 'foo_%s' % n)
+ b = 2
+ c = 3
+ d = 4
+
+ o = TestModelFactory()
+ self.assertEqual({'c': 3, 'd': 4}, o._defaults)
+ self.assertEqual('foo_2', o.a)
+ self.assertEqual(2, o.b)
+ self.assertEqual(3, o.c)
+ self.assertEqual(4, o.d)
+ self.assertEqual(2, o.id)
+
+ def test_full_get_or_create(self):
+ """Test a DjangoModelFactory with all fields in get_or_create."""
+ class TestModelFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = TestModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b', 'c', 'd')
+
+ a = factory.Sequence(lambda n: 'foo_%s' % n)
+ b = 2
+ c = 3
+ d = 4
+
+ o = TestModelFactory()
+ self.assertEqual({}, o._defaults)
+ self.assertEqual('foo_2', o.a)
+ self.assertEqual(2, o.b)
+ self.assertEqual(3, o.c)
+ self.assertEqual(4, o.d)
+ self.assertEqual(2, o.id)
+
class PostGenerationTestCase(unittest.TestCase):
def test_post_generation(self):
@@ -1580,6 +1744,45 @@ class PostGenerationTestCase(unittest.TestCase):
self.assertEqual(4, related.two)
+class RelatedFactoryExtractionTestCase(unittest.TestCase):
+ def setUp(self):
+ self.relateds = []
+
+ class TestRelatedObject(object):
+ def __init__(subself, obj):
+ self.relateds.append(subself)
+ subself.obj = obj
+ obj.related = subself
+
+ class TestRelatedObjectFactory(factory.Factory):
+ FACTORY_FOR = TestRelatedObject
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.RelatedFactory(TestRelatedObjectFactory, 'obj')
+
+ self.TestRelatedObject = TestRelatedObject
+ self.TestRelatedObjectFactory = TestRelatedObjectFactory
+ self.TestObjectFactory = TestObjectFactory
+
+ def test_no_extraction(self):
+ o = self.TestObjectFactory()
+ self.assertEqual(1, len(self.relateds))
+ rel = self.relateds[0]
+ self.assertEqual(o, rel.obj)
+ self.assertEqual(rel, o.related)
+
+ def test_passed_value(self):
+ o = self.TestObjectFactory(one=42)
+ self.assertEqual([], self.relateds)
+ self.assertFalse(hasattr(o, 'related'))
+
+ def test_passed_none(self):
+ o = self.TestObjectFactory(one=None)
+ self.assertEqual([], self.relateds)
+ self.assertFalse(hasattr(o, 'related'))
+
+
class CircularTestCase(unittest.TestCase):
def test_example(self):
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
@@ -1767,73 +1970,5 @@ class ListTestCase(unittest.TestCase):
], o.two)
-class DjangoModelFactoryTestCase(unittest.TestCase):
- def test_sequence(self):
- class TestModelFactory(factory.DjangoModelFactory):
- FACTORY_FOR = TestModel
-
- a = factory.Sequence(lambda n: 'foo_%s' % n)
-
- o1 = TestModelFactory()
- o2 = TestModelFactory()
-
- self.assertEqual('foo_2', o1.a)
- self.assertEqual('foo_3', o2.a)
-
- o3 = TestModelFactory.build()
- o4 = TestModelFactory.build()
-
- self.assertEqual('foo_4', o3.a)
- self.assertEqual('foo_5', o4.a)
-
- def test_no_get_or_create(self):
- class TestModelFactory(factory.DjangoModelFactory):
- FACTORY_FOR = TestModel
-
- a = factory.Sequence(lambda n: 'foo_%s' % n)
-
- o = TestModelFactory()
- self.assertEqual(None, o._defaults)
- self.assertEqual('foo_2', o.a)
- self.assertEqual(2, o.id)
-
- def test_get_or_create(self):
- class TestModelFactory(factory.DjangoModelFactory):
- FACTORY_FOR = TestModel
- FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b')
-
- a = factory.Sequence(lambda n: 'foo_%s' % n)
- b = 2
- c = 3
- d = 4
-
- o = TestModelFactory()
- self.assertEqual({'c': 3, 'd': 4}, o._defaults)
- self.assertEqual('foo_2', o.a)
- self.assertEqual(2, o.b)
- self.assertEqual(3, o.c)
- self.assertEqual(4, o.d)
- self.assertEqual(2, o.id)
-
- def test_full_get_or_create(self):
- """Test a DjangoModelFactory with all fields in get_or_create."""
- class TestModelFactory(factory.DjangoModelFactory):
- FACTORY_FOR = TestModel
- FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b', 'c', 'd')
-
- a = factory.Sequence(lambda n: 'foo_%s' % n)
- b = 2
- c = 3
- d = 4
-
- o = TestModelFactory()
- self.assertEqual({}, o._defaults)
- self.assertEqual('foo_2', o.a)
- self.assertEqual(2, o.b)
- self.assertEqual(3, o.c)
- self.assertEqual(4, o.d)
- self.assertEqual(2, o.id)
-
-
-if __name__ == '__main__':
+if __name__ == '__main__': # pragma: no cover
unittest.main()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b353c9d..d321c2a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -20,10 +20,14 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+from __future__ import unicode_literals
+
+
+import itertools
from factory import utils
-from .compat import unittest
+from .compat import is_python2, unittest
class ExtractDictTestCase(unittest.TestCase):
@@ -230,3 +234,154 @@ class ImportObjectTestCase(unittest.TestCase):
def test_invalid_module(self):
self.assertRaises(ImportError, utils.import_object,
'this-is-an-invalid-module', '__name__')
+
+
+class LogPPrintTestCase(unittest.TestCase):
+ def test_nothing(self):
+ txt = utils.log_pprint()
+ self.assertEqual('', txt)
+
+ def test_only_args(self):
+ txt = 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})
+ self.assertIn(txt, ['a=1, b=2', 'b=2, a=1'])
+
+ def test_bytes_args(self):
+ txt = 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(('ŧêßŧ',))
+ 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'})
+ expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'"
+ expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'"
+ if is_python2:
+ expected1 = expected1.replace('b', '')
+ expected2 = expected2.replace('b', '')
+ self.assertIn(txt, (expected1, expected2))
+
+ def test_text_kwargs(self):
+ txt = utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'})
+ expected1 = "x='ŧêßŧ', y='ŧßêŧ'"
+ expected2 = "y='ŧßêŧ', x='ŧêßŧ'"
+ if is_python2:
+ expected1 = "x=u'\\u0167\\xea\\xdf\\u0167', y=u'\\u0167\\xdf\\xea\\u0167'"
+ expected2 = "y=u'\\u0167\\xdf\\xea\\u0167', x=u'\\u0167\\xea\\xdf\\u0167'"
+ self.assertIn(txt, (expected1, expected2))
+
+
+class ResetableIteratorTestCase(unittest.TestCase):
+ def test_no_reset(self):
+ i = utils.ResetableIterator([1, 2, 3])
+ self.assertEqual([1, 2, 3], list(i))
+
+ def test_no_reset_new_iterator(self):
+ i = utils.ResetableIterator([1, 2, 3])
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+
+ iterator2 = iter(i)
+ self.assertEqual(3, next(iterator2))
+
+ def test_infinite(self):
+ i = utils.ResetableIterator(itertools.cycle([1, 2, 3]))
+ iterator = iter(i)
+ values = [next(iterator) for _i in range(10)]
+ self.assertEqual([1, 2, 3, 1, 2, 3, 1, 2, 3, 1], values)
+
+ def test_reset_simple(self):
+ i = utils.ResetableIterator([1, 2, 3])
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+
+ def test_reset_at_begin(self):
+ i = utils.ResetableIterator([1, 2, 3])
+ iterator = iter(i)
+ i.reset()
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+
+ def test_reset_at_end(self):
+ i = utils.ResetableIterator([1, 2, 3])
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+
+ def test_reset_after_end(self):
+ i = utils.ResetableIterator([1, 2, 3])
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+ self.assertRaises(StopIteration, next, iterator)
+
+ i.reset()
+ # Previous iter() has stopped
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+
+ def test_reset_twice(self):
+ i = utils.ResetableIterator([1, 2, 3, 4, 5])
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+ self.assertEqual(4, next(iterator))
+
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+ self.assertEqual(4, next(iterator))
+
+ def test_reset_shorter(self):
+ i = utils.ResetableIterator([1, 2, 3, 4, 5])
+ iterator = iter(i)
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+ self.assertEqual(4, next(iterator))
+
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+
+ i.reset()
+ self.assertEqual(1, next(iterator))
+ self.assertEqual(2, next(iterator))
+ self.assertEqual(3, next(iterator))
+ self.assertEqual(4, next(iterator))
+
diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py
new file mode 100644
index 0000000..9956610
--- /dev/null
+++ b/tests/testdata/__init__.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011-2013 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 os.path
+
+TESTDATA_ROOT = os.path.abspath(os.path.dirname(__file__))
+TESTFILE_PATH = os.path.join(TESTDATA_ROOT, 'example.data')
+TESTIMAGE_PATH = os.path.join(TESTDATA_ROOT, 'example.jpeg')
diff --git a/tests/testdata/example.data b/tests/testdata/example.data
new file mode 100644
index 0000000..02ff8ec
--- /dev/null
+++ b/tests/testdata/example.data
@@ -0,0 +1 @@
+example_data
diff --git a/tests/testdata/example.jpeg b/tests/testdata/example.jpeg
new file mode 100644
index 0000000..28beea9
--- /dev/null
+++ b/tests/testdata/example.jpeg
Binary files differ
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..215fc83
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2010 Mark Sandstrom
+# Copyright (c) 2011-2013 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 datetime
+
+from .compat import mock
+from . import alter_time
+
+
+class MultiModulePatcher(object):
+ """An abstract context processor for patching multiple modules."""
+
+ def __init__(self, *target_modules, **kwargs):
+ super(MultiModulePatcher, self).__init__(**kwargs)
+ self.patchers = [self._build_patcher(mod) for mod in target_modules]
+
+ def _build_patcher(self, target_module): # pragma: no cover
+ """Build a mock patcher for the target module."""
+ raise NotImplementedError()
+
+ def __enter__(self):
+ for patcher in self.patchers:
+ mocked_symbol = patcher.start()
+
+ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
+ for patcher in self.patchers:
+ patcher.stop()
+
+
+class mocked_date_today(MultiModulePatcher):
+ """A context processor changing the value of date.today()."""
+
+ def __init__(self, target_date, *target_modules, **kwargs):
+ self.target_date = target_date
+ super(mocked_date_today, self).__init__(*target_modules, **kwargs)
+
+ def _build_patcher(self, target_module):
+ module_datetime = getattr(target_module, 'datetime')
+ return alter_time.mock_date_today(self.target_date, module_datetime)
+
+
+class mocked_datetime_now(MultiModulePatcher):
+ def __init__(self, target_dt, *target_modules, **kwargs):
+ self.target_dt = target_dt
+ super(mocked_datetime_now, self).__init__(*target_modules, **kwargs)
+
+ def _build_patcher(self, target_module):
+ module_datetime = getattr(target_module, 'datetime')
+ return alter_time.mock_datetime_now(self.target_dt, module_datetime)