summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThomas Goirand <thomas@goirand.fr>2013-05-12 05:32:34 +0000
committerThomas Goirand <thomas@goirand.fr>2013-05-12 05:32:34 +0000
commit28991f9514e3cd78a528bbbe956d9b4536c416e0 (patch)
treea3871392d2382f60490824d79058f8a71ae1c34e
parent57fa2e21aed37c1af2a87f36a998046b73092a21 (diff)
parent876845102c4a217496d0f6435bfe1e3726d31fe4 (diff)
downloadfactory-boy-28991f9514e3cd78a528bbbe956d9b4536c416e0.tar
factory-boy-28991f9514e3cd78a528bbbe956d9b4536c416e0.tar.gz
Merge tag '2.0.2' into debian/unstable
Release of factory_boy 2.0.2 Conflicts: docs/changelog.rst docs/index.rst docs/subfactory.rst tests/cyclic/bar.py tests/cyclic/foo.py
-rw-r--r--.pylintrc240
-rw-r--r--.travis.yml13
-rw-r--r--LICENSE3
-rw-r--r--Makefile32
-rw-r--r--README250
-rw-r--r--docs/conf.py10
-rw-r--r--docs/examples.rst28
-rw-r--r--docs/fuzzy.rst88
-rw-r--r--docs/ideas.rst8
-rw-r--r--docs/internals.rst27
-rw-r--r--docs/introduction.rst258
-rw-r--r--docs/orms.rst70
-rw-r--r--docs/post_generation.rst91
-rw-r--r--docs/recipes.rst234
-rw-r--r--docs/reference.rst1409
-rw-r--r--docs/subfactory.rst56
-rw-r--r--factory/__init__.py51
-rw-r--r--factory/base.py637
-rw-r--r--factory/compat.py35
-rw-r--r--factory/containers.py81
-rw-r--r--factory/declarations.py282
-rw-r--r--factory/fuzzy.py86
-rw-r--r--factory/helpers.py123
-rw-r--r--factory/utils.py5
-rwxr-xr-xsetup.py15
-rw-r--r--tests/__init__.py3
-rw-r--r--tests/compat.py12
-rw-r--r--tests/test_base.py107
-rw-r--r--tests/test_containers.py12
-rw-r--r--tests/test_declarations.py251
-rw-r--r--tests/test_fuzzy.py104
-rw-r--r--tests/test_using.py918
-rw-r--r--tests/test_utils.py2
-rw-r--r--tests/tools.py36
-rw-r--r--tox.ini8
35 files changed, 4616 insertions, 969 deletions
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..e9f43c9
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,240 @@
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+init-hook='import os, sys; sys.path.append(os.getcwd())'
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time.
+#enable=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once).
+#disable=C0103,C0111,C0302,E1002,E1101,E1102,E1103,I0011,I0013,R0201,R0801,R0901,R0902,R0903,R0904,R0912,R0914,R0915,R0921,R0923,W0108,W0212,W0232,W0141,W0142,W0401,W0613,R0924
+disable=C0103,C0111,I0011,R0201,R0903,R0922,W0142,W0212,W0232,W0613
+# see http://www.logilab.org/card/pylintfeatures
+
+
+[REPORTS]
+
+# Set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (RP0004).
+comment=no
+
+
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=__.*__
+
+
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1200
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamically set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, add a predefined set of Zope acquired attributes
+# to generated-members.
+zope=no
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E0201 when accessed.
+generated-members=REQUEST,acl_users,aq_parent
+
+
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching the beginning of the name of dummy variables
+# (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=8
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=10
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=10
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
diff --git a/.travis.yml b/.travis.yml
index aa990e6..d0735d8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,18 @@
language: python
+
python:
- "2.6"
- "2.7"
-script: "python setup.py test"
-install: "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi"
+ - "3.2"
+ - "3.3"
+ - "pypy"
+
+script:
+ - python setup.py test
+
+install:
+ - "if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi"
+
notifications:
email: false
irc: "irc.freenode.org#factory_boy"
diff --git a/LICENSE b/LICENSE
index 919f659..620dc61 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,5 @@
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
@@ -16,4 +17,4 @@ 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. \ No newline at end of file
+THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..274ee32
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,32 @@
+PACKAGE=factory
+TESTS_DIR=tests
+DOC_DIR=docs
+
+
+all: default
+
+
+default:
+
+
+clean:
+ find . -type f -name '*.pyc' -delete
+
+
+test:
+ python -W default setup.py test
+
+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"
+
+doc:
+ $(MAKE) -C $(DOC_DIR) html
+
+
+.PHONY: all default clean coverage doc pylint test
diff --git a/README b/README
index 575c3ae..4ecc415 100644
--- a/README
+++ b/README
@@ -4,43 +4,62 @@ 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>`_ . Like factory_girl it has a straightforward definition syntax, support for multiple build strategies (saved instances, unsaved instances, attribute dicts, and stubbed objects), and support for multiple factories for the same class, including factory inheritance. Django support is included, and support for other ORMs can be easily added.
+factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_.
-The official repository is at http://github.com/rbarrois/factory_boy; the documentation at http://readthedocs.org/docs/factoryboy/.
+Its features include:
-Credits
--------
+- 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)
-This README parallels the factory_girl README as much as possible; text and examples are reproduced for comparison purposes. Ruby users of factory_girl should feel right at home with factory_boy in Python.
-factory_boy was originally written by Mark Sandstrom, and improved by Raphaël Barrois.
+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.
-Thank you Joe Ferris and thoughtbot for creating factory_girl.
Download
--------
-Github: http://github.com/rbarrois/factory_boy/
+PyPI: https://pypi.python.org/pypi/factory_boy/
+
+.. code-block:: sh
+
+ $ pip install factory_boy
+
+Source: https://github.com/rbarrois/factory_boy/
-PyPI::
+.. code-block:: sh
- pip install factory_boy
+ $ git clone git://github.com/rbarrois/factory_boy/
+ $ python setup.py install
-Source::
- # Download the source and run
- 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::
import factory
- from models import User
+ from . import models
class UserFactory(factory.Factory):
- FACTORY_FOR = User
+ FACTORY_FOR = models.User
first_name = 'John'
last_name = 'Doe'
@@ -48,14 +67,15 @@ Factories declare a set of attributes used to instantiate an object. The class o
# Another, different, factory for the same object
class AdminFactory(factory.Factory):
- FACTORY_FOR = User
+ 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::
@@ -68,174 +88,140 @@ factory_boy supports several different build strategies: build, create, attribut
# Returns a dict of attributes that can be used to build a User instance
attributes = UserFactory.attributes()
- # Returns an object with all defined attributes stubbed out:
- stub = UserFactory.stub()
You can use the Factory class as a shortcut for the default build strategy::
# Same as UserFactory.create()
user = UserFactory()
-The default strategy can be overridden::
- UserFactory.default_strategy = factory.BUILD_STRATEGY
- user = UserFactory()
-
-The default strategy can also be overridden for all factories::
+No matter which strategy is used, it's possible to override the defined attributes by passing keyword arguments:
- # This will set the default strategy for all factories that don't define a default build strategy
- factory.Factory.default_strategy = factory.BUILD_STRATEGY
-
-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'
+ >>> 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 associations and other attributes that must be dynamically generated) will need values assigned each time an instance is generated. These "lazy" attributes can be added as follows::
+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())
- UserFactory().email
- # => 'joe.blow@example.com'
+.. code-block:: pycon
-The function passed to ``LazyAttribute`` is given the attributes defined for the factory up to the point of the LazyAttribute declaration. If a lambda won't cut it, the ``lazy_attribute`` decorator can be used to wrap a function::
+ >>> UserFactory().email
+ "joe.blow@example.com"
- # Stub factories don't have an associated class.
- class SumFactory(factory.StubFactory):
- lhs = 1
- rhs = 1
- @lazy_attribute
- def sum(a):
- result = a.lhs + a.rhs # Or some other fancy calculation
- return result
-
-Associations
-------------
+Sequences
+"""""""""
-Associated instances can also be generated using ``LazyAttribute``::
+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``:
- from models import Post
+.. code-block:: python
- class PostFactory(factory.Factory):
- author = factory.LazyAttribute(lambda a: UserFactory())
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+ email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n))
-The associated object's default strategy is always used::
+ >>> UserFactory().email
+ 'person0@example.com'
+ >>> UserFactory().email
+ 'person1@example.com'
- # Builds and saves a User and a Post
- post = PostFactory()
- post.id == None # => False
- post.author.id == None # => False
- # Builds and saves a User, and then builds but does not save a Post
- post = PostFactory.build()
- post.id == None # => True
- post.author.id == None # => False
+Associations
+""""""""""""
-Inheritance
------------
+Some objects have a complex field, that should itself be defined from a dedicated factories.
+This is handled by the ``SubFactory`` helper:
-You can easily create multiple factories for the same class without repeating common attributes by using inheritance::
+.. code-block:: python
class PostFactory(factory.Factory):
- title = 'A title'
+ FACTORY_FOR = models.Post
+ author = factory.SubFactory(UserFactory)
- class ApprovedPost(PostFactory):
- approved = True
- approver = factory.LazyAttribute(lambda a: UserFactory())
-Sequences
----------
+The associated object's strategy will be used:
-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``::
- class UserFactory(factory.Factory):
- email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n))
+.. code-block:: python
- UserFactory().email # => 'person0@example.com'
- UserFactory().email # => 'person1@example.com'
+ # 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
-Sequences can be combined with lazy attributes::
+ # 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
- class UserFactory(factory.Factory):
- name = 'Mark'
- email = factory.LazyAttributeSequence(lambda a, n: '{0}+{1}@example.com'.format(a.name, n).lower())
- UserFactory().email # => mark+0@example.com
+Contributing
+------------
-If you wish to use a custom method to set the initial ID for a sequence, you can override the ``_setup_next_sequence`` class method::
+factory_boy is distributed under the MIT License.
- class MyFactory(factory.Factory):
+Issues should be opened through `GitHub Issues <http://github.com/rbarrois/factory_boy/issues/>`_; whenever possible, a pull request should be included.
- @classmethod
- def _setup_next_sequence(cls):
- return cls._associated_class.objects.values_list('id').order_by('-id')[0] + 1
+All pull request should pass the test suite, which can be launched simply with:
-Customizing creation
---------------------
+.. code-block:: sh
-Sometimes, the default build/create by keyword arguments doesn't allow for enough
-customization of the generated objects. In such cases, you should override the
-Factory._prepare method::
+ $ python setup.py test
- class UserFactory(factory.Factory):
- @classmethod
- def _prepare(cls, create, **kwargs):
- password = kwargs.pop('password', None)
- user = super(UserFactory, cls)._prepare(create, **kwargs)
- if password:
- user.set_password(password)
- if create:
- user.save()
- return user
-
-Subfactories
-------------
-If one of your factories has a field which is another factory, you can declare it as a ``SubFactory``. This allows to define attributes of that field when calling
-the global factory, using a simple syntax : ``field__attr=42`` will set the attribute ``attr`` of the ``SubFactory`` defined in ``field`` to 42::
+.. note::
+
+ Running test requires the unittest2 (standard in Python 2.7+) and mock libraries.
- class InnerFactory(factory.Factory):
- foo = 'foo'
- bar = factory.LazyAttribute(lambda o: foo * 2)
- class ExternalFactory(factory.Factory):
- inner = factory.SubFactory(InnerFactory, foo='bar')
+In order to test coverage, please use:
- >>> e = ExternalFactory()
- >>> e.foo
- 'bar'
- >>> e.bar
- 'barbar'
+.. code-block:: sh
- >>> e2 : ExternalFactory(inner__bar='baz')
- >>> e2.foo
- 'bar'
- >>> e2.bar
- 'baz'
+ $ pip install coverage
+ $ coverage erase; coverage run --branch setup.py test; coverage report
-Abstract factories
-------------------
-If a ``Factory`` simply defines generic attribute declarations without being bound to a given class,
-it should be marked 'abstract' by declaring ``ABSTRACT_FACTORY = True``.
-Such factories cannot be built/created/....
+Contents, indices and tables
+----------------------------
+.. toctree::
+ :maxdepth: 2
- class AbstractFactory(factory.Factory):
- ABSTRACT_FACTORY = True
- foo = 'foo'
+ introduction
+ reference
+ orms
+ recipes
+ fuzzy
+ examples
+ internals
+ changelog
+ ideas
- >>> AbstractFactory()
- Traceback (most recent call last):
- ...
- AttributeError: type object 'AbstractFactory' has no attribute '_associated_class'
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/conf.py b/docs/conf.py
index db523c3..47630d3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -42,7 +42,7 @@ master_doc = 'index'
# General information about the project.
project = u'Factory Boy'
-copyright = u'2011, Raphaël Barrois, Mark Sandstrom'
+copyright = u'2011-2013, Raphaël Barrois, Mark Sandstrom'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
@@ -218,4 +218,10 @@ man_pages = [
# Example configuration for intersphinx: refer to the Python standard library.
-intersphinx_mapping = {'http://docs.python.org/': None}
+intersphinx_mapping = {
+ 'http://docs.python.org/': None,
+ 'django': (
+ 'http://docs.djangoproject.com/en/dev/',
+ 'http://docs.djangoproject.com/en/dev/_objects/',
+ ),
+}
diff --git a/docs/examples.rst b/docs/examples.rst
index cac6bc6..aab990a 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -7,7 +7,10 @@ Here are some real-world examples of using FactoryBoy.
Objects
-------
-First, let's define a couple of objects::
+First, let's define a couple of objects:
+
+
+.. code-block:: python
class Account(object):
def __init__(self, username, email):
@@ -41,7 +44,10 @@ First, let's define a couple of objects::
Factories
---------
-And now, we'll define the related factories::
+And now, we'll define the related factories:
+
+
+.. code-block:: python
import factory
import random
@@ -60,15 +66,18 @@ And now, we'll define the related factories::
FACTORY_FOR = objects.Profile
account = factory.SubFactory(AccountFactory)
- gender = random.choice([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE])
+ gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE])
firstname = u'John'
lastname = u'Doe'
-We have now defined basic factories for our :py:class:`~Account` and :py:class:`~Profile` classes.
+We have now defined basic factories for our :class:`~Account` and :class:`~Profile` classes.
+
+If we commonly use a specific variant of our objects, we can refine a factory accordingly:
-If we commonly use a specific variant of our objects, we can refine a factory accordingly::
+
+.. code-block:: python
class FemaleProfileFactory(ProfileFactory):
gender = objects.Profile.GENDER_FEMALE
@@ -80,7 +89,10 @@ If we commonly use a specific variant of our objects, we can refine a factory ac
Using the factories
-------------------
-We can now use our factories, for tests::
+We can now use our factories, for tests:
+
+
+.. code-block:: python
import unittest
@@ -112,7 +124,9 @@ We can now use our factories, for tests::
self.assertLess(stats.genders[objects.Profile.GENDER_FEMALE], 2)
-Or for fixtures::
+Or for fixtures:
+
+.. code-block:: python
from . import factories
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
new file mode 100644
index 0000000..f1f4085
--- /dev/null
+++ b/docs/fuzzy.rst
@@ -0,0 +1,88 @@
+Fuzzy attributes
+================
+
+.. module:: factory.fuzzy
+
+Some tests may be interested in testing with fuzzy, random values.
+
+This is handled by the :mod:`factory.fuzzy` module, which provides a few
+random declarations.
+
+
+FuzzyAttribute
+--------------
+
+
+.. class:: FuzzyAttribute
+
+ The :class:`FuzzyAttribute` uses an arbitrary callable as fuzzer.
+ It is expected that successive calls of that function return various
+ values.
+
+ .. attribute:: fuzzer
+
+ The callable that generates random values
+
+
+FuzzyChoice
+-----------
+
+
+.. class:: FuzzyChoice(choices)
+
+ The :class:`FuzzyChoice` fuzzer yields random choices from the given
+ iterable.
+
+ .. note:: The passed in :attr:`choices` will be converted into a list at
+ declaration time.
+
+ .. attribute:: choices
+
+ The list of choices to select randomly
+
+
+FuzzyInteger
+------------
+
+.. class:: FuzzyInteger(low[, high])
+
+ The :class:`FuzzyInteger` 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
+
+ >>> FuzzyInteger(0, 42)
+ >>> fi.low, fi.high
+ 0, 42
+
+ >>> fi = FuzzyInteger(42)
+ >>> fi.low, fi.high
+ 0, 42
+
+ .. attribute:: low
+
+ int, the inclusive lower bound of generated integers
+
+ .. attribute:: high
+
+ int, the inclusive higher bound of generated integers
+
+
+Custom fuzzy fields
+-------------------
+
+Alternate fuzzy fields may be defined.
+They should inherit from the :class:`BaseFuzzyAttribute` class, and override its
+:meth:`~BaseFuzzyAttribute.fuzz` method.
+
+
+.. class:: BaseFuzzyAttribute
+
+ Base class for all fuzzy attributes.
+
+ .. method:: fuzz(self)
+
+ The method responsible for generating random values.
+ *Must* be overridden in subclasses.
diff --git a/docs/ideas.rst b/docs/ideas.rst
new file mode 100644
index 0000000..914e640
--- /dev/null
+++ b/docs/ideas.rst
@@ -0,0 +1,8 @@
+Ideas
+=====
+
+
+This is a list of future features that may be incorporated into factory_boy:
+
+* **A 'options' attribute**: instead of adding more class-level constants, use a django-style ``class Meta`` Factory attribute with all options there
+
diff --git a/docs/internals.rst b/docs/internals.rst
index 279c4e9..a7402ff 100644
--- a/docs/internals.rst
+++ b/docs/internals.rst
@@ -1,25 +1,2 @@
-Factory Boy's internals
-======================
-
-
-declarations
-------------
-
-.. automodule:: factory.declarations
- :members:
-
-
-containers
-----------
-
-.. automodule:: factory.containers
- :members:
-
-
-
-base
-----
-
-.. automodule:: factory.base
- :members:
-
+Internals
+=========
diff --git a/docs/introduction.rst b/docs/introduction.rst
new file mode 100644
index 0000000..8bbb10c
--- /dev/null
+++ b/docs/introduction.rst
@@ -0,0 +1,258 @@
+Introduction
+============
+
+
+The purpose of factory_boy is to provide a default way of getting a new instance,
+while still being able to override some fields on a per-call basis.
+
+
+.. note:: This section will drive you through an overview of factory_boy's feature.
+ New users are advised to spend a few minutes browsing through this list
+ of useful helpers.
+
+ Users looking for quick helpers may take a look at :doc:`recipes`,
+ while those needing detailed documentation will be interested in the :doc:`reference` section.
+
+
+Basic usage
+-----------
+
+
+Factories declare a set of attributes used to instantiate an object, whose class is defined in the FACTORY_FOR attribute:
+
+- Subclass ``factory.Factory`` (or a more suitable subclass)
+- Set its ``FACTORY_FOR`` attribute to the target class
+- Add defaults for keyword args to pass to the associated class' ``__init__`` method
+
+
+.. code-block:: python
+
+ import factory
+ from . import base
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = base.User
+
+ firstname = "John"
+ lastname = "Doe"
+
+You may now get ``base.User`` instances trivially:
+
+.. code-block:: pycon
+
+ >>> john = UserFactory()
+ <User: John Doe>
+
+It is also possible to override the defined attributes by passing keyword arguments to the factory:
+
+.. code-block:: pycon
+
+ >>> jack = UserFactory(firstname="Jack")
+ <User: Jack Doe>
+
+
+A given class may be associated to many :class:`~factory.Factory` subclasses:
+
+.. code-block:: python
+
+ class EnglishUserFactory(factory.Factory):
+ FACTORY_FOR = base.User
+
+ firstname = "John"
+ lastname = "Doe"
+ lang = 'en'
+
+
+ class FrenchUserFactory(factory.Factory):
+ FACTORY_FOR = base.User
+
+ firstname = "Jean"
+ lastname = "Dupont"
+ lang = 'fr'
+
+
+.. code-block:: pycon
+
+ >>> EnglishUserFactory()
+ <User: John Doe (en)>
+ >>> FrenchUserFactory()
+ <User: Jean Dupont (fr)>
+
+
+Sequences
+---------
+
+When a field has a unique key, each object generated by the factory should have a different value for that field.
+This is achieved with the :class:`~factory.Sequence` declaration:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+
+ username = factory.Sequence(lambda n: 'user%d' % n)
+
+.. code-block:: pycon
+
+ >>> UserFactory()
+ <User: user1>
+ >>> UserFactory()
+ <User: user2>
+
+.. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator:
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+
+ @factory.sequence
+ def username(self, n):
+ return 'user%d' % n
+
+
+LazyAttribute
+-------------
+
+Some fields may be deduced from others, for instance the email based on the username.
+The :class:`~factory.LazyAttribute` handles such cases: it should receive a function
+taking the object being built and returning the value for the field:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = models.User
+
+ username = factory.Sequence(lambda n: 'user%d' % n)
+ email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username)
+
+.. code-block:: pycon
+
+ >>> UserFactory()
+ <User: user1 (user1@example.com)>
+
+ >>> # The LazyAttribute handles overridden fields
+ >>> UserFactory(username='john')
+ <User: john (john@example.com)>
+
+ >>> # They can be directly overridden as well
+ >>> UserFactory(email='doe@example.com')
+ <User: user3 (doe@example.com)>
+
+
+.. note:: As for :class:`~factory.Sequence`, a :meth:`~factory.@lazy_attribute` decorator is available.
+
+
+Inheritance
+-----------
+
+
+Once a "base" factory has been defined for a given class,
+alternate versions can be easily defined through subclassing.
+
+The subclassed :class:`~factory.Factory` will inherit all declarations from its parent,
+and update them with its own declarations:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = base.User
+
+ firstname = "John"
+ lastname = "Doe"
+ group = 'users'
+
+ class AdminFactory(UserFactory):
+ admin = True
+ group = 'admins'
+
+.. code-block:: pycon
+
+ >>> user = UserFactory()
+ >>> user
+ <User: John Doe>
+ >>> user.group
+ 'users'
+
+ >>> admin = AdminFactory()
+ >>> admin
+ <User: John Doe (admin)>
+ >>> admin.group # The AdminFactory field has overridden the base field
+ 'admins'
+
+
+Any argument of all factories in the chain can easily be overridden:
+
+.. code-block:: pycon
+
+ >>> super_admin = AdminFactory(group='superadmins', lastname="Lennon")
+ >>> super_admin
+ <User: John Lennon (admin)>
+ >>> super_admin.group # Overridden at call time
+ 'superadmins'
+
+
+Non-kwarg arguments
+-------------------
+
+Some classes take a few, non-kwarg arguments first.
+
+This is handled by the :data:`~factory.Factory.FACTORY_ARG_PARAMETERS` attribute:
+
+.. code-block:: python
+
+ class MyFactory(factory.Factory):
+ FACTORY_FOR = MyClass
+ FACTORY_ARG_PARAMETERS = ('x', 'y')
+
+ x = 1
+ y = 2
+ z = 3
+
+.. code-block:: pycon
+
+ >>> MyFactory(y=4)
+ <MyClass(1, 4, z=3)>
+
+
+Strategies
+----------
+
+All factories support two built-in strategies:
+
+* ``build`` provides a local object
+* ``create`` instantiates a local object, and saves it to the database.
+
+.. note:: For 1.X versions, the ``create`` will actually call ``AssociatedClass.objects.create``,
+ 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.
+
+
+When a :class:`~factory.Factory` includes related fields (:class:`~factory.SubFactory`, :class:`~factory.RelatedFactory`),
+the parent's strategy will be pushed onto related factories.
+
+
+Calling a :class:`~factory.Factory` subclass will provide an object through the default strategy:
+
+.. code-block:: python
+
+ class MyFactory(factory.Factory):
+ FACTORY_FOR = MyClass
+
+.. code-block:: pycon
+
+ >>> MyFactory.create()
+ <MyFactory: X (saved)>
+
+ >>> MyFactory.build()
+ <MyFactory: X (unsaved)>
+
+ >>> MyFactory() # equivalent to MyFactory.create()
+ <MyClass: X (saved)>
+
+
+The default strategy can ba changed by setting the class-level :attr:`~factory.Factory.FACTROY_STRATEGY` attribute.
+
+
diff --git a/docs/orms.rst b/docs/orms.rst
new file mode 100644
index 0000000..8e5b6f6
--- /dev/null
+++ b/docs/orms.rst
@@ -0,0 +1,70 @@
+Using factory_boy with ORMs
+===========================
+
+.. currentmodule:: factory
+
+
+factory_boy provides custom :class:`Factory` subclasses for various ORMs,
+adding dedicated features.
+
+
+Django
+------
+
+
+The first versions of factory_boy were designed specifically for Django,
+but the library has now evolved to be framework-independant.
+
+Most features should thus feel quite familiar to Django users.
+
+The :class:`DjangoModelFactory` subclass
+"""""""""""""""""""""""""""""""""""""""""
+
+All factories for a Django :class:`~django.db.models.Model` should use the
+:class:`DjangoModelFactory` base class.
+
+
+.. class:: DjangoModelFactory(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
+ * When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration`
+ attributes, the base object will be :meth:`saved <django.db.models.Model.save>`
+ once all post-generation hooks have run.
+
+ .. attribute:: FACTORY_DJANGO_GET_OR_CREATE
+
+ Fields whose name are passed in this list will be used to perform a
+ :meth:`Model.objects.get_or_create() <django.db.models.query.QuerySet.get_or_create>`
+ instead of the usual :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>`:
+
+ .. code-block:: python
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+ FACTORY_DJANGO_GET_OR_CREATE = ('username',)
+
+ username = 'john'
+
+ .. code-block:: pycon
+
+ >>> User.objects.all()
+ []
+ >>> UserFactory() # Creates a new user
+ <User: john>
+ >>> User.objects.all()
+ [<User: john>]
+
+ >>> UserFactory() # Fetches the existing user
+ <User: john>
+ >>> User.objects.all() # No new user!
+ [<User: john>]
+
+ >>> UserFactory(username='jack') # Creates another user
+ <User: jack>
+ >>> User.objects.all()
+ [<User: john>, <User: jack>]
diff --git a/docs/post_generation.rst b/docs/post_generation.rst
deleted file mode 100644
index d712abc..0000000
--- a/docs/post_generation.rst
+++ /dev/null
@@ -1,91 +0,0 @@
-PostGenerationHook
-==================
-
-
-Some objects expect additional method calls or complex processing for proper definition.
-For instance, a ``User`` may need to have a related ``Profile``, where the ``Profile`` is built from the ``User`` object.
-
-To support this pattern, factory_boy provides the following tools:
- - :py:class:`factory.PostGeneration`: this class allows calling a given function with the generated object as argument
- - :py:func:`factory.post_generation`: decorator performing the same functions as :py:class:`~factory.PostGeneration`
- - :py:class:`factory.RelatedFactory`: this builds or creates a given factory *after* building/creating the first Factory.
-
-
-Passing arguments to a post-generation hook
--------------------------------------------
-
-A post-generation hook will be defined with a given attribute name.
-When calling the ``Factory``, some arguments will be passed to the post-generation hook instead of being available for ``Factory`` building:
-
- - An argument with the same name as the post-generation hook attribute will be passed to the hook
- - All arguments beginning with that name and ``__`` will be passed to the hook, after removing the prefix.
-
-Example::
-
- class MyFactory(factory.Factory):
- blah = factory.PostGeneration(lambda obj, create, extracted, **kwargs: 42)
-
- MyFactory(
- blah=42, # Passed in the 'extracted' argument of the lambda
- blah__foo=1, # Passed in kwargs as 'foo': 1
- blah__baz=2, # Passed in kwargs as 'baz': 2
- blah_bar=3, # Not passed
- )
-
-The prefix used for extraction can be changed by setting the ``extract_prefix`` argument of the hook::
-
- class MyFactory(factory.Factory):
- @factory.post_generation(extract_prefix='bar')
- def foo(self, create, extracted, **kwargs):
- self.foo = extracted
-
- MyFactory(
- bar=42, # Will be passed to 'extracted'
- bar__baz=13, # Will be passed as 'baz': 13 in kwargs
- foo=2, # Won't be passed to the post-generation hook
- )
-
-
-PostGeneration and @post_generation
------------------------------------
-
-Both declarations wrap a function, which will be called with the following arguments:
- - ``obj``: the generated factory
- - ``create``: whether the factory was "built" or "created"
- - ``extracted``: if the :py:class:`~factory.PostGeneration` was declared as attribute ``foo``,
- and another value was given for ``foo`` when calling the ``Factory``,
- that value will be available in the ``extracted`` parameter.
- - other keyword args are extracted from those passed to the ``Factory`` with the same prefix as the name of the :py:class:`~factory.PostGeneration` attribute
-
-
-RelatedFactory
---------------
-
-This is declared with the following arguments:
- - ``factory``: the :py:class:`~factory.Factory` to call
- - ``name``: the keyword to use when passing this object to the related :py:class:`~factory.Factory`; if empty, the object won't be passed to the related :py:class:`~factory.Factory`
- - Extra keyword args which will be passed to the factory
-
-When the object is built, the keyword arguments passed to the related :py:class:`~factory.Factory` are:
- - ``name: obj`` if ``name`` was passed when defining the :py:class:`~factory.RelatedFactory`
- - extra keyword args defined in the :py:class:`~factory.RelatedFactory` definition, overridden by any prefixed arguments passed to the object definition
-
-
-Example::
-
- class RelatedObjectFactory(factory.Factory):
- FACTORY_FOR = RelatedObject
- one = 1
- two = 2
- related = None
-
- class ObjectWithRelatedFactory(factory.Factory):
- FACTORY_FOR = SomeObject
- foo = factory.RelatedFactory(RelatedObjectFactory, 'related', one=2)
-
- ObjectWithRelatedFactory(foo__two=3)
-
-The ``RelatedObject`` will be called with:
- - ``one=2``
- - ``two=3``
- - ``related=<SomeObject>``
diff --git a/docs/recipes.rst b/docs/recipes.rst
new file mode 100644
index 0000000..e226732
--- /dev/null
+++ b/docs/recipes.rst
@@ -0,0 +1,234 @@
+Common recipes
+==============
+
+
+.. note:: Most recipes below take on Django model examples, but can also be used on their own.
+
+
+Dependent objects (ForeignKey)
+------------------------------
+
+When one attribute is actually a complex field
+(e.g a :class:`~django.db.models.ForeignKey` to another :class:`~django.db.models.Model`),
+use the :class:`~factory.SubFactory` declaration:
+
+
+.. code-block:: python
+
+ # models.py
+ class User(models.Model):
+ first_name = models.CharField()
+ group = models.ForeignKey(Group)
+
+
+ # factories.py
+ import factory
+ from . import models
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+
+ first_name = factory.Sequence(lambda n: "Agent %03d" % n)
+ group = factory.SubFactory(GroupFactory)
+
+
+Reverse dependencies (reverse ForeignKey)
+-----------------------------------------
+
+When a related object should be created upon object creation
+(e.g a reverse :class:`~django.db.models.ForeignKey` from another :class:`~django.db.models.Model`),
+use a :class:`~factory.RelatedFactory` declaration:
+
+
+.. code-block:: python
+
+ # models.py
+ class User(models.Model):
+ pass
+
+ class UserLog(models.Model):
+ user = models.ForeignKey(User)
+ action = models.CharField()
+
+
+ # factories.py
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+
+ log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE)
+
+
+When a :class:`UserFactory` is instantiated, factory_boy will call
+``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``.
+
+
+Simple ManyToMany
+-----------------
+
+Building the adequate link between two models depends heavily on the use case;
+factory_boy doesn't provide a "all in one tools" as for :class:`~factory.SubFactory`
+or :class:`~factory.RelatedFactory`, users will have to craft their own depending
+on the model.
+
+The base building block for this feature is the :class:`~factory.post_generation`
+hook:
+
+.. code-block:: python
+
+ # models.py
+ class Group(models.Model):
+ name = models.CharField()
+
+ class User(models.Model):
+ name = models.CharField()
+ groups = models.ManyToMany(Group)
+
+
+ # factories.py
+ class GroupFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.Group
+
+ name = factory.Sequence(lambda n: "Group #%s" % n)
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+
+ name = "John Doe"
+
+ @factory.post_generation
+ def groups(self, create, extracted, **kwargs):
+ if not create:
+ # Simple build, do nothing.
+ return
+
+ if extracted:
+ # A list of groups were passed in, use them
+ for group in extracted:
+ self.groups.add(group)
+
+.. OHAI_VIM**
+
+When calling ``UserFactory()`` or ``UserFactory.build()``, no group binding
+will be created.
+
+But when ``UserFactory.create(groups=(group1, group2, group3))`` is called,
+the ``groups`` declaration will add passed in groups to the set of groups for the
+user.
+
+
+ManyToMany with a 'through'
+---------------------------
+
+
+If only one link is required, this can be simply performed with a :class:`RelatedFactory`.
+If more links are needed, simply add more :class:`RelatedFactory` declarations:
+
+.. code-block:: python
+
+ # models.py
+ class User(models.Model):
+ name = models.CharField()
+
+ class Group(models.Model):
+ name = models.CharField()
+ members = models.ManyToMany(User, through='GroupLevel')
+
+ class GroupLevel(models.Model):
+ user = models.ForeignKey(User)
+ group = models.ForeignKey(Group)
+ rank = models.IntegerField()
+
+
+ # factories.py
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+
+ name = "John Doe"
+
+ class GroupFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.Group
+
+ name = "Admins"
+
+ class GroupLevelFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.GroupLevel
+
+ user = factory.SubFactory(UserFactory)
+ group = factory.SubFactory(GroupFactory)
+ rank = 1
+
+ class UserWithGroupFactory(UserFactory):
+ membership = factory.RelatedFactory(GroupLevelFactory, 'user')
+
+ class UserWith2GroupsFactory(UserFactory):
+ membership1 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group1')
+ membership2 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group2')
+
+
+Whenever the ``UserWithGroupFactory`` is called, it will, as a post-generation hook,
+call the ``GroupLevelFactory``, passing the generated user as a ``user`` field:
+
+1. ``UserWithGroupFactory()`` generates a ``User`` instance, ``obj``
+2. It calls ``GroupLevelFactory(user=obj)``
+3. It returns ``obj``
+
+
+When using the ``UserWith2GroupsFactory``, that behavior becomes:
+
+1. ``UserWith2GroupsFactory()`` generates a ``User`` instance, ``obj``
+2. It calls ``GroupLevelFactory(user=obj, group__name='Group1')``
+3. It calls ``GroupLevelFactory(user=obj, group__name='Group2')``
+4. It returns ``obj``
+
+
+Copying fields to a SubFactory
+------------------------------
+
+When a field of a related class should match one of the container:
+
+
+.. code-block:: python
+
+ # models.py
+ class Country(models.Model):
+ name = models.CharField()
+ lang = models.CharField()
+
+ class User(models.Model):
+ name = models.CharField()
+ lang = models.CharField()
+ country = models.ForeignKey(Country)
+
+ class Company(models.Model):
+ name = models.CharField()
+ owner = models.ForeignKey(User)
+ country = models.ForeignKey(Country)
+
+
+Here, we want:
+
+- The User to have the lang of its country (``factory.SelfAttribute('country.lang')``)
+- The Company owner to live in the country of the company (``factory.SelfAttribute('..country')``)
+
+.. code-block:: python
+
+ # factories.py
+ class CountryFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.Country
+
+ name = factory.Iterator(["France", "Italy", "Spain"])
+ lang = factory.Iterator(['fr', 'it', 'es'])
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+
+ name = "John"
+ lang = factory.SelfAttribute('country.lang')
+ country = factory.SubFactory(CountryFactory)
+
+ class CompanyFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.Company
+
+ name = "ACME, Inc."
+ country = factory.SubFactory(CountryFactory)
+ owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country'))
diff --git a/docs/reference.rst b/docs/reference.rst
new file mode 100644
index 0000000..81aa645
--- /dev/null
+++ b/docs/reference.rst
@@ -0,0 +1,1409 @@
+Reference
+=========
+
+.. currentmodule:: factory
+
+This section offers an in-depth description of factory_boy features.
+
+For internals and customization points, please refer to the :doc:`internals` section.
+
+
+The :class:`Factory` class
+--------------------------
+
+.. class:: Factory
+
+ The :class:`Factory` class is the base of factory_boy features.
+
+ It accepts a few specific attributes (must be specified on class declaration):
+
+ .. 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`.
+
+ .. attribute:: ABSTRACT_FACTORY
+
+ This attribute indicates that the :class:`Factory` subclass should not
+ be used to generate objects, but instead provides some extra defaults.
+
+ .. attribute:: FACTORY_ARG_PARAMETERS
+
+ Some factories require non-keyword arguments to their :meth:`~object.__init__`.
+ They should be listed, in order, in the :attr:`FACTORY_ARG_PARAMETERS`
+ attribute:
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+ FACTORY_ARG_PARAMETERS = ('login', 'email')
+
+ login = 'john'
+ email = factory.LazyAttribute(lambda o: '%s@example.com' % o.login)
+ firstname = "John"
+
+ .. code-block:: pycon
+
+ >>> UserFactory()
+ <User: john>
+ >>> User('john', 'john@example.com', firstname="John") # actual call
+
+ .. attribute:: FACTORY_HIDDEN_ARGS
+
+ While writing a :class:`Factory` for some object, it may be useful to
+ have general fields helping defining others, but that should not be
+ passed to the target class; for instance, a field named 'now' that would
+ hold a reference time used by other objects.
+
+ Factory fields whose name are listed in :attr:`FACTORY_HIDDEN_ARGS` will
+ be removed from the set of args/kwargs passed to the underlying class;
+ they can be any valid factory_boy declaration:
+
+ .. code-block:: python
+
+ class OrderFactory(factory.Factory):
+ FACTORY_FOR = Order
+ FACTORY_HIDDEN_ARGS = ('now',)
+
+ now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow())
+ started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1))
+ paid_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=50))
+
+ .. code-block:: pycon
+
+ >>> OrderFactory() # The value of 'now' isn't passed to Order()
+ <Order: started 2013-04-01 12:00:00, paid 2013-04-01 12:10:00>
+
+ >>> # An alternate value may be passed for 'now'
+ >>> OrderFactory(now=datetime.datetime(2013, 4, 1, 10))
+ <Order: started 2013-04-01 09:00:00, paid 2013-04-01 09:10:00>
+
+
+ **Base functions:**
+
+ The :class:`Factory` class provides a few methods for getting objects;
+ the usual way being to simply call the class:
+
+ .. code-block:: pycon
+
+ >>> UserFactory() # Calls UserFactory.create()
+ >>> UserFactory(login='john') # Calls UserFactory.create(login='john')
+
+ Under the hood, factory_boy will define the :class:`Factory`
+ :meth:`~object.__new__` method to call the default :ref:`strategy <strategies>`
+ of the :class:`Factory`.
+
+
+ A specific strategy for getting instance can be selected by calling the
+ adequate method:
+
+ .. classmethod:: build(cls, **kwargs)
+
+ Provides a new object, using the 'build' strategy.
+
+ .. classmethod:: build_batch(cls, size, **kwargs)
+
+ Provides a list of :obj:`size` instances from the :class:`Factory`,
+ through the 'build' strategy.
+
+
+ .. classmethod:: create(cls, **kwargs)
+
+ Provides a new object, using the 'create' strategy.
+
+ .. classmethod:: create_batch(cls, size, **kwargs)
+
+ Provides a list of :obj:`size` instances from the :class:`Factory`,
+ through the 'create' strategy.
+
+
+ .. classmethod:: stub(cls, **kwargs)
+
+ Provides a new stub
+
+ .. classmethod:: stub_batch(cls, size, **kwargs)
+
+ Provides a list of :obj:`size` stubs from the :class:`Factory`.
+
+
+ .. classmethod:: generate(cls, strategy, **kwargs)
+
+ Provide a new instance, with the provided :obj:`strategy`.
+
+ .. classmethod:: generate_batch(cls, strategy, size, **kwargs)
+
+ Provides a list of :obj:`size` instances using the specified strategy.
+
+
+ .. classmethod:: simple_generate(cls, create, **kwargs)
+
+ Provide a new instance, either built (``create=False``) or created (``create=True``).
+
+ .. classmethod:: simple_generate_batch(cls, create, size, **kwargs)
+
+ Provides a list of :obj:`size` instances, either built or created
+ according to :obj:`create`.
+
+
+ **Extension points:**
+
+ A :class:`Factory` subclass may override a couple of class methods to adapt
+ its behaviour:
+
+ .. classmethod:: _adjust_kwargs(cls, **kwargs)
+
+ .. OHAI_VIM**
+
+ The :meth:`_adjust_kwargs` extension point allows for late fields tuning.
+
+ It is called once keyword arguments have been resolved and post-generation
+ items removed, but before the :attr:`FACTORY_ARG_PARAMETERS` extraction
+ phase.
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+
+ @classmethod
+ def _adjust_kwargs(cls, **kwargs):
+ # Ensure ``lastname`` is upper-case.
+ kwargs['lastname'] = kwargs['lastname'].upper()
+ return kwargs
+
+ .. OHAI_VIM**
+
+
+ .. classmethod:: _setup_next_sequence(cls)
+
+ This method will compute the first value to use for the sequence counter
+ of this factory.
+
+ It is called when the first instance of the factory (or one of its subclasses)
+ is created.
+
+ Subclasses may fetch the next free ID from the database, for instance.
+
+
+ .. classmethod:: _build(cls, target_class, *args, **kwargs)
+
+ .. OHAI_VIM*
+
+ This class method is called whenever a new instance needs to be built.
+ It receives the target class (provided to :attr:`FACTORY_FOR`), and
+ the positional and keyword arguments to use for the class once all has
+ been computed.
+
+ Subclasses may override this for custom APIs.
+
+
+ .. classmethod:: _create(cls, target_class, *args, **kwargs)
+
+ .. OHAI_VIM*
+
+ The :meth:`_create` method is called whenever an instance needs to be
+ created.
+ It receives the same arguments as :meth:`_build`.
+
+ Subclasses may override this for specific persistence backends:
+
+ .. code-block:: python
+
+ class BaseBackendFactory(factory.Factory):
+ ABSTRACT_FACTORY = True
+
+ def _create(cls, target_class, *args, **kwargs):
+ obj = target_class(*args, **kwargs)
+ obj.save()
+ return obj
+
+ .. OHAI_VIM*
+
+ .. classmethod:: _after_postgeneration(cls, obj, create, results=None)
+
+ :arg object obj: The object just generated
+ :arg bool create: Whether the object was 'built' or 'created'
+ :arg dict results: Map of post-generation declaration name to call
+ result
+
+ The :meth:`_after_postgeneration` is called once post-generation
+ declarations have been handled.
+
+ Its arguments allow to handle specifically some post-generation return
+ values, for instance.
+
+
+.. _strategies:
+
+Strategies
+""""""""""
+
+factory_boy supports two main strategies for generating instances, plus stubs.
+
+
+.. data:: BUILD_STRATEGY
+
+ The 'build' strategy is used when an instance should be created,
+ but not persisted to any datastore.
+
+ It is usually a simple call to the :meth:`~object.__init__` method of the
+ :attr:`~Factory.FACTORY_FOR` class.
+
+
+.. data:: CREATE_STRATEGY
+
+ The 'create' strategy builds and saves an instance into its appropriate datastore.
+
+ This is the default strategy of factory_boy; it would typically instantiate an
+ object, then save it:
+
+ .. code-block:: pycon
+
+ >>> obj = self._associated_class(*args, **kwargs)
+ >>> obj.save()
+ >>> return obj
+
+ .. OHAI_VIM*
+
+ .. warning:: For backward compatibility reasons, the default behaviour of
+ factory_boy is to call ``MyClass.objects.create(*args, **kwargs)``
+ when using the ``create`` strategy.
+
+ That policy will be used if the
+ :attr:`associated class <Factory.FACTORY_FOR>` has an ``objects``
+ attribute *and* the :meth:`~Factory._create` classmethod of the
+ :class:`Factory` wasn't overridden.
+
+
+.. function:: use_strategy(strategy)
+
+ *Decorator*
+
+ Change the default strategy of the decorated :class:`Factory` to the chosen :obj:`strategy`:
+
+ .. code-block:: python
+
+ @use_strategy(factory.BUILD_STRATEGY)
+ class UserBuildingFactory(UserFactory):
+ pass
+
+
+.. data:: STUB_STRATEGY
+
+ The 'stub' strategy is an exception in the factory_boy world: it doesn't return
+ an instance of the :attr:`~Factory.FACTORY_FOR` class, and actually doesn't
+ require one to be present.
+
+ Instead, it returns an instance of :class:`StubObject` whose attributes have been
+ set according to the declarations.
+
+
+.. class:: StubObject(object)
+
+ A plain, stupid object. No method, no helpers, simply a bunch of attributes.
+
+ It is typically instantiated, then has its attributes set:
+
+ .. code-block:: pycon
+
+ >>> obj = StubObject()
+ >>> obj.x = 1
+ >>> obj.y = 2
+
+
+.. class:: StubFactory(Factory)
+
+ An :attr:`abstract <Factory.ABSTRACT_FACTORY>` :class:`Factory`,
+ with a default strategy set to :data:`STUB_STRATEGY`.
+
+
+.. _declarations:
+
+Declarations
+------------
+
+LazyAttribute
+"""""""""""""
+
+.. class:: LazyAttribute(method_to_call)
+
+The :class:`LazyAttribute` is a simple yet extremely powerful building brick
+for extending a :class:`Factory`.
+
+It takes as argument a method to call (usually a lambda); that method should
+accept the object being built as sole argument, and return a value.
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ username = 'john'
+ email = factory.LazyAttribute(lambda o: '%s@example.com' % o.username)
+
+.. code-block:: pycon
+
+ >>> u = UserFactory()
+ >>> u.email
+ 'john@example.com'
+
+ >>> u = UserFactory(username='leo')
+ >>> u.email
+ 'leo@example.com'
+
+
+Decorator
+~~~~~~~~~
+
+.. function:: lazy_attribute
+
+If a simple lambda isn't enough, you may use the :meth:`lazy_attribute` decorator instead.
+
+This decorates an instance method that should take a single argument, ``self``;
+the name of the method will be used as the name of the attribute to fill with the
+return value of the method:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory)
+ FACTORY_FOR = User
+
+ name = u"Jean"
+
+ @factory.lazy_attribute
+ def email(self):
+ # Convert to plain ascii text
+ clean_name = (unicodedata.normalize('NFKD', self.name)
+ .encode('ascii', 'ignore')
+ .decode('utf8'))
+ return u'%s@example.com' % clean_name
+
+.. code-block:: pycon
+
+ >>> joel = UserFactory(name=u"Joël")
+ >>> joel.email
+ u'joel@example.com'
+
+
+Sequence
+""""""""
+
+.. class:: Sequence(lambda, type=int)
+
+If a field should be unique, and thus different for all built instances,
+use a :class:`Sequence`.
+
+This declaration takes a single argument, a function accepting a single parameter
+- the current sequence counter - and returning the related value.
+
+
+.. note:: An extra kwarg argument, ``type``, may be provided.
+ This feature is deprecated in 1.3.0 and will be removed in 2.0.0.
+
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory)
+ FACTORY_FOR = User
+
+ phone = factory.Sequence(lambda n: '123-555-%04d' % n)
+
+.. code-block:: pycon
+
+ >>> UserFactory().phone
+ '123-555-0001'
+ >>> UserFactory().phone
+ '123-555-0002'
+
+
+Decorator
+~~~~~~~~~
+
+.. function:: sequence
+
+As with :meth:`lazy_attribute`, a decorator is available for complex situations.
+
+:meth:`sequence` decorates an instance method, whose ``self`` method will actually
+be the sequence counter - this might be confusing:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory)
+ FACTORY_FOR = User
+
+ @factory.sequence
+ def phone(n):
+ a = n // 10000
+ b = n % 10000
+ return '%03d-555-%04d' % (a, b)
+
+.. code-block:: pycon
+
+ >>> UserFactory().phone
+ '000-555-9999'
+ >>> UserFactory().phone
+ '001-555-0000'
+
+
+Sharing
+~~~~~~~
+
+The sequence counter is shared across all :class:`Sequence` attributes of the
+:class:`Factory`:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ phone = factory.Sequence(lambda n: '%04d' % n)
+ office = factory.Sequence(lambda n: 'A23-B%03d' % n)
+
+.. code-block:: pycon
+
+ >>> u = UserFactory()
+ >>> u.phone, u.office
+ '0041', 'A23-B041'
+ >>> u2 = UserFactory()
+ >>> u2.phone, u2.office
+ '0042', 'A23-B042'
+
+
+Inheritance
+~~~~~~~~~~~
+
+When a :class:`Factory` inherits from another :class:`Factory`, their
+sequence counter is shared:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ phone = factory.Sequence(lambda n: '123-555-%04d' % n)
+
+
+ class EmployeeFactory(UserFactory):
+ office_phone = factory.Sequence(lambda n: '%04d' % n)
+
+.. code-block:: pycon
+
+ >>> u = UserFactory()
+ >>> u.phone
+ '123-555-0001'
+
+ >>> e = EmployeeFactory()
+ >>> e.phone, e.office_phone
+ '123-555-0002', '0002'
+
+ >>> u2 = UserFactory()
+ >>> u2.phone
+ '123-555-0003'
+
+
+Forcing a sequence counter
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If a specific value of the sequence counter is required for one instance, the
+``__sequence`` keyword argument should be passed to the factory method.
+
+This will force the sequence counter during the call, without altering the
+class-level value.
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ uid = factory.Sequence(int)
+
+.. code-block:: pycon
+
+ >>> UserFactory()
+ <User: 0>
+ >>> UserFactory()
+ <User: 1>
+ >>> UserFactory(__sequence=42)
+ <User: 42>
+
+
+.. warning:: The impact of setting ``__sequence=n`` on a ``_batch`` call is
+ undefined. Each generated instance may share a same counter, or
+ use incremental values starting from the forced value.
+
+
+LazyAttributeSequence
+"""""""""""""""""""""
+
+.. class:: LazyAttributeSequence(method_to_call)
+
+The :class:`LazyAttributeSequence` declaration merges features of :class:`Sequence`
+and :class:`LazyAttribute`.
+
+It takes a single argument, a function whose two parameters are, in order:
+
+* The object being built
+* The sequence counter
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ login = 'john'
+ email = factory.LazyAttributeSequence(lambda o, n: '%s@s%d.example.com' % (o.login, n))
+
+.. code-block:: pycon
+
+ >>> UserFactory().email
+ 'john@s1.example.com'
+ >>> UserFactory(login='jack').email
+ 'jack@s2.example.com'
+
+
+Decorator
+~~~~~~~~~
+
+.. function:: lazy_attribute_sequence(method_to_call)
+
+As for :meth:`lazy_attribute` and :meth:`sequence`, the :meth:`lazy_attribute_sequence`
+handles more complex cases:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ login = 'john'
+
+ @lazy_attribute_sequence
+ def email(self, n):
+ bucket = n % 10
+ return '%s@s%d.example.com' % (self.login, bucket)
+
+
+SubFactory
+""""""""""
+
+.. class:: SubFactory(sub_factory, **kwargs)
+
+ .. OHAI_VIM**
+
+This attribute declaration calls another :class:`Factory` subclass,
+selecting the same build strategy and collecting extra kwargs in the process.
+
+The :class:`SubFactory` attribute should be called with:
+
+* A :class:`Factory` subclass as first argument, or the fully qualified import
+ path to that :class:`Factory` (see :ref:`Circular imports <subfactory-circular>`)
+* An optional set of keyword arguments that should be passed when calling that
+ factory
+
+
+Definition
+~~~~~~~~~~
+
+.. code-block:: python
+
+
+ # A standard factory
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ # Various fields
+ first_name = 'John'
+ last_name = factory.Sequence(lambda n: 'D%se' % ('o' * n)) # De, Doe, Dooe, Doooe, ...
+ email = factory.LazyAttribute(lambda o: '%s.%s@example.org' % (o.first_name.lower(), o.last_name.lower()))
+
+ # A factory for an object with a 'User' field
+ class CompanyFactory(factory.Factory):
+ FACTORY_FOR = Company
+
+ name = factory.Sequence(lambda n: 'FactoryBoyz' + 'z' * n)
+
+ # Let's use our UserFactory to create that user, and override its first name.
+ owner = factory.SubFactory(UserFactory, first_name='Jack')
+
+
+Calling
+~~~~~~~
+
+The wrapping factory will call of the inner factory:
+
+.. code-block:: pycon
+
+ >>> c = CompanyFactory()
+ >>> c
+ <Company: FactoryBoyz>
+
+ # Notice that the first_name was overridden
+ >>> c.owner
+ <User: Jack De>
+ >>> c.owner.email
+ jack.de@example.org
+
+
+Fields of the :class:`~factory.SubFactory` may be overridden from the external factory:
+
+.. code-block:: pycon
+
+ >>> c = CompanyFactory(owner__first_name='Henry')
+ >>> c.owner
+ <User: Henry Doe>
+
+ # Notice that the updated first_name was propagated to the email LazyAttribute.
+ >>> c.owner.email
+ henry.doe@example.org
+
+ # It is also possible to override other fields of the SubFactory
+ >>> c = CompanyFactory(owner__last_name='Jones')
+ >>> c.owner
+ <User: Henry Jones>
+ >>> c.owner.email
+ henry.jones@example.org
+
+
+Strategies
+~~~~~~~~~~
+
+The strategy chosen for the external factory will be propagated to all subfactories:
+
+.. code-block:: pycon
+
+ >>> c = CompanyFactory()
+ >>> c.pk # Saved to the database
+ 3
+ >>> c.owner.pk # Saved to the database
+ 8
+
+ >>> c = CompanyFactory.build()
+ >>> c.pk # Not saved
+ None
+ >>> c.owner.pk # Not saved either
+ None
+
+
+.. _subfactory-circular:
+
+Circular imports
+~~~~~~~~~~~~~~~~
+
+Some factories may rely on each other in a circular manner.
+This issue can be handled by passing the absolute import path to the target
+:class:`Factory` to the :class:`SubFactory`.
+
+.. versionadded:: 1.3.0
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ username = 'john'
+ main_group = factory.SubFactory('users.factories.GroupFactory')
+
+ class GroupFactory(factory.Factory):
+ FACTORY_FOR = Group
+
+ name = "MyGroup"
+ owner = factory.SubFactory(UserFactory)
+
+
+Obviously, such circular relationships require careful handling of loops:
+
+.. code-block:: pycon
+
+ >>> owner = UserFactory(main_group=None)
+ >>> UserFactory(main_group__owner=owner)
+ <john (group: MyGroup)>
+
+
+SelfAttribute
+"""""""""""""
+
+.. class:: SelfAttribute(dotted_path_to_attribute)
+
+Some fields should reference another field of the object being constructed, or an attribute thereof.
+
+This is performed by the :class:`~factory.SelfAttribute` declaration.
+That declaration takes a single argument, a dot-delimited path to the attribute to fetch:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory)
+ FACTORY_FOR = User
+
+ birthdate = factory.Sequence(lambda n: datetime.date(2000, 1, 1) + datetime.timedelta(days=n))
+ birthmonth = factory.SelfAttribute('birthdate.month')
+
+.. code-block:: pycon
+
+ >>> u = UserFactory()
+ >>> u.birthdate
+ date(2000, 3, 15)
+ >>> u.birthmonth
+ 3
+
+
+Parents
+~~~~~~~
+
+When used in conjunction with :class:`~factory.SubFactory`, the :class:`~factory.SelfAttribute`
+gains an "upward" semantic through the double-dot notation, as used in Python imports.
+
+``factory.SelfAttribute('..country.language')`` means
+"Select the ``language`` of the ``country`` of the :class:`~factory.Factory` calling me".
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ language = 'en'
+
+
+ class CompanyFactory(factory.Factory):
+ FACTORY_FOR = Company
+
+ country = factory.SubFactory(CountryFactory)
+ owner = factory.SubFactory(UserFactory, language=factory.SelfAttribute('..country.language'))
+
+.. code-block:: pycon
+
+ >>> company = CompanyFactory()
+ >>> company.country.language
+ 'fr'
+ >>> company.owner.language
+ 'fr'
+
+Obviously, this "follow parents" hability also handles overriding some attributes on call:
+
+.. code-block:: pycon
+
+ >>> company = CompanyFactory(country=china)
+ >>> company.owner.language
+ 'cn'
+
+
+Iterator
+""""""""
+
+.. class:: Iterator(iterable, cycle=True, getter=None)
+
+ The :class:`Iterator` declaration takes succesive values from the given
+ iterable. When it is exhausted, it starts again from zero (unless ``cycle=False``).
+
+ .. attribute:: cycle
+
+ The ``cycle`` argument is only useful for advanced cases, where the provided
+ iterable has no end (as wishing to cycle it means storing values in memory...).
+
+ .. versionadded:: 1.3.0
+ The ``cycle`` argument is available as of v1.3.0; previous versions
+ had a behaviour equivalent to ``cycle=False``.
+
+ .. attribute:: getter
+
+ A custom function called on each value returned by the iterable.
+ See the :ref:`iterator-getter` section for details.
+
+ .. versionadded:: 1.3.0
+
+Each call to the factory will receive the next value from the iterable:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory)
+ lang = factory.Iterator(['en', 'fr', 'es', 'it', 'de'])
+
+.. code-block:: pycon
+
+ >>> UserFactory().lang
+ 'en'
+ >>> UserFactory().lang
+ 'fr'
+
+
+When a value is passed in for the argument, the iterator will *not* be advanced:
+
+.. code-block:: pycon
+
+ >>> UserFactory().lang
+ 'en'
+ >>> UserFactory(lang='cn').lang
+ 'cn'
+ >>> UserFactory().lang
+ 'fr'
+
+.. _iterator-getter:
+
+Getter
+~~~~~~
+
+Some situations may reuse an existing iterable, using only some component.
+This is handled by the :attr:`~Iterator.getter` attribute: this is a function
+that accepts as sole parameter a value from the iterable, and returns an
+adequate value.
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ # CATEGORY_CHOICES is a list of (key, title) tuples
+ category = factory.Iterator(User.CATEGORY_CHOICES, getter=lambda c: c[0])
+
+
+Decorator
+~~~~~~~~~
+
+.. function:: iterator(func)
+
+
+When generating items of the iterator gets too complex for a simple list comprehension,
+use the :func:`iterator` decorator:
+
+.. warning:: The decorated function takes **no** argument,
+ notably no ``self`` parameter.
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ @factory.iterator
+ def name():
+ with open('test/data/names.dat', 'r') as f:
+ for line in f:
+ yield line
+
+
+Dict and List
+"""""""""""""
+
+When a factory expects lists or dicts as arguments, such values can be generated
+through the whole range of factory_boy declarations,
+with the :class:`Dict` and :class:`List` attributes:
+
+.. class:: Dict(params[, dict_factory=factory.DictFactory])
+
+ The :class:`Dict` class is used for dict-like attributes.
+ It receives as non-keyword argument a dictionary of fields to define, whose
+ value may be any factory-enabled declarations:
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ is_superuser = False
+ roles = factory.Dict({
+ 'role1': True,
+ 'role2': False,
+ 'role3': factory.Iterator([True, False]),
+ 'admin': factory.SelfAttribute('..is_superuser'),
+ })
+
+ .. note:: Declarations used as a :class:`Dict` values are evaluated within
+ that :class:`Dict`'s context; this means that you must use
+ the ``..foo`` syntax to access fields defined at the factory level.
+
+ On the other hand, the :class:`Sequence` counter is aligned on the
+ containing factory's one.
+
+
+ The :class:`Dict` behaviour can be tuned through the following parameters:
+
+ .. attribute:: dict_factory
+
+ The actual factory to use for generating the dict can be set as a keyword
+ argument, if an exotic dictionary-like object (SortedDict, ...) is required.
+
+
+.. class:: List(items[, list_factory=factory.ListFactory])
+
+ The :class:`List` can be used for list-like attributes.
+
+ Internally, the fields are converted into a ``index=value`` dict, which
+ makes it possible to override some values at use time:
+
+ .. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ flags = factory.List([
+ 'user',
+ 'active',
+ 'admin',
+ ])
+
+ .. code-block:: pycon
+
+ >>> u = UserFactory(flags__2='superadmin')
+ >>> u.flags
+ ['user', 'active', 'superadmin']
+
+
+ The :class:`List` behaviour can be tuned through the following parameters:
+
+ .. attribute:: list_factory
+
+ The actual factory to use for generating the list can be set as a keyword
+ argument, if another type (tuple, set, ...) is required.
+
+
+Post-generation hooks
+"""""""""""""""""""""
+
+Some objects expect additional method calls or complex processing for proper definition.
+For instance, a ``User`` may need to have a related ``Profile``, where the ``Profile`` is built from the ``User`` object.
+
+To support this pattern, factory_boy provides the following tools:
+ - :class:`PostGenerationMethodCall`: allows you to hook a particular attribute to a function call
+ - :class:`PostGeneration`: this class allows calling a given function with the generated object as argument
+ - :func:`post_generation`: decorator performing the same functions as :class:`PostGeneration`
+ - :class:`RelatedFactory`: this builds or creates a given factory *after* building/creating the first Factory.
+
+
+Extracting parameters
+"""""""""""""""""""""
+
+All post-building hooks share a common base for picking parameters from the
+set of attributes passed to the :class:`Factory`.
+
+For instance, a :class:`PostGeneration` hook is declared as ``post``:
+
+.. code-block:: python
+
+ class SomeFactory(factory.Factory):
+ FACTORY_FOR = SomeObject
+
+ @post_generation
+ def post(self, create, extracted, **kwargs):
+ obj.set_origin(create)
+
+.. OHAI_VIM**
+
+
+When calling the factory, some arguments will be extracted for this method:
+
+- If a ``post`` argument is passed, it will be passed as the ``extracted`` field
+- Any argument starting with ``post__XYZ`` will be extracted, its ``post__`` prefix
+ removed, and added to the kwargs passed to the post-generation hook.
+
+Extracted arguments won't be passed to the :attr:`~Factory.FACTORY_FOR` class.
+
+Thus, in the following call:
+
+.. code-block:: pycon
+
+ >>> SomeFactory(
+ post=1,
+ post_x=2,
+ post__y=3,
+ post__z__t=42,
+ )
+
+The ``post`` hook will receive ``1`` as ``extracted`` and ``{'y': 3, 'z__t': 42}``
+as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory.FACTORY_FOR``.
+
+
+RelatedFactory
+""""""""""""""
+
+.. class:: RelatedFactory(factory, name='', **kwargs)
+
+ .. OHAI_VIM**
+
+ A :class:`RelatedFactory` behaves mostly like a :class:`SubFactory`,
+ with the main difference that the related :class:`Factory` will be generated
+ *after* the base :class:`Factory`.
+
+
+ .. attribute:: factory
+
+ As for :class:`SubFactory`, the :attr:`factory` argument can be:
+
+ - A :class:`Factory` subclass
+ - Or the fully qualified path to a :class:`Factory` subclass
+ (see :ref:`subfactory-circular` for details)
+
+ .. attribute:: name
+
+ The generated object (where the :class:`RelatedFactory` attribute will
+ set) may be passed to the related factory if the :attr:`name` parameter
+ is set.
+
+ It will be passed as a keyword argument, using the :attr:`name` value as
+ keyword:
+
+
+.. code-block:: python
+
+ class CityFactory(factory.Factory):
+ FACTORY_FOR = City
+
+ capital_of = None
+ name = "Toronto"
+
+ class CountryFactory(factory.Factory):
+ FACTORY_FOR = Country
+
+ lang = 'fr'
+ capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris")
+
+.. code-block:: pycon
+
+ >>> france = CountryFactory()
+ >>> City.objects.get(capital_of=france)
+ <City: Paris>
+
+
+Extra kwargs may be passed to the related factory, through the usual ``ATTR__SUBATTR`` syntax:
+
+.. code-block:: pycon
+
+ >>> england = CountryFactory(lang='en', capital_city__name="London")
+ >>> City.objects.get(capital_of=england)
+ <City: London>
+
+
+PostGeneration
+""""""""""""""
+
+.. class:: PostGeneration(callable)
+
+The :class:`PostGeneration` declaration performs actions once the target object
+has been generated.
+
+Its sole argument is a callable, that will be called once the base object has
+ been generated.
+
+Once the base object has been generated, the provided callable will be called
+as ``callable(obj, create, extracted, **kwargs)``, where:
+
+- ``obj`` is the base object previously generated
+- ``create`` is a boolean indicating which strategy was used
+- ``extracted`` is ``None`` unless a value was passed in for the
+ :class:`PostGeneration` declaration at :class:`Factory` declaration time
+- ``kwargs`` are any extra parameters passed as ``attr__key=value`` when calling
+ the :class:`Factory`:
+
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ login = 'john'
+ make_mbox = factory.PostGeneration(
+ lambda obj, create, extracted, **kwargs: os.makedirs(obj.login))
+
+.. OHAI_VIM**
+
+Decorator
+~~~~~~~~~
+
+.. function:: post_generation
+
+A decorator is also provided, decorating a single method accepting the same
+``obj``, ``created``, ``extracted`` and keyword arguments as :class:`PostGeneration`.
+
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ login = 'john'
+
+ @factory.post_generation
+ def mbox(self, create, extracted, **kwargs):
+ if not create:
+ return
+ path = extracted or os.path.join('/tmp/mbox/', self.login)
+ os.path.makedirs(path)
+ return path
+
+.. OHAI_VIM**
+
+.. code-block:: pycon
+
+ >>> UserFactory.build() # Nothing was created
+ >>> UserFactory.create() # Creates dir /tmp/mbox/john
+ >>> UserFactory.create(login='jack') # Creates dir /tmp/mbox/jack
+ >>> UserFactory.create(mbox='/tmp/alt') # Creates dir /tmp/alt
+
+
+PostGenerationMethodCall
+""""""""""""""""""""""""
+
+.. class:: PostGenerationMethodCall(method_name, *args, **kwargs)
+
+ .. OHAI_VIM*
+
+ The :class:`PostGenerationMethodCall` declaration will call a method on
+ the generated object just after instantiation. This declaration class
+ provides a friendly means of generating attributes of a factory instance
+ during initialization. The declaration is created using the following arguments:
+
+ .. attribute:: method_name
+
+ The name of the method to call on the :attr:`~Factory.FACTORY_FOR` object
+
+ .. attribute:: args
+
+ The default set of unnamed arguments to pass to the method given in
+ :attr:`method_name`
+
+ .. attribute:: kwargs
+
+ The default set of keyword arguments to pass to the method given in
+ :attr:`method_name`
+
+Once the factory instance has been generated, the method specified in
+:attr:`~PostGenerationMethodCall.method_name` will be called on the generated object
+with any arguments specified in the :class:`PostGenerationMethodCall` declaration, by
+default.
+
+For example, to set a default password on a generated User instance
+during instantiation, we could make a declaration for a ``password``
+attribute like below:
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ username = 'user'
+ password = factory.PostGenerationMethodCall('set_password',
+ 'defaultpassword')
+
+When we instantiate a user from the ``UserFactory``, the factory
+will create a password attribute by calling ``User.set_password('defaultpassword')``.
+Thus, by default, our users will have a password set to ``'defaultpassword'``.
+
+.. code-block:: pycon
+
+ >>> u = UserFactory() # Calls user.set_password('defaultpassword')
+ >>> u.check_password('defaultpassword')
+ True
+
+If the :class:`PostGenerationMethodCall` declaration contained no
+arguments or one argument, an overriding the value can be passed
+directly to the method through a keyword argument matching the attribute name.
+For example we can override the default password specified in the declaration
+above by simply passing in the desired password as a keyword argument to the
+factory during instantiation.
+
+.. code-block:: pycon
+
+ >>> other_u = UserFactory(password='different') # Calls user.set_password('different')
+ >>> other_u.check_password('defaultpassword')
+ False
+ >>> other_u.check_password('different')
+ True
+
+.. note::
+
+ For Django models, unless the object method called by
+ :class:`PostGenerationMethodCall` saves the object back to the
+ database, we will have to explicitly remember to save the object back
+ if we performed a ``create()``.
+
+ .. code-block:: pycon
+
+ >>> u = UserFactory.create() # u.password has not been saved back to the database
+ >>> u.save() # we must remember to do it ourselves
+
+
+ We can avoid this by subclassing from :class:`DjangoModelFactory`,
+ instead, e.g.,
+
+ .. code-block:: python
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = User
+
+ username = 'user'
+ password = factory.PostGenerationMethodCall('set_password',
+ 'defaultpassword')
+
+
+If instead the :class:`PostGenerationMethodCall` declaration uses two or
+more positional arguments, the overriding value must be an iterable. For
+example, if we declared the ``password`` attribute like the following,
+
+.. code-block:: python
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ username = 'user'
+ password = factory.PostGenerationMethodCall('set_password', '', 'sha1')
+
+then we must be cautious to pass in an iterable for the ``password``
+keyword argument when creating an instance from the factory:
+
+.. code-block:: pycon
+
+ >>> UserFactory() # Calls user.set_password('', 'sha1')
+ >>> UserFactory(password=('test', 'md5')) # Calls user.set_password('test', 'md5')
+
+ >>> # Always pass in a good iterable:
+ >>> UserFactory(password=('test',)) # Calls user.set_password('test')
+ >>> UserFactory(password='test') # Calls user.set_password('t', 'e', 's', 't')
+
+
+.. note:: While this setup provides sane and intuitive defaults for most users,
+ it prevents passing more than one argument when the declaration used
+ zero or one.
+
+ In such cases, users are advised to either resort to the more powerful
+ :class:`PostGeneration` or to add the second expected argument default
+ value to the :class:`PostGenerationMethodCall` declaration
+ (``PostGenerationMethodCall('method', 'x', 'y_that_is_the_default')``)
+
+Keywords extracted from the factory arguments are merged into the
+defaults present in the :class:`PostGenerationMethodCall` declaration.
+
+.. code-block:: pycon
+
+ >>> UserFactory(password__disabled=True) # Calls user.set_password('', 'sha1', disabled=True)
+
+
+Module-level functions
+----------------------
+
+Beyond the :class:`Factory` class and the various :ref:`declarations` classes
+and methods, factory_boy exposes a few module-level functions, mostly useful
+for lightweight factory generation.
+
+
+Lightweight factory declaration
+"""""""""""""""""""""""""""""""
+
+.. function:: make_factory(klass, **kwargs)
+
+ .. OHAI_VIM**
+
+ The :func:`make_factory` function takes a class, declarations as keyword arguments,
+ and generates a new :class:`Factory` for that class accordingly:
+
+ .. code-block:: python
+
+ UserFactory = make_factory(User,
+ login='john',
+ email=factory.LazyAttribute(lambda u: '%s@example.com' % u.login),
+ )
+
+ # This is equivalent to:
+
+ class UserFactory(factory.Factory):
+ FACTORY_FOR = User
+
+ login = 'john'
+ email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login)
+
+ An alternate base class to :class:`Factory` can be specified in the
+ ``FACTORY_CLASS`` argument:
+
+ .. code-block:: python
+
+ UserFactory = make_factory(models.User,
+ login='john',
+ email=factory.LazyAttribute(lambda u: '%s@example.com' % u.login),
+ FACTORY_CLASS=factory.DjangoModelFactory,
+ )
+
+ # This is equivalent to:
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = models.User
+
+ login = 'john'
+ email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login)
+
+ .. versionadded:: 2.0.0
+ The ``FACTORY_CLASS`` kwarg was added in 2.0.0.
+
+
+Instance building
+"""""""""""""""""
+
+The :mod:`factory` module provides a bunch of shortcuts for creating a factory and
+extracting instances from them:
+
+.. function:: build(klass, FACTORY_CLASS=None, **kwargs)
+.. function:: build_batch(klass, size, FACTORY_CLASS=None, **kwargs)
+
+ Create a factory for :obj:`klass` using declarations passed in kwargs;
+ return an instance built from that factory,
+ or a list of :obj:`size` instances (for :func:`build_batch`).
+
+ :param class klass: Class of the instance to build
+ :param int size: Number of instances to build
+ :param kwargs: Declarations to use for the generated factory
+ :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`)
+
+
+
+.. function:: create(klass, FACTORY_CLASS=None, **kwargs)
+.. function:: create_batch(klass, size, FACTORY_CLASS=None, **kwargs)
+
+ Create a factory for :obj:`klass` using declarations passed in kwargs;
+ return an instance created from that factory,
+ or a list of :obj:`size` instances (for :func:`create_batch`).
+
+ :param class klass: Class of the instance to create
+ :param int size: Number of instances to create
+ :param kwargs: Declarations to use for the generated factory
+ :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`)
+
+
+
+.. function:: stub(klass, FACTORY_CLASS=None, **kwargs)
+.. function:: stub_batch(klass, size, FACTORY_CLASS=None, **kwargs)
+
+ Create a factory for :obj:`klass` using declarations passed in kwargs;
+ return an instance stubbed from that factory,
+ or a list of :obj:`size` instances (for :func:`stub_batch`).
+
+ :param class klass: Class of the instance to stub
+ :param int size: Number of instances to stub
+ :param kwargs: Declarations to use for the generated factory
+ :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`)
+
+
+
+.. function:: generate(klass, strategy, FACTORY_CLASS=None, **kwargs)
+.. function:: generate_batch(klass, strategy, size, FACTORY_CLASS=None, **kwargs)
+
+ Create a factory for :obj:`klass` using declarations passed in kwargs;
+ return an instance generated from that factory with the :obj:`strategy` strategy,
+ or a list of :obj:`size` instances (for :func:`generate_batch`).
+
+ :param class klass: Class of the instance to generate
+ :param str strategy: The strategy to use
+ :param int size: Number of instances to generate
+ :param kwargs: Declarations to use for the generated factory
+ :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`)
+
+
+
+.. function:: simple_generate(klass, create, FACTORY_CLASS=None, **kwargs)
+.. function:: simple_generate_batch(klass, create, size, FACTORY_CLASS=None, **kwargs)
+
+ Create a factory for :obj:`klass` using declarations passed in kwargs;
+ return an instance generated from that factory according to the :obj:`create` flag,
+ or a list of :obj:`size` instances (for :func:`simple_generate_batch`).
+
+ :param class klass: Class of the instance to generate
+ :param bool create: Whether to build (``False``) or create (``True``) instances
+ :param int size: Number of instances to generate
+ :param kwargs: Declarations to use for the generated factory
+ :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`)
+
+
diff --git a/docs/subfactory.rst b/docs/subfactory.rst
deleted file mode 100644
index 1815312..0000000
--- a/docs/subfactory.rst
+++ /dev/null
@@ -1,56 +0,0 @@
-SubFactory
-==========
-
-Some objects may use other complex objects as parameters; in order to simplify this setup, factory_boy
-provides the :py:class:`factory.SubFactory` class.
-
-This should be used when defining a :py:class:`~factory.Factory` attribute that will hold the other complex object::
-
- import factory
-
- # A standard factory
- class UserFactory(factory.Factory):
- FACTORY_FOR = User
-
- # Various fields
- first_name = 'John'
- last_name = factory.Sequence(lambda n: 'D%se' % ('o' * n)) # De, Doe, Dooe, Doooe, ...
- email = factory.LazyAttribute(lambda o: '%s.%s@example.org' % (o.first_name.lower(), o.last_name.lower()))
-
- # A factory for an object with a 'User' field
- class CompanyFactory(factory.Factory):
- FACTORY_FOR = Company
-
- name = factory.Sequence(lambda n: 'FactoryBoyz' + z * n)
-
- # Let's use our UserFactory to create that user, and override its first name.
- owner = factory.SubFactory(UserFactory, first_name='Jack')
-
-Instantiating the external factory will in turn instantiate an object of the internal factory::
-
- >>> c = CompanyFactory()
- >>> c
- <Company: FactoryBoyz>
-
- # Notice that the first_name was overridden
- >>> c.owner
- <User: Jack De>
- >>> c.owner.email
- jack.de@example.org
-
-Fields of the SubFactory can also be overridden when instantiating the external factory::
-
- >>> c = CompanyFactory(owner__first_name='Henry')
- >>> c.owner
- <User: Henry Doe>
-
- # Notice that the updated first_name was propagated to the email LazyAttribute.
- >>> c.owner.email
- henry.doe@example.org
-
- # It is also possible to override other fields of the SubFactory
- >>> c = CompanyFactory(owner__last_name='Jones')
- >>> c.owner
- <User: Henry Jones>
- >>> c.owner.email
- henry.jones@example.org
diff --git a/factory/__init__.py b/factory/__init__.py
index 3753461..88865ba 100644
--- a/factory/__init__.py
+++ b/factory/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -20,52 +20,55 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-__version__ = '1.1.5' # Remember to change in setup.py as well!
-__author__ = 'Raphaël Barrois <raphael.barrois@polytechnique.org>'
+__version__ = '2.0.2'
+__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
-from base import (
+from .base import (
Factory,
+ BaseDictFactory,
+ DictFactory,
+ BaseListFactory,
+ ListFactory,
+ MogoFactory,
StubFactory,
DjangoModelFactory,
- build,
- create,
- stub,
- generate,
- simple_generate,
- make_factory,
-
- build_batch,
- create_batch,
- stub_batch,
- generate_batch,
- simple_generate_batch,
-
BUILD_STRATEGY,
CREATE_STRATEGY,
STUB_STRATEGY,
use_strategy,
-
- DJANGO_CREATION,
- NAIVE_BUILD,
- MOGO_BUILD,
)
-from declarations import (
+from .declarations import (
LazyAttribute,
Iterator,
- InfiniteIterator,
Sequence,
LazyAttributeSequence,
SelfAttribute,
ContainerAttribute,
SubFactory,
+ Dict,
+ List,
PostGeneration,
RelatedFactory,
+)
+
+from .helpers import (
+ build,
+ create,
+ stub,
+ generate,
+ simple_generate,
+ make_factory,
+
+ build_batch,
+ create_batch,
+ stub_batch,
+ generate_batch,
+ simple_generate_batch,
lazy_attribute,
iterator,
- infinite_iterator,
sequence,
lazy_attribute_sequence,
container_attribute,
diff --git a/factory/base.py b/factory/base.py
index c1dbd98..2ff2944 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -20,24 +20,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-import re
-import sys
-import warnings
-
-from factory import containers
+from . import containers
# Strategies
BUILD_STRATEGY = 'build'
CREATE_STRATEGY = 'create'
STUB_STRATEGY = 'stub'
-# Creation functions. Use Factory.set_creation_function() to set a creation function appropriate for your ORM.
-DJANGO_CREATION = lambda class_to_create, **kwargs: class_to_create.objects.create(**kwargs)
-
-# Building functions. Use Factory.set_building_function() to set a building functions appropriate for your ORM.
-NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs)
-MOGO_BUILD = lambda class_to_build, **kwargs: class_to_build.new(**kwargs)
-
# Special declarations
FACTORY_CLASS_DECLARATION = 'FACTORY_FOR'
@@ -48,210 +37,190 @@ CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations'
CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class'
+class FactoryError(Exception):
+ """Any exception raised by factory_boy."""
+
+
+class AssociatedClassError(FactoryError):
+ """Exception for Factory subclasses lacking FACTORY_FOR."""
+
+
+class UnknownStrategy(FactoryError):
+ """Raised when a factory uses an unknown strategy."""
+
+
+class UnsupportedStrategy(FactoryError):
+ """Raised when trying to use a strategy on an incompatible Factory."""
+
+
# Factory metaclasses
def get_factory_bases(bases):
- """Retrieve all BaseFactoryMetaClass-derived bases from a list."""
- return [b for b in bases if isinstance(b, BaseFactoryMetaClass)]
+ """Retrieve all FactoryMetaClass-derived bases from a list."""
+ return [b for b in bases if issubclass(b, BaseFactory)]
-class BaseFactoryMetaClass(type):
+class FactoryMetaClass(type):
"""Factory metaclass for handling ordered declarations."""
def __call__(cls, **kwargs):
- """Override the default Factory() syntax to call the default build strategy.
+ """Override the default Factory() syntax to call the default strategy.
Returns an instance of the associated class.
"""
- if cls.default_strategy == BUILD_STRATEGY:
+ if cls.FACTORY_STRATEGY == BUILD_STRATEGY:
return cls.build(**kwargs)
- elif cls.default_strategy == CREATE_STRATEGY:
+ elif cls.FACTORY_STRATEGY == CREATE_STRATEGY:
return cls.create(**kwargs)
- elif cls.default_strategy == STUB_STRATEGY:
+ elif cls.FACTORY_STRATEGY == STUB_STRATEGY:
return cls.stub(**kwargs)
else:
- raise BaseFactory.UnknownStrategy('Unknown default_strategy: {0}'.format(cls.default_strategy))
+ raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(
+ cls.FACTORY_STRATEGY))
- def __new__(cls, class_name, bases, attrs, extra_attrs=None):
- """Record attributes as a pattern for later instance construction.
+ @classmethod
+ def _discover_associated_class(mcs, class_name, attrs, inherited=None):
+ """Try to find the class associated with this factory.
- This is called when a new Factory subclass is defined; it will collect
- attribute declaration from the class definition.
+ In order, the following tests will be performed:
+ - Lookup the FACTORY_CLASS_DECLARATION attribute
+ - If an inherited associated class was provided, use it.
Args:
- class_name (str): the name of the class being created
- bases (list of class): the parents of the class being created
- attrs (str => obj dict): the attributes as defined in the class
+ class_name (str): the name of the factory class being created
+ attrs (dict): the dict of attributes from the factory 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.
+ inherited (class): the optional associated class inherited from a
+ parent factory
Returns:
- A new class
+ 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]
- parent_factories = get_factory_bases(bases)
- if not parent_factories:
- # If this isn't a subclass of Factory, don't do anything special.
- return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, attrs)
+ # No specific associated class was given, and one was defined for our
+ # parent, use it.
+ 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)
+
+ @classmethod
+ def _extract_declarations(mcs, bases, attributes):
+ """Extract declarations from a class definition.
+ Args:
+ bases (class list): parent Factory subclasses
+ attributes (dict): attributes declared in the class definition
+
+ Returns:
+ dict: the original attributes, where declarations have been moved to
+ _declarations and post-generation declarations to
+ _postgen_declarations.
+ """
declarations = containers.DeclarationDict()
postgen_declarations = containers.PostGenerationDeclarationDict()
# Add parent declarations in reverse order.
- for base in reversed(parent_factories):
+ for base in reversed(bases):
# Import parent PostGenerationDeclaration
postgen_declarations.update_with_public(
getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {}))
# Import all 'public' attributes (avoid those starting with _)
- declarations.update_with_public(getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {}))
+ declarations.update_with_public(
+ getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {}))
# Import attributes from the class definition
- non_postgen_attrs = postgen_declarations.update_with_public(attrs)
+ attributes = postgen_declarations.update_with_public(attributes)
# Store protected/private attributes in 'non_factory_attrs'.
- non_factory_attrs = declarations.update_with_public(non_postgen_attrs)
+ attributes = declarations.update_with_public(attributes)
# Store the DeclarationDict in the attributes of the newly created class
- non_factory_attrs[CLASS_ATTRIBUTE_DECLARATIONS] = declarations
- non_factory_attrs[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations
-
- # Add extra args if provided.
- if extra_attrs:
- non_factory_attrs.update(extra_attrs)
-
- return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, non_factory_attrs)
+ attributes[CLASS_ATTRIBUTE_DECLARATIONS] = declarations
+ attributes[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations
+ return attributes
-class FactoryMetaClass(BaseFactoryMetaClass):
- """Factory metaclass for handling class association and ordered declarations."""
-
- ERROR_MESSAGE = """Could not determine what class this factory is for.
- Use the {0} attribute to specify a class."""
- ERROR_MESSAGE_AUTODISCOVERY = ERROR_MESSAGE + """
- Also, autodiscovery failed using the name '{1}'
- based on the Factory name '{2}' in {3}."""
-
- @classmethod
- def _discover_associated_class(cls, class_name, attrs, inherited=None):
- """Try to find the class associated with this factory.
+ def __new__(mcs, class_name, bases, attrs, extra_attrs=None):
+ """Record attributes as a pattern for later instance construction.
- In order, the following tests will be performed:
- - Lookup the FACTORY_CLASS_DECLARATION attribute
- - If the newly created class is named 'FooBarFactory', look for a FooBar
- class in its module
- - If an inherited associated class was provided, use it.
+ This is called when a new Factory subclass is defined; it will collect
+ attribute declaration from the class definition.
Args:
- class_name (str): the name of the factory class being created
- attrs (dict): the dict of attributes from the factory class
+ class_name (str): the name of the class being created
+ bases (list of class): the parents of the class being created
+ attrs (str => obj dict): the attributes as defined in the class
definition
- inherited (class): the optional associated class inherited from a
- parent factory
+ 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:
- class: the class to associate with this factory
-
- Raises:
- AssociatedClassError: If we were unable to associate this factory
- to a class.
+ A new class
"""
- own_associated_class = None
- used_auto_discovery = False
-
- if FACTORY_CLASS_DECLARATION in attrs:
- return attrs[FACTORY_CLASS_DECLARATION]
-
- # No specific associated calss was given, and one was defined for our
- # parent, use it.
- if inherited is not None:
- return inherited
+ parent_factories = get_factory_bases(bases)
+ if not parent_factories:
+ return super(FactoryMetaClass, mcs).__new__(
+ mcs, class_name, bases, attrs)
- if '__module__' in attrs:
- factory_module = sys.modules[attrs['__module__']]
- if class_name.endswith('Factory'):
- # Try a module lookup
- used_auto_discovery = True
- associated_name = class_name[:-len('Factory')]
- if associated_name and hasattr(factory_module, associated_name):
- warnings.warn(
- "Auto-discovery of associated class is deprecated, and "
- "will be removed in the future. Please set '%s = %s' "
- "in the %s class definition." % (
- FACTORY_CLASS_DECLARATION,
- associated_name,
- class_name,
- ), PendingDeprecationWarning)
-
- return getattr(factory_module, associated_name)
-
- # Unable to guess a good option; return the inherited class.
- # Unable to find an associated class; fail.
- if used_auto_discovery:
- raise Factory.AssociatedClassError(
- FactoryMetaClass.ERROR_MESSAGE_AUTODISCOVERY.format(
- FACTORY_CLASS_DECLARATION,
- associated_name,
- class_name,
- factory_module,))
- else:
- raise Factory.AssociatedClassError(
- FactoryMetaClass.ERROR_MESSAGE.format(
- FACTORY_CLASS_DECLARATION))
+ is_abstract = attrs.pop('ABSTRACT_FACTORY', False)
+ extra_attrs = {}
- def __new__(cls, class_name, bases, attrs):
- """Determine the associated class based on the factory class name. Record the associated class
- for construction of an associated class instance at a later time."""
+ if not is_abstract:
- parent_factories = get_factory_bases(bases)
- if not parent_factories or attrs.get('ABSTRACT_FACTORY', False):
- # If this isn't a subclass of Factory, or specifically declared
- # abstract, don't do anything special.
- if 'ABSTRACT_FACTORY' in attrs:
- attrs.pop('ABSTRACT_FACTORY')
+ base = parent_factories[0]
- return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs)
+ inherited_associated_class = getattr(base,
+ CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
+ associated_class = mcs._discover_associated_class(class_name, attrs,
+ inherited_associated_class)
- base = parent_factories[0]
+ # 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:
+ attrs['_base_factory'] = base
- inherited_associated_class = getattr(base,
- CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None)
- associated_class = cls._discover_associated_class(class_name, attrs,
- inherited_associated_class)
+ # 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}
- # Remove the FACTORY_CLASS_DECLARATION attribute from attrs, if present.
- attrs.pop(FACTORY_CLASS_DECLARATION, None)
+ # Extract pre- and post-generation declarations
+ attributes = mcs._extract_declarations(parent_factories, attrs)
- # 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:
- attrs['_base_factory'] = base
+ # Add extra args if provided.
+ if extra_attrs:
+ attributes.update(extra_attrs)
- # 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}
+ return super(FactoryMetaClass, mcs).__new__(
+ mcs, class_name, bases, attributes)
- return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs, extra_attrs=extra_attrs)
+ def __str__(cls):
+ return '<%s for %s>' % (cls.__name__,
+ getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
- def __str__(self):
- return '<%s for %s>' % (self.__name__,
- getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__)
# Factory base classes
class BaseFactory(object):
"""Factory base support for sequences, attributes and stubs."""
- class UnknownStrategy(RuntimeError):
- pass
-
- class UnsupportedStrategy(RuntimeError):
- pass
+ # Backwards compatibility
+ UnknownStrategy = UnknownStrategy
+ UnsupportedStrategy = UnsupportedStrategy
def __new__(cls, *args, **kwargs):
"""Would be called if trying to instantiate the class."""
- raise RuntimeError('You cannot instantiate BaseFactory')
+ raise FactoryError('You cannot instantiate BaseFactory')
# ID to use for the next 'declarations.Sequence' attribute.
_next_sequence = None
@@ -261,6 +230,15 @@ class BaseFactory(object):
# class.
_base_factory = None
+ # Holds the target class, once resolved.
+ _associated_class = None
+
+ # List of arguments that should be passed as *args instead of **kwargs
+ FACTORY_ARG_PARAMETERS = ()
+
+ # List of attributes that should not be passed to the underlying class
+ FACTORY_HIDDEN_ARGS = ()
+
@classmethod
def _setup_next_sequence(cls):
"""Set up an initial sequence value for Sequence attributes.
@@ -304,7 +282,13 @@ class BaseFactory(object):
applicable; the current list of computed attributes is available
to the currently processed object.
"""
- return containers.AttributeBuilder(cls, extra).build(create)
+ force_sequence = None
+ if extra:
+ force_sequence = extra.pop('__sequence', None)
+ return containers.AttributeBuilder(cls, extra).build(
+ create=create,
+ force_sequence=force_sequence,
+ )
@classmethod
def declarations(cls, extra_defs=None):
@@ -317,9 +301,108 @@ class BaseFactory(object):
return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs)
@classmethod
+ def _adjust_kwargs(cls, **kwargs):
+ """Extension point for custom kwargs adjustment."""
+ return kwargs
+
+ @classmethod
+ def _prepare(cls, create, **kwargs):
+ """Prepare an object for this factory.
+
+ Args:
+ 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)
+ kwargs = cls._adjust_kwargs(**kwargs)
+
+ # Remove 'hidden' arguments.
+ for arg in cls.FACTORY_HIDDEN_ARGS:
+ del kwargs[arg]
+
+ # Extract *args from **kwargs
+ args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS)
+
+ if create:
+ return cls._create(target_class, *args, **kwargs)
+ else:
+ return cls._build(target_class, *args, **kwargs)
+
+ @classmethod
+ def _generate(cls, create, attrs):
+ """generate the object.
+
+ Args:
+ create (bool): whether to 'build' or 'create' the object
+ attrs (dict): attributes to use for generating the object
+ """
+ # Extract declarations used for post-generation
+ postgen_declarations = getattr(cls,
+ CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS)
+ postgen_attributes = {}
+ for name, decl in sorted(postgen_declarations.items()):
+ postgen_attributes[name] = decl.extract(name, attrs)
+
+ # Generate the object
+ obj = cls._prepare(create, **attrs)
+
+ # 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)
+
+ cls._after_postgeneration(obj, create, results)
+
+ return obj
+
+ @classmethod
+ def _after_postgeneration(cls, obj, create, results=None):
+ """Hook called after post-generation declarations have been handled.
+
+ Args:
+ obj (object): the generated object
+ create (bool): whether the strategy was 'build' or 'create'
+ results (dict or None): result of post-generation declarations
+ """
+ pass
+
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ """Actually build an instance of the target_class.
+
+ Customization point, will be called once the full set of args and kwargs
+ has been computed.
+
+ Args:
+ target_class (type): the class for which an instance should be
+ built
+ args (tuple): arguments to use when building the class
+ kwargs (dict): keyword arguments to use when building the class
+ """
+ return target_class(*args, **kwargs)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """Actually create an instance of the target_class.
+
+ Customization point, will be called once the full set of args and kwargs
+ has been computed.
+
+ Args:
+ target_class (type): the class for which an instance should be
+ created
+ args (tuple): arguments to use when creating the class
+ kwargs (dict): keyword arguments to use when creating the class
+ """
+ return target_class(*args, **kwargs)
+
+ @classmethod
def build(cls, **kwargs):
"""Build an instance of the associated class, with overriden attrs."""
- raise cls.UnsupportedStrategy()
+ attrs = cls.attributes(create=False, extra=kwargs)
+ return cls._generate(False, attrs)
@classmethod
def build_batch(cls, size, **kwargs):
@@ -331,12 +414,13 @@ class BaseFactory(object):
Returns:
object list: the built instances
"""
- return [cls.build(**kwargs) for _ in xrange(size)]
+ return [cls.build(**kwargs) for _ in range(size)]
@classmethod
def create(cls, **kwargs):
"""Create an instance of the associated class, with overriden attrs."""
- raise cls.UnsupportedStrategy()
+ attrs = cls.attributes(create=True, extra=kwargs)
+ return cls._generate(True, attrs)
@classmethod
def create_batch(cls, size, **kwargs):
@@ -348,7 +432,7 @@ class BaseFactory(object):
Returns:
object list: the created instances
"""
- return [cls.create(**kwargs) for _ in xrange(size)]
+ return [cls.create(**kwargs) for _ in range(size)]
@classmethod
def stub(cls, **kwargs):
@@ -358,7 +442,7 @@ class BaseFactory(object):
factory's declarations or in the extra kwargs.
"""
stub_object = containers.StubObject()
- for name, value in cls.attributes(create=False, extra=kwargs).iteritems():
+ for name, value in cls.attributes(create=False, extra=kwargs).items():
setattr(stub_object, name, value)
return stub_object
@@ -372,7 +456,7 @@ class BaseFactory(object):
Returns:
object list: the stubbed instances
"""
- return [cls.stub(**kwargs) for _ in xrange(size)]
+ return [cls.stub(**kwargs) for _ in range(size)]
@classmethod
def generate(cls, strategy, **kwargs):
@@ -441,205 +525,152 @@ class BaseFactory(object):
return cls.generate_batch(strategy, size, **kwargs)
-class StubFactory(BaseFactory):
- __metaclass__ = BaseFactoryMetaClass
-
- default_strategy = STUB_STRATEGY
-
-
-class Factory(BaseFactory):
- """Factory base with build and create support.
+Factory = FactoryMetaClass('Factory', (BaseFactory,), {
+ 'ABSTRACT_FACTORY': True,
+ 'FACTORY_STRATEGY': CREATE_STRATEGY,
+ '__doc__': """Factory base with build and create support.
This class has the ability to support multiple ORMs by using custom creation
functions.
- """
- __metaclass__ = FactoryMetaClass
-
- default_strategy = CREATE_STRATEGY
+ """,
+ })
- class AssociatedClassError(RuntimeError):
- pass
-
- # Customizing 'create' strategy, using a tuple to keep the creation function
- # from turning it into an instance method.
- _creation_function = (DJANGO_CREATION,)
- @classmethod
- def set_creation_function(cls, creation_function):
- """Set the creation function for this class.
+# Backwards compatibility
+Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201
- Args:
- creation_function (function): the new creation function. That
- function should take one non-keyword argument, the 'class' for
- which an instance will be created. The value of the various
- fields are passed as keyword arguments.
- """
- cls._creation_function = (creation_function,)
- @classmethod
- def get_creation_function(cls):
- """Retrieve the creation function for this class.
+class StubFactory(Factory):
- Returns:
- function: A function that takes one parameter, the class for which
- an instance will be created, and keyword arguments for the value
- of the fields of the instance.
- """
- return cls._creation_function[0]
-
- # Customizing 'build' strategy, using a tuple to keep the creation function
- # from turning it into an instance method.
- _building_function = (NAIVE_BUILD,)
-
- @classmethod
- def set_building_function(cls, building_function):
- """Set the building function for this class.
-
- Args:
- building_function (function): the new building function. That
- function should take one non-keyword argument, the 'class' for
- which an instance will be built. The value of the various
- fields are passed as keyword arguments.
- """
- cls._building_function = (building_function,)
-
- @classmethod
- def get_building_function(cls):
- """Retrieve the building function for this class.
-
- Returns:
- function: A function that takes one parameter, the class for which
- an instance will be created, and keyword arguments for the value
- of the fields of the instance.
- """
- return cls._building_function[0]
-
- @classmethod
- def _prepare(cls, create, **kwargs):
- """Prepare an object for this factory.
-
- Args:
- create: bool, whether to create or to build the object
- **kwargs: arguments to pass to the creation function
- """
- if create:
- return cls.get_creation_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs)
- else:
- return cls.get_building_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs)
-
- @classmethod
- def _generate(cls, create, attrs):
- """generate the object.
-
- Args:
- create (bool): whether to 'build' or 'create' the object
- attrs (dict): attributes to use for generating the object
- """
- # Extract declarations used for post-generation
- postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS)
- postgen_attributes = {}
- for name, decl in sorted(postgen_declarations.items()):
- postgen_attributes[name] = decl.extract(name, attrs)
-
- # Generate the object
- obj = cls._prepare(create, **attrs)
-
- # Handle post-generation attributes
- for name, decl in sorted(postgen_declarations.items()):
- extracted, extracted_kwargs = postgen_attributes[name]
- decl.call(obj, create, extracted, **extracted_kwargs)
- return obj
+ FACTORY_STRATEGY = STUB_STRATEGY
+ FACTORY_FOR = containers.StubObject
@classmethod
def build(cls, **kwargs):
- attrs = cls.attributes(create=False, extra=kwargs)
- return cls._generate(False, attrs)
+ raise UnsupportedStrategy()
@classmethod
def create(cls, **kwargs):
- attrs = cls.attributes(create=True, extra=kwargs)
- return cls._generate(True, attrs)
+ raise UnsupportedStrategy()
class DjangoModelFactory(Factory):
"""Factory for Django models.
- This makes sure that the 'sequence' field of created objects is an unused id.
+ 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 ID, based on the 'id' database field."""
+ """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 + cls._associated_class._default_manager.values_list('id', flat=True
- ).order_by('-id')[0]
+ 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)
-def make_factory(klass, **kwargs):
- """Create a new, simple factory for the given class."""
- factory_name = '%sFactory' % klass.__name__
- kwargs[FACTORY_CLASS_DECLARATION] = klass
- factory_class = type(Factory).__new__(type(Factory), factory_name, (Factory,), kwargs)
- factory_class.__name__ = '%sFactory' % klass.__name__
- factory_class.__doc__ = 'Auto-generated factory for class %s' % klass
- return factory_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
-def build(klass, **kwargs):
- """Create a factory for the given class, and build an instance."""
- return make_factory(klass, **kwargs).build()
+ 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)
-def build_batch(klass, size, **kwargs):
- """Create a factory for the given class, and build a batch of instances."""
- return make_factory(klass, **kwargs).build_batch(size)
+ if cls.FACTORY_DJANGO_GET_OR_CREATE:
+ return cls._get_or_create(target_class, *args, **kwargs)
+ return manager.create(*args, **kwargs)
-def create(klass, **kwargs):
- """Create a factory for the given class, and create an instance."""
- return make_factory(klass, **kwargs).create()
+ @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)
-def create_batch(klass, size, **kwargs):
- """Create a factory for the given class, and create a batch of instances."""
- return make_factory(klass, **kwargs).create_batch(size)
+class BaseDictFactory(Factory):
+ """Factory for dictionary-like classes."""
+ ABSTRACT_FACTORY = True
-def stub(klass, **kwargs):
- """Create a factory for the given class, and stub an instance."""
- return make_factory(klass, **kwargs).stub()
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ if args:
+ raise ValueError(
+ "DictFactory %r does not support FACTORY_ARG_PARAMETERS.", cls)
+ return target_class(**kwargs)
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ return cls._build(target_class, *args, **kwargs)
-def stub_batch(klass, size, **kwargs):
- """Create a factory for the given class, and stub a batch of instances."""
- return make_factory(klass, **kwargs).stub_batch(size)
+class DictFactory(BaseDictFactory):
+ FACTORY_FOR = dict
-def generate(klass, strategy, **kwargs):
- """Create a factory for the given class, and generate an instance."""
- return make_factory(klass, **kwargs).generate(strategy)
+class BaseListFactory(Factory):
+ """Factory for list-like classes."""
+ ABSTRACT_FACTORY = True
-def generate_batch(klass, strategy, size, **kwargs):
- """Create a factory for the given class, and generate instances."""
- return make_factory(klass, **kwargs).generate_batch(strategy, size)
+ @classmethod
+ def _build(cls, target_class, *args, **kwargs):
+ if args:
+ raise ValueError(
+ "ListFactory %r does not support FACTORY_ARG_PARAMETERS.", cls)
+ values = [v for k, v in sorted(kwargs.items())]
+ return target_class(values)
-def simple_generate(klass, create, **kwargs):
- """Create a factory for the given class, and simple_generate an instance."""
- return make_factory(klass, **kwargs).simple_generate(create)
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ return cls._build(target_class, *args, **kwargs)
-def simple_generate_batch(klass, create, size, **kwargs):
- """Create a factory for the given class, and simple_generate instances."""
- return make_factory(klass, **kwargs).simple_generate_batch(create, size)
+class ListFactory(BaseListFactory):
+ FACTORY_FOR = list
def use_strategy(new_strategy):
@@ -648,6 +679,6 @@ def use_strategy(new_strategy):
This is an alternative to setting default_strategy in the class definition.
"""
def wrapped_class(klass):
- klass.default_strategy = new_strategy
+ klass.FACTORY_STRATEGY = new_strategy
return klass
return wrapped_class
diff --git a/factory/compat.py b/factory/compat.py
new file mode 100644
index 0000000..84f31b7
--- /dev/null
+++ b/factory/compat.py
@@ -0,0 +1,35 @@
+# -*- 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.
+
+
+"""Compatibility tools"""
+
+import sys
+
+PY2 = (sys.version_info[0] == 2)
+
+if PY2:
+ def is_string(obj):
+ return isinstance(obj, (str, unicode))
+else:
+ def is_string(obj):
+ return isinstance(obj, str)
diff --git a/factory/containers.py b/factory/containers.py
index 946fbd3..c8be431 100644
--- a/factory/containers.py
+++ b/factory/containers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -21,8 +21,8 @@
# THE SOFTWARE.
-from factory import declarations
-from factory import utils
+from . import declarations
+from . import utils
class CyclicDefinitionError(Exception):
@@ -62,7 +62,7 @@ class LazyStub(object):
def __str__(self):
return '<LazyStub for %s with %s>' % (
- self.__target_class.__name__, self.__attrs.keys())
+ self.__target_class.__name__, list(self.__attrs.keys()))
def __fill__(self):
"""Fill this LazyStub, computing values of all defined attributes.
@@ -136,7 +136,7 @@ class DeclarationDict(dict):
Returns a dict containing all remaining elements.
"""
remaining = {}
- for k, v in d.iteritems():
+ for k, v in d.items():
if self.is_declaration(k, v):
self[k] = v
else:
@@ -172,30 +172,6 @@ class LazyValue(object):
raise NotImplementedError("This is an abstract method.")
-class SubFactoryWrapper(LazyValue):
- """Lazy wrapper around a SubFactory.
-
- Attributes:
- subfactory (declarations.SubFactory): the SubFactory being wrapped
- subfields (DeclarationDict): Default values to override when evaluating
- the SubFactory
- create (bool): whether to 'create' or 'build' the SubFactory.
- """
-
- def __init__(self, subfactory, subfields, create, *args, **kwargs):
- super(SubFactoryWrapper, self).__init__(*args, **kwargs)
- self.subfactory = subfactory
- self.subfields = subfields
- self.create = create
-
- def evaluate(self, obj, containers=()):
- expanded_containers = (obj,)
- if containers:
- expanded_containers += tuple(containers)
- return self.subfactory.evaluate(self.create, self.subfields,
- expanded_containers)
-
-
class OrderedDeclarationWrapper(LazyValue):
"""Lazy wrapper around an OrderedDeclaration.
@@ -206,10 +182,12 @@ class OrderedDeclarationWrapper(LazyValue):
declaration
"""
- def __init__(self, declaration, sequence, *args, **kwargs):
- super(OrderedDeclarationWrapper, self).__init__(*args, **kwargs)
+ def __init__(self, declaration, sequence, create, extra=None, **kwargs):
+ super(OrderedDeclarationWrapper, self).__init__(**kwargs)
self.declaration = declaration
self.sequence = sequence
+ self.create = create
+ self.extra = extra
def evaluate(self, obj, containers=()):
"""Lazily evaluate the attached OrderedDeclaration.
@@ -219,7 +197,14 @@ class OrderedDeclarationWrapper(LazyValue):
containers (object list): the chain of containers of the object
being built, its immediate holder being first.
"""
- return self.declaration.evaluate(self.sequence, obj, containers)
+ return self.declaration.evaluate(self.sequence, obj,
+ create=self.create,
+ extra=self.extra,
+ containers=containers,
+ )
+
+ def __repr__(self):
+ return '<%s for %r>' % (self.__class__.__name__, self.declaration)
class AttributeBuilder(object):
@@ -240,33 +225,43 @@ class AttributeBuilder(object):
extra = {}
self.factory = factory
- self._containers = extra.pop('__containers', None)
+ self._containers = extra.pop('__containers', ())
self._attrs = factory.declarations(extra)
- attrs_with_subfields = [k for k, v in self._attrs.items() if self.has_subfields(v)]
+ attrs_with_subfields = [
+ k for k, v in self._attrs.items()
+ if self.has_subfields(v)]
- self._subfields = utils.multi_extract_dict(attrs_with_subfields, self._attrs)
+ self._subfields = utils.multi_extract_dict(
+ attrs_with_subfields, self._attrs)
def has_subfields(self, value):
- return isinstance(value, declarations.SubFactory)
+ return isinstance(value, declarations.ParameteredAttribute)
- def build(self, create):
+ def build(self, create, force_sequence=None):
"""Build a dictionary of attributes.
Args:
create (bool): whether to 'build' or 'create' the subfactories.
+ force_sequence (int or None): if set to an int, use this value for
+ the sequence counter; don't advance the related counter.
"""
# Setup factory sequence.
- self.factory.sequence = self.factory._generate_next_sequence()
+ if force_sequence is None:
+ sequence = self.factory._generate_next_sequence()
+ else:
+ sequence = force_sequence
# Parse attribute declarations, wrapping SubFactory and
# OrderedDeclaration.
wrapped_attrs = {}
- for k, v in self._attrs.iteritems():
- if isinstance(v, declarations.SubFactory):
- v = SubFactoryWrapper(v, self._subfields.get(k, {}), create)
- elif isinstance(v, declarations.OrderedDeclaration):
- v = OrderedDeclarationWrapper(v, self.factory.sequence)
+ for k, v in self._attrs.items():
+ if isinstance(v, declarations.OrderedDeclaration):
+ v = OrderedDeclarationWrapper(v,
+ sequence=sequence,
+ create=create,
+ extra=self._subfields.get(k, {}),
+ )
wrapped_attrs[k] = v
stub = LazyStub(wrapped_attrs, containers=self._containers,
diff --git a/factory/declarations.py b/factory/declarations.py
index 83c32ab..969d780 100644
--- a/factory/declarations.py
+++ b/factory/declarations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -23,7 +23,8 @@
import itertools
-from factory import utils
+from . import compat
+from . import utils
class OrderedDeclaration(object):
@@ -34,7 +35,7 @@ class OrderedDeclaration(object):
in the same factory.
"""
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
"""Evaluate this declaration.
Args:
@@ -44,6 +45,10 @@ class OrderedDeclaration(object):
attributes
containers (list of containers.LazyStub): The chain of SubFactory
which led to building this object.
+ create (bool): whether the target class should be 'built' or
+ 'created'
+ extra (DeclarationDict or None): extracted key/value extracted from
+ the attribute prefix
"""
raise NotImplementedError('This is an abstract method')
@@ -60,7 +65,7 @@ class LazyAttribute(OrderedDeclaration):
super(LazyAttribute, self).__init__(*args, **kwargs)
self.function = function
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
return self.function(obj)
@@ -100,7 +105,11 @@ def deepgetattr(obj, name, default=_UNSPECIFIED):
class SelfAttribute(OrderedDeclaration):
"""Specific OrderedDeclaration copying values from other fields.
+ If the field name starts with two dots or more, the lookup will be anchored
+ in the related 'parent'.
+
Attributes:
+ depth (int): the number of steps to go up in the containers chain
attribute_name (str): the name of the attribute to copy.
default (object): the default value to use if the attribute doesn't
exist.
@@ -108,11 +117,27 @@ class SelfAttribute(OrderedDeclaration):
def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs):
super(SelfAttribute, self).__init__(*args, **kwargs)
+ depth = len(attribute_name) - len(attribute_name.lstrip('.'))
+ attribute_name = attribute_name[depth:]
+
+ self.depth = depth
self.attribute_name = attribute_name
self.default = default
- def evaluate(self, sequence, obj, containers=()):
- return deepgetattr(obj, self.attribute_name, self.default)
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ if self.depth > 1:
+ # Fetching from a parent
+ target = containers[self.depth - 2]
+ else:
+ target = obj
+ return deepgetattr(target, self.attribute_name, self.default)
+
+ def __repr__(self):
+ return '<%s(%r, default=%r)>' % (
+ self.__class__.__name__,
+ self.attribute_name,
+ self.default,
+ )
class Iterator(OrderedDeclaration):
@@ -122,25 +147,23 @@ class Iterator(OrderedDeclaration):
Attributes:
iterator (iterable): the iterator whose value should be used.
+ getter (callable or None): a function to parse returned values
"""
- def __init__(self, iterator):
+ def __init__(self, iterator, cycle=True, getter=None):
super(Iterator, self).__init__()
- self.iterator = iter(iterator)
-
- def evaluate(self, sequence, obj, containers=()):
- return self.iterator.next()
+ self.getter = getter
+ if cycle:
+ self.iterator = itertools.cycle(iterator)
+ else:
+ self.iterator = iter(iterator)
-class InfiniteIterator(Iterator):
- """Same as Iterator, but make the iterator infinite by cycling at the end.
-
- Attributes:
- iterator (iterable): the iterator, once made infinite.
- """
-
- def __init__(self, iterator):
- return super(InfiniteIterator, self).__init__(itertools.cycle(iterator))
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ value = next(self.iterator)
+ if self.getter is None:
+ return value
+ return self.getter(value)
class Sequence(OrderedDeclaration):
@@ -154,12 +177,12 @@ class Sequence(OrderedDeclaration):
type (function): A function converting an integer into the expected kind
of counter for the 'function' attribute.
"""
- def __init__(self, function, type=str):
+ def __init__(self, function, type=int): # pylint: disable=W0622
super(Sequence, self).__init__()
self.function = function
self.type = type
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
return self.function(self.type(sequence))
@@ -172,7 +195,7 @@ class LazyAttributeSequence(Sequence):
type (function): A function converting an integer into the expected kind
of counter for the 'function' attribute.
"""
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
return self.function(obj, self.type(sequence))
@@ -190,7 +213,7 @@ class ContainerAttribute(OrderedDeclaration):
self.function = function
self.strict = strict
- def evaluate(self, sequence, obj, containers=()):
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
"""Evaluate the current ContainerAttribute.
Args:
@@ -217,12 +240,23 @@ class SubFactory(OrderedDeclaration):
factory (base.Factory): the wrapped factory
"""
- def __init__(self, factory, **kwargs):
- super(SubFactory, self).__init__()
+ CONTAINERS_FIELD = '__containers'
+
+ # Whether to add the current object to the stack of containers
+ EXTEND_CONTAINERS = False
+
+ def __init__(self, **kwargs):
+ super(ParameteredAttribute, self).__init__()
self.defaults = kwargs
self.factory = factory
- def evaluate(self, create, extra, containers):
+ def _prepare_containers(self, obj, containers=()):
+ if self.EXTEND_CONTAINERS:
+ return (obj,) + tuple(containers)
+
+ return containers
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
"""Evaluate the current definition and fill its attributes.
Uses attributes definition in the following order:
@@ -242,25 +276,105 @@ class SubFactory(OrderedDeclaration):
defaults = dict(self.defaults)
if extra:
defaults.update(extra)
- defaults['__containers'] = containers
+ if self.CONTAINERS_FIELD:
+ containers = self._prepare_containers(obj, containers)
+ defaults[self.CONTAINERS_FIELD] = containers
- if create:
- return self.factory.create(**defaults)
- else:
- return self.factory.build(**defaults)
+ return self.generate(sequence, obj, create, defaults)
+ def generate(self, sequence, obj, create, params): # pragma: no cover
+ """Actually generate the related attribute.
-class PostGenerationDeclaration(object):
- """Declarations to be called once the target object has been generated.
+ Args:
+ sequence (int): the current sequence number
+ obj (LazyStub): the object being constructed
+ create (bool): whether the calling factory was in 'create' or
+ 'build' mode
+ params (dict): parameters inherited from init and evaluation-time
+ overrides.
+
+ Returns:
+ Computed value for the current declaration.
+ """
+ raise NotImplementedError()
+
+
+class SubFactory(ParameteredAttribute):
+ """Base class for attributes based upon a sub-factory.
Attributes:
- extract_prefix (str): prefix to use when extracting attributes from
- the factory's declaration for this declaration. If empty, uses
- the attribute name of the PostGenerationDeclaration.
+ defaults (dict): Overrides to the defaults defined in the wrapped
+ factory
+ factory (base.Factory): the wrapped factory
"""
- def __init__(self, extract_prefix=None):
- self.extract_prefix = extract_prefix
+ EXTEND_CONTAINERS = True
+
+ 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)
+
+ 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
+
+ def generate(self, sequence, obj, create, params):
+ """Evaluate the current definition and fill its attributes.
+
+ Args:
+ create (bool): whether the subfactory should call 'build' or
+ 'create'
+ params (containers.DeclarationDict): extra values that should
+ override the wrapped factory's defaults
+ """
+ subfactory = self.get_factory()
+ return subfactory.simple_generate(create, **params)
+
+
+class Dict(SubFactory):
+ """Fill a dict with usual declarations."""
+
+ def __init__(self, params, dict_factory='factory.DictFactory'):
+ super(Dict, self).__init__(dict_factory, **dict(params))
+
+ def generate(self, sequence, obj, create, params):
+ dict_factory = self.get_factory()
+ return dict_factory.simple_generate(create,
+ __sequence=sequence,
+ **params)
+
+
+class List(SubFactory):
+ """Fill a list with standard declarations."""
+
+ def __init__(self, params, list_factory='factory.ListFactory'):
+ params = dict((str(i), v) for i, v in enumerate(params))
+ super(List, self).__init__(list_factory, **params)
+
+ def generate(self, sequence, obj, create, params):
+ list_factory = self.get_factory()
+ return list_factory.simple_generate(create,
+ __sequence=sequence,
+ **params)
+
+
+class PostGenerationDeclaration(object):
+ """Declarations to be called once the target object has been generated."""
def extract(self, name, attrs):
"""Extract relevant attributes from a dict.
@@ -275,12 +389,8 @@ class PostGenerationDeclaration(object):
(object, dict): a tuple containing the attribute at 'name' (if
provided) and a dict of extracted attributes
"""
- if self.extract_prefix:
- extract_prefix = self.extract_prefix
- else:
- extract_prefix = name
- extracted = attrs.pop(extract_prefix, None)
- kwargs = utils.extract_dict(extract_prefix, attrs)
+ extracted = attrs.pop(name, None)
+ kwargs = utils.extract_dict(name, attrs)
return extracted, kwargs
def call(self, obj, create, extracted=None, **kwargs):
@@ -289,7 +399,7 @@ class PostGenerationDeclaration(object):
Args:
obj (object): the newly generated object
create (bool): whether the object was 'built' or 'created'
- extracted (object): the value given for <extract_prefix> in the
+ 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
@@ -299,18 +409,12 @@ class PostGenerationDeclaration(object):
class PostGeneration(PostGenerationDeclaration):
"""Calls a given function once the object has been generated."""
- def __init__(self, function, extract_prefix=None):
- super(PostGeneration, self).__init__(extract_prefix)
+ def __init__(self, function):
+ super(PostGeneration, self).__init__()
self.function = function
def call(self, obj, create, extracted=None, **kwargs):
- self.function(obj, create, extracted, **kwargs)
-
-
-def post_generation(extract_prefix=None):
- def decorator(fun):
- return PostGeneration(fun, extract_prefix=extract_prefix)
- return decorator
+ return self.function(obj, create, extracted, **kwargs)
class RelatedFactory(PostGenerationDeclaration):
@@ -324,17 +428,39 @@ class RelatedFactory(PostGenerationDeclaration):
"""
def __init__(self, factory, name='', **defaults):
- super(RelatedFactory, self).__init__(extract_prefix=None)
- self.factory = factory
+ super(RelatedFactory, self).__init__()
self.name = 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)
+
+ 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
+
def call(self, obj, create, extracted=None, **kwargs):
passed_kwargs = dict(self.defaults)
passed_kwargs.update(kwargs)
if self.name:
passed_kwargs[self.name] = obj
- self.factory.simple_generate(create, **passed_kwargs)
+
+ factory = self.get_factory()
+ factory.simple_generate(create, **passed_kwargs)
class PostGenerationMethodCall(PostGenerationDeclaration):
@@ -348,39 +474,25 @@ class PostGenerationMethodCall(PostGenerationDeclaration):
Example:
class UserFactory(factory.Factory):
...
- password = factory.PostGenerationMethodCall('set_password', password='')
+ password = factory.PostGenerationMethodCall('set_pass', password='')
"""
- def __init__(self, method_name, extract_prefix=None, *args, **kwargs):
- super(PostGenerationMethodCall, self).__init__(extract_prefix)
+ def __init__(self, method_name, *args, **kwargs):
+ super(PostGenerationMethodCall, self).__init__()
self.method_name = method_name
self.method_args = args
self.method_kwargs = kwargs
def call(self, obj, create, extracted=None, **kwargs):
+ if extracted is None:
+ passed_args = self.method_args
+
+ elif len(self.method_args) <= 1:
+ # Max one argument expected
+ passed_args = (extracted,)
+ else:
+ passed_args = tuple(extracted)
+
passed_kwargs = dict(self.method_kwargs)
passed_kwargs.update(kwargs)
method = getattr(obj, self.method_name)
- method(*self.method_args, **passed_kwargs)
-
-
-# Decorators... in case lambdas don't cut it
-
-def lazy_attribute(func):
- return LazyAttribute(func)
-
-def iterator(func):
- """Turn a generator function into an iterator attribute."""
- return Iterator(func())
-
-def infinite_iterator(func):
- """Turn a generator function into an infinite iterator attribute."""
- return InfiniteIterator(func())
-
-def sequence(func):
- return Sequence(func)
-
-def lazy_attribute_sequence(func):
- return LazyAttributeSequence(func)
-
-def container_attribute(func):
- return ContainerAttribute(func, strict=False)
+ method(*passed_args, **passed_kwargs)
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
new file mode 100644
index 0000000..186b4a7
--- /dev/null
+++ b/factory/fuzzy.py
@@ -0,0 +1,86 @@
+# -*- 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.
+
+
+"""Additional declarations for "fuzzy" attribute definitions."""
+
+
+import random
+
+from . import declarations
+
+
+class BaseFuzzyAttribute(declarations.OrderedDeclaration):
+ """Base class for fuzzy attributes.
+
+ Custom fuzzers should override the `fuzz()` method.
+ """
+
+ def fuzz(self):
+ raise NotImplementedError()
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ return self.fuzz()
+
+
+class FuzzyAttribute(BaseFuzzyAttribute):
+ """Similar to LazyAttribute, but yields random values.
+
+ Attributes:
+ function (callable): function taking no parameters and returning a
+ random value.
+ """
+
+ def __init__(self, fuzzer, **kwargs):
+ super(FuzzyAttribute, self).__init__(**kwargs)
+ self.fuzzer = fuzzer
+
+ def fuzz(self):
+ return self.fuzzer()
+
+
+class FuzzyChoice(BaseFuzzyAttribute):
+ """Handles fuzzy choice of an attribute."""
+
+ def __init__(self, choices, **kwargs):
+ self.choices = list(choices)
+ super(FuzzyChoice, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.choice(self.choices)
+
+
+class FuzzyInteger(BaseFuzzyAttribute):
+ """Random integer within a given range."""
+
+ def __init__(self, low, high=None, **kwargs):
+ if high is None:
+ high = low
+ low = 0
+
+ self.low = low
+ self.high = high
+
+ super(FuzzyInteger, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.randint(self.low, self.high)
diff --git a/factory/helpers.py b/factory/helpers.py
new file mode 100644
index 0000000..8f0d161
--- /dev/null
+++ b/factory/helpers.py
@@ -0,0 +1,123 @@
+# -*- 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.
+
+
+"""Simple wrappers around Factory class definition."""
+
+
+from . import base
+from . import declarations
+
+
+def make_factory(klass, **kwargs):
+ """Create a new, simple factory for the given class."""
+ factory_name = '%sFactory' % klass.__name__
+ kwargs[base.FACTORY_CLASS_DECLARATION] = klass
+ base_class = kwargs.pop('FACTORY_CLASS', base.Factory)
+
+ factory_class = type(base.Factory).__new__(
+ type(base.Factory), factory_name, (base_class,), kwargs)
+ factory_class.__name__ = '%sFactory' % klass.__name__
+ factory_class.__doc__ = 'Auto-generated factory for class %s' % klass
+ return factory_class
+
+
+def build(klass, **kwargs):
+ """Create a factory for the given class, and build an instance."""
+ return make_factory(klass, **kwargs).build()
+
+
+def build_batch(klass, size, **kwargs):
+ """Create a factory for the given class, and build a batch of instances."""
+ return make_factory(klass, **kwargs).build_batch(size)
+
+
+def create(klass, **kwargs):
+ """Create a factory for the given class, and create an instance."""
+ return make_factory(klass, **kwargs).create()
+
+
+def create_batch(klass, size, **kwargs):
+ """Create a factory for the given class, and create a batch of instances."""
+ return make_factory(klass, **kwargs).create_batch(size)
+
+
+def stub(klass, **kwargs):
+ """Create a factory for the given class, and stub an instance."""
+ return make_factory(klass, **kwargs).stub()
+
+
+def stub_batch(klass, size, **kwargs):
+ """Create a factory for the given class, and stub a batch of instances."""
+ return make_factory(klass, **kwargs).stub_batch(size)
+
+
+def generate(klass, strategy, **kwargs):
+ """Create a factory for the given class, and generate an instance."""
+ return make_factory(klass, **kwargs).generate(strategy)
+
+
+def generate_batch(klass, strategy, size, **kwargs):
+ """Create a factory for the given class, and generate instances."""
+ return make_factory(klass, **kwargs).generate_batch(strategy, size)
+
+
+# We're reusing 'create' as a keyword.
+# pylint: disable=W0621
+
+
+def simple_generate(klass, create, **kwargs):
+ """Create a factory for the given class, and simple_generate an instance."""
+ return make_factory(klass, **kwargs).simple_generate(create)
+
+
+def simple_generate_batch(klass, create, size, **kwargs):
+ """Create a factory for the given class, and simple_generate instances."""
+ return make_factory(klass, **kwargs).simple_generate_batch(create, size)
+
+
+# pylint: enable=W0621
+
+
+def lazy_attribute(func):
+ return declarations.LazyAttribute(func)
+
+
+def iterator(func):
+ """Turn a generator function into an iterator attribute."""
+ return declarations.Iterator(func())
+
+
+def sequence(func):
+ return declarations.Sequence(func)
+
+
+def lazy_attribute_sequence(func):
+ return declarations.LazyAttributeSequence(func)
+
+
+def container_attribute(func):
+ return declarations.ContainerAttribute(func, strict=False)
+
+
+def post_generation(fun):
+ return declarations.PostGeneration(fun)
diff --git a/factory/utils.py b/factory/utils.py
index 2fcd7ff..f845f46 100644
--- a/factory/utils.py
+++ b/factory/utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -43,7 +43,8 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()):
"""
prefix = prefix + ATTR_SPLITTER
extracted = {}
- for key in kwargs.keys():
+
+ for key in list(kwargs):
if key in exclude:
continue
diff --git a/setup.py b/setup.py
index ace4473..2a43ed4 100755
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,14 @@ class test(cmd.Command):
else:
verbosity=0
- suite = unittest.TestLoader().loadTestsFromName(self.test_suite)
+ 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():
@@ -51,7 +58,7 @@ setup(
author='Mark Sandstrom',
author_email='mark@deliciouslynerdy.com',
maintainer='Raphaël Barrois',
- maintainer_email='raphael.barrois@polytechnique.org',
+ maintainer_email='raphael.barrois+fboy@polytechnique.org',
url='https://github.com/rbarrois/factory_boy',
keywords=['factory_boy', 'factory', 'fixtures'],
packages=['factory'],
@@ -66,6 +73,10 @@ setup(
'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'
],
diff --git a/tests/__init__.py b/tests/__init__.py
index 80a96a4..3c620d6 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011 Raphaël Barrois
+# Copyright (c) 2011-2013 Raphaël Barrois
from .test_base import *
from .test_containers import *
from .test_declarations import *
+from .test_fuzzy import *
from .test_using import *
from .test_utils import *
diff --git a/tests/compat.py b/tests/compat.py
index 15fa3ae..6a1eb80 100644
--- a/tests/compat.py
+++ b/tests/compat.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -21,7 +21,17 @@
"""Compatibility tools for tests"""
+import sys
+
+is_python2 = (sys.version_info[0] == 2)
+
try:
import unittest2 as unittest
except ImportError:
import unittest
+
+if sys.version_info[0:2] < (3, 3):
+ import mock
+else:
+ from unittest import mock
+
diff --git a/tests/test_base.py b/tests/test_base.py
index 7575ee2..216711a 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -44,7 +44,7 @@ class FakeDjangoModel(object):
objects = FakeDjangoManager()
def __init__(self, **kwargs):
- for name, value in kwargs.iteritems():
+ for name, value in kwargs.items():
setattr(self, name, value)
self.id = None
@@ -53,25 +53,42 @@ class TestModel(FakeDjangoModel):
class SafetyTestCase(unittest.TestCase):
- def testBaseFactory(self):
- self.assertRaises(RuntimeError, base.BaseFactory)
+ def test_base_factory(self):
+ self.assertRaises(base.FactoryError, base.BaseFactory)
+
+
+class AbstractFactoryTestCase(unittest.TestCase):
+ def test_factory_for_optional(self):
+ """Ensure that FACTORY_FOR is optional for ABSTRACT_FACTORY."""
+ class TestObjectFactory(base.Factory):
+ ABSTRACT_FACTORY = True
+
+ # Passed
class FactoryTestCase(unittest.TestCase):
- def testDisplay(self):
+ def test_factory_for(self):
+ class TestObjectFactory(base.Factory):
+ FACTORY_FOR = TestObject
+
+ self.assertEqual(TestObject, TestObjectFactory.FACTORY_FOR)
+ obj = TestObjectFactory.build()
+ self.assertFalse(hasattr(obj, 'FACTORY_FOR'))
+
+ def test_display(self):
class TestObjectFactory(base.Factory):
FACTORY_FOR = FakeDjangoModel
self.assertIn('TestObjectFactory', str(TestObjectFactory))
self.assertIn('FakeDjangoModel', str(TestObjectFactory))
- def testLazyAttributeNonExistentParam(self):
+ def test_lazy_attribute_non_existent_param(self):
class TestObjectFactory(base.Factory):
one = declarations.LazyAttribute(lambda a: a.does_not_exist )
self.assertRaises(AttributeError, TestObjectFactory)
- def testInheritanceWithSequence(self):
+ def test_inheritance_with_sequence(self):
"""Tests that sequence IDs are shared between parent and son."""
class TestObjectFactory(base.Factory):
one = declarations.Sequence(lambda a: a)
@@ -86,15 +103,16 @@ class FactoryTestCase(unittest.TestCase):
ones = set([x.one for x in (parent, alt_parent, sub, alt_sub)])
self.assertEqual(4, len(ones))
+
class FactoryDefaultStrategyTestCase(unittest.TestCase):
def setUp(self):
- self.default_strategy = base.Factory.default_strategy
+ self.default_strategy = base.Factory.FACTORY_STRATEGY
def tearDown(self):
- base.Factory.default_strategy = self.default_strategy
+ base.Factory.FACTORY_STRATEGY = self.default_strategy
- def testBuildStrategy(self):
- base.Factory.default_strategy = base.BUILD_STRATEGY
+ def test_build_strategy(self):
+ base.Factory.FACTORY_STRATEGY = base.BUILD_STRATEGY
class TestModelFactory(base.Factory):
one = 'one'
@@ -103,8 +121,8 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
self.assertEqual(test_model.one, 'one')
self.assertFalse(test_model.id)
- def testCreateStrategy(self):
- # Default default_strategy
+ def test_create_strategy(self):
+ # Default FACTORY_STRATEGY
class TestModelFactory(base.Factory):
one = 'one'
@@ -113,8 +131,8 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
self.assertEqual(test_model.one, 'one')
self.assertTrue(test_model.id)
- def testStubStrategy(self):
- base.Factory.default_strategy = base.STUB_STRATEGY
+ def test_stub_strategy(self):
+ base.Factory.FACTORY_STRATEGY = base.STUB_STRATEGY
class TestModelFactory(base.Factory):
one = 'one'
@@ -123,23 +141,23 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
self.assertEqual(test_model.one, 'one')
self.assertFalse(hasattr(test_model, 'id')) # We should have a plain old object
- def testUnknownStrategy(self):
- base.Factory.default_strategy = 'unknown'
+ def test_unknown_strategy(self):
+ base.Factory.FACTORY_STRATEGY = 'unknown'
class TestModelFactory(base.Factory):
one = 'one'
self.assertRaises(base.Factory.UnknownStrategy, TestModelFactory)
- def testStubWithNonStubStrategy(self):
+ def test_stub_with_non_stub_strategy(self):
class TestModelFactory(base.StubFactory):
one = 'one'
- TestModelFactory.default_strategy = base.CREATE_STRATEGY
+ TestModelFactory.FACTORY_STRATEGY = base.CREATE_STRATEGY
self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory)
- TestModelFactory.default_strategy = base.BUILD_STRATEGY
+ TestModelFactory.FACTORY_STRATEGY = base.BUILD_STRATEGY
self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory)
def test_change_strategy(self):
@@ -147,54 +165,35 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase):
class TestModelFactory(base.StubFactory):
one = 'one'
- self.assertEqual(base.CREATE_STRATEGY, TestModelFactory.default_strategy)
+ self.assertEqual(base.CREATE_STRATEGY, TestModelFactory.FACTORY_STRATEGY)
class FactoryCreationTestCase(unittest.TestCase):
- def testFactoryFor(self):
+ def test_factory_for(self):
class TestFactory(base.Factory):
FACTORY_FOR = TestObject
self.assertTrue(isinstance(TestFactory.build(), TestObject))
- def testAutomaticAssociatedClassDiscovery(self):
- class TestObjectFactory(base.Factory):
- pass
-
- self.assertTrue(isinstance(TestObjectFactory.build(), TestObject))
-
- def testDeprecationWarning(self):
- """Make sure the 'auto-discovery' deprecation warning is issued."""
-
- with warnings.catch_warnings(record=True) as w:
- # Clear the warning registry.
- if hasattr(base, '__warningregistry__'):
- base.__warningregistry__.clear()
-
- warnings.simplefilter('always')
- class TestObjectFactory(base.Factory):
- pass
-
- self.assertEqual(1, len(w))
- self.assertIn('deprecated', str(w[0].message))
-
- def testStub(self):
+ def test_stub(self):
class TestFactory(base.StubFactory):
pass
- self.assertEqual(TestFactory.default_strategy, base.STUB_STRATEGY)
+ self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY)
- def testInheritanceWithStub(self):
+ def test_inheritance_with_stub(self):
class TestObjectFactory(base.StubFactory):
pass
class TestFactory(TestObjectFactory):
pass
- self.assertEqual(TestFactory.default_strategy, base.STUB_STRATEGY)
+ self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY)
+
+ def test_custom_creation(self):
+ class TestModelFactory(FakeModelFactory):
+ FACTORY_FOR = TestModel
- def testCustomCreation(self):
- class TestModelFactory(base.Factory):
@classmethod
def _prepare(cls, create, **kwargs):
kwargs['four'] = 4
@@ -212,15 +211,7 @@ class FactoryCreationTestCase(unittest.TestCase):
# Errors
- def testNoAssociatedClassWithAutodiscovery(self):
- try:
- class TestFactory(base.Factory):
- pass
- self.fail()
- except base.Factory.AssociatedClassError as e:
- self.assertTrue('autodiscovery' in str(e))
-
- def testNoAssociatedClassWithoutAutodiscovery(self):
+ def test_no_associated_class(self):
try:
class Test(base.Factory):
pass
diff --git a/tests/test_containers.py b/tests/test_containers.py
index 797c480..7c8d829 100644
--- a/tests/test_containers.py
+++ b/tests/test_containers.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -242,7 +242,7 @@ class AttributeBuilderTestCase(unittest.TestCase):
self.assertEqual({'one': 2}, ab.build(create=False))
def test_factory_defined_sequence(self):
- seq = declarations.Sequence(lambda n: 'xx' + n)
+ seq = declarations.Sequence(lambda n: 'xx%d' % n)
class FakeFactory(object):
@classmethod
@@ -259,7 +259,7 @@ class AttributeBuilderTestCase(unittest.TestCase):
self.assertEqual({'one': 'xx1'}, ab.build(create=False))
def test_additionnal_sequence(self):
- seq = declarations.Sequence(lambda n: 'xx' + n)
+ seq = declarations.Sequence(lambda n: 'xx%d' % n)
class FakeFactory(object):
@classmethod
@@ -276,8 +276,8 @@ class AttributeBuilderTestCase(unittest.TestCase):
self.assertEqual({'one': 1, 'two': 'xx1'}, ab.build(create=False))
def test_replaced_sequence(self):
- seq = declarations.Sequence(lambda n: 'xx' + n)
- seq2 = declarations.Sequence(lambda n: 'yy' + n)
+ seq = declarations.Sequence(lambda n: 'xx%d' % n)
+ seq2 = declarations.Sequence(lambda n: 'yy%d' % n)
class FakeFactory(object):
@classmethod
@@ -331,7 +331,7 @@ class AttributeBuilderTestCase(unittest.TestCase):
ab = containers.AttributeBuilder(FakeFactory, {'one__blah': 1, 'two__bar': 2})
self.assertTrue(ab.has_subfields(sf))
- self.assertEqual(['one'], ab._subfields.keys())
+ self.assertEqual(['one'], list(ab._subfields.keys()))
self.assertEqual(2, ab._attrs['two__bar'])
def test_sub_factory(self):
diff --git a/tests/test_declarations.py b/tests/test_declarations.py
index 1c0502b..4c08dfa 100644
--- a/tests/test_declarations.py
+++ b/tests/test_declarations.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -20,16 +20,21 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+import datetime
+import itertools
+import warnings
-from factory.declarations import deepgetattr, OrderedDeclaration, \
- PostGenerationDeclaration, Sequence
+from factory import declarations
+from factory import helpers
+
+from .compat import mock, unittest
+from . import tools
-from .compat import unittest
class OrderedDeclarationTestCase(unittest.TestCase):
def test_errors(self):
- decl = OrderedDeclaration()
- self.assertRaises(NotImplementedError, decl.evaluate, None, {})
+ decl = declarations.OrderedDeclaration()
+ self.assertRaises(NotImplementedError, decl.evaluate, None, {}, False)
class DigTestCase(unittest.TestCase):
@@ -43,34 +48,234 @@ class DigTestCase(unittest.TestCase):
obj.a.b = self.MyObj(3)
obj.a.b.c = self.MyObj(4)
- self.assertEqual(2, deepgetattr(obj, 'a').n)
- self.assertRaises(AttributeError, deepgetattr, obj, 'b')
- self.assertEqual(2, deepgetattr(obj, 'a.n'))
- self.assertEqual(3, deepgetattr(obj, 'a.c', 3))
- self.assertRaises(AttributeError, deepgetattr, obj, 'a.c.n')
- self.assertRaises(AttributeError, deepgetattr, obj, 'a.d')
- self.assertEqual(3, deepgetattr(obj, 'a.b').n)
- self.assertEqual(3, deepgetattr(obj, 'a.b.n'))
- self.assertEqual(4, deepgetattr(obj, 'a.b.c').n)
- self.assertEqual(4, deepgetattr(obj, 'a.b.c.n'))
- self.assertEqual(42, deepgetattr(obj, 'a.b.c.n.x', 42))
+ self.assertEqual(2, declarations.deepgetattr(obj, 'a').n)
+ self.assertRaises(AttributeError, declarations.deepgetattr, obj, 'b')
+ self.assertEqual(2, declarations.deepgetattr(obj, 'a.n'))
+ self.assertEqual(3, declarations.deepgetattr(obj, 'a.c', 3))
+ self.assertRaises(AttributeError, declarations.deepgetattr, obj, 'a.c.n')
+ self.assertRaises(AttributeError, declarations.deepgetattr, obj, 'a.d')
+ self.assertEqual(3, declarations.deepgetattr(obj, 'a.b').n)
+ self.assertEqual(3, declarations.deepgetattr(obj, 'a.b.n'))
+ self.assertEqual(4, declarations.deepgetattr(obj, 'a.b.c').n)
+ self.assertEqual(4, declarations.deepgetattr(obj, 'a.b.c.n'))
+ self.assertEqual(42, declarations.deepgetattr(obj, 'a.b.c.n.x', 42))
+
+
+class SelfAttributeTestCase(unittest.TestCase):
+ def test_standard(self):
+ a = declarations.SelfAttribute('foo.bar.baz')
+ self.assertEqual(0, a.depth)
+ self.assertEqual('foo.bar.baz', a.attribute_name)
+ self.assertEqual(declarations._UNSPECIFIED, a.default)
+
+ def test_dot(self):
+ a = declarations.SelfAttribute('.bar.baz')
+ self.assertEqual(1, a.depth)
+ self.assertEqual('bar.baz', a.attribute_name)
+ self.assertEqual(declarations._UNSPECIFIED, a.default)
+
+ def test_default(self):
+ a = declarations.SelfAttribute('bar.baz', 42)
+ self.assertEqual(0, a.depth)
+ self.assertEqual('bar.baz', a.attribute_name)
+ self.assertEqual(42, a.default)
+
+ def test_parent(self):
+ a = declarations.SelfAttribute('..bar.baz')
+ self.assertEqual(2, a.depth)
+ self.assertEqual('bar.baz', a.attribute_name)
+ self.assertEqual(declarations._UNSPECIFIED, a.default)
+
+ def test_grandparent(self):
+ a = declarations.SelfAttribute('...bar.baz')
+ self.assertEqual(3, a.depth)
+ self.assertEqual('bar.baz', a.attribute_name)
+ self.assertEqual(declarations._UNSPECIFIED, a.default)
+
+
+class IteratorTestCase(unittest.TestCase):
+ def test_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))
+
+ def test_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)
+
+ def test_getter(self):
+ it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1])
+ self.assertEqual(2, it.evaluate(0, None, False))
+ self.assertEqual(3, it.evaluate(1, None, False))
+ self.assertEqual(2, it.evaluate(2, None, False))
+ self.assertEqual(3, it.evaluate(3, None, False))
class PostGenerationDeclarationTestCase(unittest.TestCase):
def test_extract_no_prefix(self):
- decl = PostGenerationDeclaration()
+ decl = declarations.PostGenerationDeclaration()
extracted, kwargs = decl.extract('foo', {'foo': 13, 'foo__bar': 42})
self.assertEqual(extracted, 13)
self.assertEqual(kwargs, {'bar': 42})
- def test_extract_with_prefix(self):
- decl = PostGenerationDeclaration(extract_prefix='blah')
+ def test_decorator_simple(self):
+ call_params = []
+ @helpers.post_generation
+ def foo(*args, **kwargs):
+ call_params.append(args)
+ call_params.append(kwargs)
- extracted, kwargs = decl.extract('foo',
+ extracted, kwargs = foo.extract('foo',
{'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1})
- self.assertEqual(extracted, 42)
- self.assertEqual(kwargs, {'baz': 1})
+ self.assertEqual(13, extracted)
+ self.assertEqual({'bar': 42}, kwargs)
+
+ # No value returned.
+ foo.call(None, False, extracted, **kwargs)
+ 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):
+
+ def test_arg(self):
+ self.assertRaises(ValueError, declarations.SubFactory, 'UnqualifiedSymbol')
+
+ def test_lazyness(self):
+ f = declarations.SubFactory('factory.declarations.Sequence', x=3)
+ self.assertEqual(None, f.factory)
+
+ self.assertEqual({'x': 3}, f.defaults)
+
+ factory_class = f.get_factory()
+ self.assertEqual(declarations.Sequence, factory_class)
+
+ def test_cache(self):
+ orig_date = datetime.date
+ f = declarations.SubFactory('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
+
+
+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)
+
+ factory_class = f.get_factory()
+ self.assertEqual(declarations.Sequence, factory_class)
+
+ 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
+
+
+class PostGenerationMethodCallTestCase(unittest.TestCase):
+ def setUp(self):
+ self.obj = mock.MagicMock()
+
+ def test_simplest_setup_and_call(self):
+ decl = declarations.PostGenerationMethodCall('method')
+ decl.call(self.obj, False)
+ self.obj.method.assert_called_once_with()
+
+ def test_call_with_method_args(self):
+ decl = declarations.PostGenerationMethodCall(
+ 'method', 'data')
+ decl.call(self.obj, False)
+ 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')
+ 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)
+ 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))
+ 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)
+ 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')
+ 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)
+ 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'))
+ 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'),))
+ 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)
+ self.obj.method.assert_called_once_with('arg1', 'arg2', x=2)
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
new file mode 100644
index 0000000..70a2095
--- /dev/null
+++ b/tests/test_fuzzy.py
@@ -0,0 +1,104 @@
+# -*- 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 factory import fuzzy
+
+from .compat import mock, unittest
+
+
+class FuzzyAttributeTestCase(unittest.TestCase):
+ def test_simple_call(self):
+ d = fuzzy.FuzzyAttribute(lambda: 10)
+
+ res = d.evaluate(2, None, False)
+ self.assertEqual(10, res)
+
+ res = d.evaluate(2, None, False)
+ self.assertEqual(10, res)
+
+
+class FuzzyChoiceTestCase(unittest.TestCase):
+ def test_unbiased(self):
+ options = [1, 2, 3]
+ d = fuzzy.FuzzyChoice(options)
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, options)
+
+ def test_mock(self):
+ options = [1, 2, 3]
+ fake_choice = lambda d: sum(d)
+
+ d = fuzzy.FuzzyChoice(options)
+
+ with mock.patch('random.choice', fake_choice):
+ res = d.evaluate(2, None, False)
+
+ self.assertEqual(6, res)
+
+ def test_generator(self):
+ def options():
+ for i in range(3):
+ yield i
+
+ d = fuzzy.FuzzyChoice(options())
+
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, [0, 1, 2])
+
+ # And repeat
+ res = d.evaluate(2, None, False)
+ self.assertIn(res, [0, 1, 2])
+
+
+class FuzzyIntegerTestCase(unittest.TestCase):
+ def test_definition(self):
+ """Tests all ways of defining a FuzzyInteger."""
+ fuzz = fuzzy.FuzzyInteger(2, 3)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertIn(res, [2, 3])
+
+ fuzz = fuzzy.FuzzyInteger(4)
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertIn(res, [0, 1, 2, 3, 4])
+
+ def test_biased(self):
+ fake_randint = lambda low, high: low + high
+
+ fuzz = fuzzy.FuzzyInteger(2, 8)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(10, res)
+
+ def test_biased_high_only(self):
+ fake_randint = lambda low, high: low + high
+
+ fuzz = fuzzy.FuzzyInteger(8)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(8, res)
diff --git a/tests/test_using.py b/tests/test_using.py
index f4d5440..497e206 100644
--- a/tests/test_using.py
+++ b/tests/test_using.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2011 Raphaël Barrois
+# 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
@@ -23,7 +23,8 @@
import factory
-from .compat import unittest
+from .compat import is_python2, unittest
+from . import tools
class TestObject(object):
@@ -34,17 +35,39 @@ class TestObject(object):
self.four = four
self.five = five
-class FakeDjangoModel(object):
- class FakeDjangoManager(object):
+
+class FakeModel(object):
+ @classmethod
+ def create(cls, **kwargs):
+ instance = cls(**kwargs)
+ instance.id = 1
+ return instance
+
+ class FakeModelManager(object):
+ def get_or_create(self, **kwargs):
+ defaults = kwargs.pop('defaults', {})
+ kwargs.update(defaults)
+ instance = FakeModel.create(**kwargs)
+ instance.id = 2
+ instance._defaults = defaults
+ return instance, True
+
def create(self, **kwargs):
- fake_model = FakeDjangoModel(**kwargs)
- fake_model.id = 1
- return fake_model
+ instance = FakeModel.create(**kwargs)
+ instance.id = 2
+ instance._defaults = None
+ return instance
+
+ def values_list(self, *args, **kwargs):
+ return self
+
+ def order_by(self, *args, **kwargs):
+ return [1]
- objects = FakeDjangoManager()
+ objects = FakeModelManager()
def __init__(self, **kwargs):
- for name, value in kwargs.iteritems():
+ for name, value in kwargs.items():
setattr(self, name, value)
self.id = None
@@ -83,8 +106,13 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(obj.four, None)
def test_create(self):
- obj = factory.create(FakeDjangoModel, foo='bar')
- self.assertEqual(obj.id, 1)
+ obj = factory.create(FakeModel, foo='bar')
+ self.assertEqual(obj.id, None)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_create_custom_base(self):
+ obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory)
+ self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
def test_create_batch(self):
@@ -94,7 +122,18 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(4, len(set(objs)))
for obj in objs:
- self.assertEqual(obj.id, 1)
+ self.assertEqual(obj.id, None)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_create_batch_custom_base(self):
+ objs = factory.create_batch(FakeModel, 4, foo='bar',
+ FACTORY_CLASS=factory.DjangoModelFactory)
+
+ self.assertEqual(4, len(objs))
+ self.assertEqual(4, len(set(objs)))
+
+ for obj in objs:
+ self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
def test_stub(self):
@@ -118,8 +157,14 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(obj.foo, 'bar')
def test_generate_create(self):
- obj = factory.generate(FakeDjangoModel, factory.CREATE_STRATEGY, foo='bar')
- self.assertEqual(obj.id, 1)
+ obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar')
+ self.assertEqual(obj.id, None)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_generate_create_custom_base(self):
+ obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar',
+ FACTORY_CLASS=factory.DjangoModelFactory)
+ self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
def test_generate_stub(self):
@@ -144,7 +189,18 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(20, len(set(objs)))
for obj in objs:
- self.assertEqual(obj.id, 1)
+ self.assertEqual(obj.id, None)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_generate_batch_create_custom_base(self):
+ objs = factory.generate_batch(FakeModel, factory.CREATE_STRATEGY, 20, foo='bar',
+ FACTORY_CLASS=factory.DjangoModelFactory)
+
+ self.assertEqual(20, len(objs))
+ self.assertEqual(20, len(set(objs)))
+
+ for obj in objs:
+ self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
def test_generate_batch_stub(self):
@@ -163,8 +219,13 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(obj.foo, 'bar')
def test_simple_generate_create(self):
- obj = factory.simple_generate(FakeDjangoModel, True, foo='bar')
- self.assertEqual(obj.id, 1)
+ obj = factory.simple_generate(FakeModel, True, foo='bar')
+ self.assertEqual(obj.id, None)
+ 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)
+ self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
def test_simple_generate_batch_build(self):
@@ -184,7 +245,18 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(20, len(set(objs)))
for obj in objs:
- self.assertEqual(obj.id, 1)
+ self.assertEqual(obj.id, None)
+ self.assertEqual(obj.foo, 'bar')
+
+ def test_simple_generate_batch_create_custom_base(self):
+ objs = factory.simple_generate_batch(FakeModel, True, 20, foo='bar',
+ FACTORY_CLASS=factory.DjangoModelFactory)
+
+ self.assertEqual(20, len(objs))
+ self.assertEqual(20, len(set(objs)))
+
+ for obj in objs:
+ self.assertEqual(obj.id, 2)
self.assertEqual(obj.foo, 'bar')
def test_make_factory(self):
@@ -204,13 +276,23 @@ class SimpleBuildTestCase(unittest.TestCase):
class UsingFactoryTestCase(unittest.TestCase):
- def testAttribute(self):
+ def test_attribute(self):
class TestObjectFactory(factory.Factory):
one = 'one'
test_object = TestObjectFactory.build()
self.assertEqual(test_object.one, 'one')
+ def test_inheritance(self):
+ @factory.use_strategy(factory.BUILD_STRATEGY)
+ class TestObjectFactory(factory.Factory, TestObject):
+ FACTORY_FOR = TestObject
+
+ one = 'one'
+
+ test_object = TestObjectFactory()
+ self.assertEqual(test_object.one, 'one')
+
def test_abstract(self):
class SomeAbstractFactory(factory.Factory):
ABSTRACT_FACTORY = True
@@ -222,10 +304,12 @@ class UsingFactoryTestCase(unittest.TestCase):
test_object = InheritedFactory.build()
self.assertEqual(test_object.one, 'one')
- def testSequence(self):
+ def test_sequence(self):
class TestObjectFactory(factory.Factory):
- one = factory.Sequence(lambda n: 'one' + n)
- two = factory.Sequence(lambda n: 'two' + n)
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: 'one%d' % n)
+ two = factory.Sequence(lambda n: 'two%d' % n)
test_object0 = TestObjectFactory.build()
self.assertEqual(test_object0.one, 'one0')
@@ -235,14 +319,14 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(test_object1.one, 'one1')
self.assertEqual(test_object1.two, 'two1')
- def testSequenceCustomBegin(self):
+ def test_sequence_custom_begin(self):
class TestObjectFactory(factory.Factory):
@classmethod
def _setup_next_sequence(cls):
return 42
- one = factory.Sequence(lambda n: 'one' + n)
- two = factory.Sequence(lambda n: 'two' + n)
+ one = factory.Sequence(lambda n: 'one%d' % n)
+ two = factory.Sequence(lambda n: 'two%d' % n)
test_object0 = TestObjectFactory.build()
self.assertEqual('one42', test_object0.one)
@@ -252,10 +336,61 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual('one43', test_object1.one)
self.assertEqual('two43', test_object1.two)
+ def test_sequence_override(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: 'one%d' % n)
+
+ o1 = TestObjectFactory()
+ o2 = TestObjectFactory()
+ o3 = TestObjectFactory(__sequence=42)
+ o4 = TestObjectFactory()
+
+ self.assertEqual('one0', o1.one)
+ self.assertEqual('one1', o2.one)
+ self.assertEqual('one42', o3.one)
+ self.assertEqual('one2', o4.one)
+
+ def test_custom_create(self):
+ class TestModelFactory(factory.Factory):
+ FACTORY_FOR = TestModel
+
+ two = 2
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ obj = target_class.create(**kwargs)
+ obj.properly_created = True
+ return obj
+
+ obj = TestModelFactory.create(one=1)
+ self.assertEqual(1, obj.one)
+ self.assertEqual(2, obj.two)
+ self.assertEqual(1, obj.id)
+ self.assertTrue(obj.properly_created)
+
+ def test_non_django_create(self):
+ class NonDjango(object):
+ def __init__(self, x, y=2):
+ self.x = x
+ self.y = y
+
+ class NonDjangoFactory(factory.Factory):
+ FACTORY_FOR = NonDjango
+
+ x = 3
+
+ obj = NonDjangoFactory.create()
+ self.assertEqual(3, obj.x)
+ self.assertEqual(2, obj.y)
+
def test_sequence_batch(self):
class TestObjectFactory(factory.Factory):
- one = factory.Sequence(lambda n: 'one' + n)
- two = factory.Sequence(lambda n: 'two' + n)
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: 'one%d' % n)
+ two = factory.Sequence(lambda n: 'two%d' % n)
objs = TestObjectFactory.build_batch(20)
@@ -265,7 +400,7 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual('one%d' % i, obj.one)
self.assertEqual('two%d' % i, obj.two)
- def testLazyAttribute(self):
+ def test_lazy_attribute(self):
class TestObjectFactory(factory.Factory):
one = factory.LazyAttribute(lambda a: 'abc' )
two = factory.LazyAttribute(lambda a: a.one + ' xyz')
@@ -274,10 +409,12 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(test_object.one, 'abc')
self.assertEqual(test_object.two, 'abc xyz')
- def testLazyAttributeSequence(self):
+ def test_lazy_attribute_sequence(self):
class TestObjectFactory(factory.Factory):
- one = factory.LazyAttributeSequence(lambda a, n: 'abc' + n)
- two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz' + n)
+ FACTORY_FOR = TestObject
+
+ one = factory.LazyAttributeSequence(lambda a, n: 'abc%d' % n)
+ two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz%d' % n)
test_object0 = TestObjectFactory.build()
self.assertEqual(test_object0.one, 'abc0')
@@ -287,7 +424,7 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(test_object1.one, 'abc1')
self.assertEqual(test_object1.two, 'abc1 xyz1')
- def testLazyAttributeDecorator(self):
+ def test_lazy_attribute_decorator(self):
class TestObjectFactory(factory.Factory):
@factory.lazy_attribute
def one(a):
@@ -296,7 +433,7 @@ class UsingFactoryTestCase(unittest.TestCase):
test_object = TestObjectFactory.build()
self.assertEqual(test_object.one, 'one')
- def testSelfAttribute(self):
+ def test_self_attribute(self):
class TmpObj(object):
n = 3
@@ -313,32 +450,51 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(3, test_object.four)
self.assertEqual(5, test_object.five)
- def testSequenceDecorator(self):
+ def test_self_attribute_parent(self):
+ class TestModel2(FakeModel):
+ pass
+
+ class TestModelFactory(FakeModelFactory):
+ FACTORY_FOR = TestModel
+ one = 3
+ three = factory.SelfAttribute('..bar')
+
+ class TestModel2Factory(FakeModelFactory):
+ FACTORY_FOR = TestModel2
+ bar = 4
+ two = factory.SubFactory(TestModelFactory, one=1)
+
+ test_model = TestModel2Factory()
+ self.assertEqual(4, test_model.two.three)
+
+ def test_sequence_decorator(self):
class TestObjectFactory(factory.Factory):
@factory.sequence
def one(n):
- return 'one' + n
+ return 'one%d' % n
test_object = TestObjectFactory.build()
self.assertEqual(test_object.one, 'one0')
- def testLazyAttributeSequenceDecorator(self):
+ def test_lazy_attribute_sequence_decorator(self):
class TestObjectFactory(factory.Factory):
@factory.lazy_attribute_sequence
def one(a, n):
- return 'one' + n
+ return 'one%d' % n
@factory.lazy_attribute_sequence
def two(a, n):
- return a.one + ' two' + n
+ return a.one + ' two%d' % n
test_object = TestObjectFactory.build()
self.assertEqual(test_object.one, 'one0')
self.assertEqual(test_object.two, 'one0 two0')
- def testBuildWithParameters(self):
+ def test_build_with_parameters(self):
class TestObjectFactory(factory.Factory):
- one = factory.Sequence(lambda n: 'one' + n)
- two = factory.Sequence(lambda n: 'two' + n)
+ FACTORY_FOR = TestObject
+
+ one = factory.Sequence(lambda n: 'one%d' % n)
+ two = factory.Sequence(lambda n: 'two%d' % n)
test_object0 = TestObjectFactory.build(three='three')
self.assertEqual(test_object0.one, 'one0')
@@ -349,8 +505,10 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(test_object1.one, 'other')
self.assertEqual(test_object1.two, 'two1')
- def testCreate(self):
- class TestModelFactory(factory.Factory):
+ def test_create(self):
+ class TestModelFactory(FakeModelFactory):
+ FACTORY_FOR = TestModel
+
one = 'one'
test_model = TestModelFactory.create()
@@ -488,7 +646,7 @@ class UsingFactoryTestCase(unittest.TestCase):
three = factory.Sequence(lambda n: int(n))
objs = TestObjectFactory.stub_batch(20,
- one=factory.Sequence(lambda n: n))
+ one=factory.Sequence(lambda n: str(n)))
self.assertEqual(20, len(objs))
self.assertEqual(20, len(set(objs)))
@@ -498,7 +656,7 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual('%d two' % i, obj.two)
self.assertEqual(i, obj.three)
- def testInheritance(self):
+ def test_inheritance(self):
class TestObjectFactory(factory.Factory):
one = 'one'
two = factory.LazyAttribute(lambda a: a.one + ' two')
@@ -518,7 +676,7 @@ class UsingFactoryTestCase(unittest.TestCase):
test_object_alt = TestObjectFactory.build()
self.assertEqual(None, test_object_alt.three)
- def testInheritanceWithInheritedClass(self):
+ def test_inheritance_with_inherited_class(self):
class TestObjectFactory(factory.Factory):
one = 'one'
two = factory.LazyAttribute(lambda a: a.one + ' two')
@@ -533,7 +691,7 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(test_object.three, 'three')
self.assertEqual(test_object.four, 'three four')
- def testDualInheritance(self):
+ def test_dual_inheritance(self):
class TestObjectFactory(factory.Factory):
one = 'one'
@@ -551,19 +709,7 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual('three', obj.three)
self.assertEqual('four', obj.four)
- def testSetCreationFunction(self):
- def creation_function(class_to_create, **kwargs):
- return "This doesn't even return an instance of {0}".format(class_to_create.__name__)
-
- class TestModelFactory(factory.Factory):
- pass
-
- TestModelFactory.set_creation_function(creation_function)
-
- test_object = TestModelFactory.create()
- self.assertEqual(test_object, "This doesn't even return an instance of TestModel")
-
- def testClassMethodAccessible(self):
+ def test_class_method_accessible(self):
class TestObjectFactory(factory.Factory):
@classmethod
def alt_create(cls, **kwargs):
@@ -571,7 +717,7 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1})
- def testStaticMethodAccessible(self):
+ def test_static_method_accessible(self):
class TestObjectFactory(factory.Factory):
@staticmethod
def alt_create(**kwargs):
@@ -579,10 +725,140 @@ class UsingFactoryTestCase(unittest.TestCase):
self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1})
+ def test_arg_parameters(self):
+ class TestObject(object):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ FACTORY_ARG_PARAMETERS = ('x', 'y')
+
+ x = 1
+ y = 2
+ z = 3
+ t = 4
+
+ obj = TestObjectFactory.build(x=42, z=5)
+ self.assertEqual((42, 2), obj.args)
+ self.assertEqual({'z': 5, 't': 4}, obj.kwargs)
+
+ def test_hidden_args(self):
+ class TestObject(object):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ FACTORY_HIDDEN_ARGS = ('x', 'z')
+
+ x = 1
+ y = 2
+ z = 3
+ t = 4
+
+ obj = TestObjectFactory.build(x=42, z=5)
+ self.assertEqual((), obj.args)
+ self.assertEqual({'y': 2, 't': 4}, obj.kwargs)
+
+ def test_hidden_args_and_arg_parameters(self):
+ class TestObject(object):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ FACTORY_HIDDEN_ARGS = ('x', 'z')
+ FACTORY_ARG_PARAMETERS = ('y',)
+
+ x = 1
+ y = 2
+ z = 3
+ t = 4
+
+ obj = TestObjectFactory.build(x=42, z=5)
+ self.assertEqual((2,), obj.args)
+ self.assertEqual({'t': 4}, obj.kwargs)
+
+
+
+class NonKwargParametersTestCase(unittest.TestCase):
+ def test_build(self):
+ class TestObject(object):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ FACTORY_ARG_PARAMETERS = ('one', 'two',)
+
+ one = 1
+ two = 2
+ three = 3
+
+ obj = TestObjectFactory.build()
+ self.assertEqual((1, 2), obj.args)
+ self.assertEqual({'three': 3}, obj.kwargs)
+
+ def test_create(self):
+ class TestObject(object):
+ def __init__(self, *args, **kwargs):
+ self.args = None
+ self.kwargs = None
+
+ @classmethod
+ def create(cls, *args, **kwargs):
+ inst = cls()
+ inst.args = args
+ inst.kwargs = kwargs
+ return inst
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ FACTORY_ARG_PARAMETERS = ('one', 'two')
+
+ one = 1
+ two = 2
+ three = 3
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ return target_class.create(*args, **kwargs)
+
+ obj = TestObjectFactory.create()
+ self.assertEqual((1, 2), obj.args)
+ self.assertEqual({'three': 3}, obj.kwargs)
+
+
+class KwargAdjustTestCase(unittest.TestCase):
+ """Tests for the _adjust_kwargs method."""
+
+ def test_build(self):
+ class TestObject(object):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ @classmethod
+ def _adjust_kwargs(cls, **kwargs):
+ kwargs['foo'] = len(kwargs)
+ return kwargs
+
+ obj = TestObjectFactory.build(x=1, y=2, z=3)
+ self.assertEqual({'x': 1, 'y': 2, 'z': 3, 'foo': 3}, obj.kwargs)
+ self.assertEqual((), obj.args)
+
class SubFactoryTestCase(unittest.TestCase):
- def testSubFactory(self):
- class TestModel2(FakeDjangoModel):
+ def test_sub_factory(self):
+ class TestModel2(FakeModel):
pass
class TestModelFactory(factory.Factory):
@@ -598,8 +874,8 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual(1, test_model.id)
self.assertEqual(1, test_model.two.id)
- def testSubFactoryWithLazyFields(self):
- class TestModel2(FakeDjangoModel):
+ def test_sub_factory_with_lazy_fields(self):
+ class TestModel2(FakeModel):
pass
class TestModelFactory(factory.Factory):
@@ -608,7 +884,7 @@ class SubFactoryTestCase(unittest.TestCase):
class TestModel2Factory(factory.Factory):
FACTORY_FOR = TestModel2
two = factory.SubFactory(TestModelFactory,
- one=factory.Sequence(lambda n: 'x%sx' % n),
+ one=factory.Sequence(lambda n: 'x%dx' % n),
two=factory.LazyAttribute(
lambda o: '%s%s' % (o.one, o.one)))
@@ -616,10 +892,10 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual('x0x', test_model.two.one)
self.assertEqual('x0xx0x', test_model.two.two)
- def testSubFactoryAndSequence(self):
+ def test_sub_factory_and_sequence(self):
class TestObject(object):
def __init__(self, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in kwargs.items():
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
@@ -637,10 +913,10 @@ class SubFactoryTestCase(unittest.TestCase):
wrapping = WrappingTestObjectFactory.build()
self.assertEqual(1, wrapping.wrapped.one)
- def testSubFactoryOverriding(self):
+ def test_sub_factory_overriding(self):
class TestObject(object):
def __init__(self, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in kwargs.items():
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
@@ -649,7 +925,7 @@ class SubFactoryTestCase(unittest.TestCase):
class OtherTestObject(object):
def __init__(self, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in kwargs.items():
setattr(self, k, v)
class WrappingTestObjectFactory(factory.Factory):
@@ -664,12 +940,12 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual(wrapping.wrapped.three, 3)
self.assertEqual(wrapping.wrapped.four, 4)
- def testNestedSubFactory(self):
+ def test_nested_sub_factory(self):
"""Test nested sub-factories."""
class TestObject(object):
def __init__(self, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in kwargs.items():
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
@@ -690,12 +966,12 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual(outer.wrap.wrapped.two, 2)
self.assertEqual(outer.wrap.wrapped_bis.one, 1)
- def testNestedSubFactoryWithOverriddenSubFactories(self):
+ def test_nested_sub_factory_with_overridden_sub_factories(self):
"""Test nested sub-factories, with attributes overridden with subfactories."""
class TestObject(object):
def __init__(self, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in kwargs.items():
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
@@ -718,11 +994,11 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual(outer.wrap.wrapped.two.four, 4)
self.assertEqual(outer.wrap.friend, 5)
- def testSubFactoryAndInheritance(self):
+ def test_sub_factory_and_inheritance(self):
"""Test inheriting from a factory with subfactories, overriding."""
class TestObject(object):
def __init__(self, **kwargs):
- for k, v in kwargs.iteritems():
+ for k, v in kwargs.items():
setattr(self, k, v)
class TestObjectFactory(factory.Factory):
@@ -742,7 +1018,7 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual(wrapping.wrapped.two, 4)
self.assertEqual(wrapping.friend, 5)
- def testDiamondSubFactory(self):
+ def test_diamond_sub_factory(self):
"""Tests the case where an object has two fields with a common field."""
class InnerMost(object):
def __init__(self, a, b):
@@ -802,33 +1078,33 @@ class IteratorTestCase(unittest.TestCase):
def test_iterator(self):
class TestObjectFactory(factory.Factory):
- one = factory.Iterator(xrange(10, 30))
+ FACTORY_FOR = TestObject
+
+ one = factory.Iterator(range(10, 30))
objs = TestObjectFactory.build_batch(20)
for i, obj in enumerate(objs):
self.assertEqual(i + 10, obj.one)
- def test_infinite_iterator(self):
+ @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+")
+ @tools.disable_warnings
+ def test_iterator_list_comprehension_scope_bleeding(self):
class TestObjectFactory(factory.Factory):
- one = factory.InfiniteIterator(xrange(5))
-
- objs = TestObjectFactory.build_batch(20)
-
- for i, obj in enumerate(objs):
- self.assertEqual(i % 5, obj.one)
+ FACTORY_FOR = TestObject
- def test_infinite_iterator_list_comprehension(self):
- class TestObjectFactory(factory.Factory):
- one = factory.InfiniteIterator([j * 3 for j in xrange(5)])
+ one = factory.Iterator([j * 3 for j in range(5)])
# Scope bleeding: j will end up in TestObjectFactory's scope.
self.assertRaises(TypeError, TestObjectFactory.build)
- def test_infinite_iterator_list_comprehension_protected(self):
+ @tools.disable_warnings
+ def test_iterator_list_comprehension_protected(self):
class TestObjectFactory(factory.Factory):
- one = factory.InfiniteIterator([_j * 3 for _j in xrange(5)])
+ FACTORY_FOR = TestObject
+
+ one = factory.Iterator([_j * 3 for _j in range(5)])
# Scope bleeding : _j will end up in TestObjectFactory's scope.
# But factory_boy ignores it, as a protected variable.
@@ -841,7 +1117,7 @@ class IteratorTestCase(unittest.TestCase):
class TestObjectFactory(factory.Factory):
@factory.iterator
def one():
- for i in xrange(10, 50):
+ for i in range(10, 50):
yield i
objs = TestObjectFactory.build_batch(20)
@@ -849,17 +1125,133 @@ class IteratorTestCase(unittest.TestCase):
for i, obj in enumerate(objs):
self.assertEqual(i + 10, obj.one)
- def test_infinite_iterator_decorator(self):
- class TestObjectFactory(factory.Factory):
- @factory.infinite_iterator
- def one():
- for i in xrange(5):
- yield i
- objs = TestObjectFactory.build_batch(20)
+class BetterFakeModelManager(object):
+ def __init__(self, keys, instance):
+ self.keys = keys
+ self.instance = instance
- for i, obj in enumerate(objs):
- self.assertEqual(i % 5, obj.one)
+ def get_or_create(self, **kwargs):
+ defaults = kwargs.pop('defaults', {})
+ if kwargs == self.keys:
+ return self.instance, False
+ kwargs.update(defaults)
+ instance = FakeModel.create(**kwargs)
+ instance.id = 2
+ return instance, True
+
+ def values_list(self, *args, **kwargs):
+ return self
+
+ def order_by(self, *args, **kwargs):
+ return [1]
+
+
+class BetterFakeModel(object):
+ @classmethod
+ def create(cls, **kwargs):
+ instance = cls(**kwargs)
+ instance.id = 1
+ return instance
+
+ def __init__(self, **kwargs):
+ for name, value in kwargs.items():
+ setattr(self, name, value)
+ self.id = None
+
+
+class DjangoModelFactoryTestCase(unittest.TestCase):
+ def test_simple(self):
+ class FakeModelFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = FakeModel
+
+ obj = FakeModelFactory(one=1)
+ self.assertEqual(1, obj.one)
+ self.assertEqual(2, obj.id)
+
+ def test_existing_instance(self):
+ prev = BetterFakeModel.create(x=1, y=2, z=3)
+ prev.id = 42
+
+ class MyFakeModel(BetterFakeModel):
+ objects = BetterFakeModelManager({'x': 1}, prev)
+
+ class MyFakeModelFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = MyFakeModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('x',)
+ x = 1
+ y = 4
+ z = 6
+
+ obj = MyFakeModelFactory()
+ self.assertEqual(prev, obj)
+ self.assertEqual(1, obj.x)
+ self.assertEqual(2, obj.y)
+ self.assertEqual(3, obj.z)
+ self.assertEqual(42, obj.id)
+
+ def test_existing_instance_complex_key(self):
+ prev = BetterFakeModel.create(x=1, y=2, z=3)
+ prev.id = 42
+
+ class MyFakeModel(BetterFakeModel):
+ objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev)
+
+ class MyFakeModelFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = MyFakeModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z')
+ x = 1
+ y = 4
+ z = 6
+
+ obj = MyFakeModelFactory(y=2, z=3)
+ self.assertEqual(prev, obj)
+ self.assertEqual(1, obj.x)
+ self.assertEqual(2, obj.y)
+ self.assertEqual(3, obj.z)
+ self.assertEqual(42, obj.id)
+
+ def test_new_instance(self):
+ prev = BetterFakeModel.create(x=1, y=2, z=3)
+ prev.id = 42
+
+ class MyFakeModel(BetterFakeModel):
+ objects = BetterFakeModelManager({'x': 1}, prev)
+
+ class MyFakeModelFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = MyFakeModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('x',)
+ x = 1
+ y = 4
+ z = 6
+
+ obj = MyFakeModelFactory(x=2)
+ self.assertNotEqual(prev, obj)
+ self.assertEqual(2, obj.x)
+ self.assertEqual(4, obj.y)
+ self.assertEqual(6, obj.z)
+ self.assertEqual(2, obj.id)
+
+ def test_new_instance_complex_key(self):
+ prev = BetterFakeModel.create(x=1, y=2, z=3)
+ prev.id = 42
+
+ class MyFakeModel(BetterFakeModel):
+ objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev)
+
+ class MyFakeModelFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = MyFakeModel
+ FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z')
+ x = 1
+ y = 4
+ z = 6
+
+ obj = MyFakeModelFactory(y=2, z=4)
+ self.assertNotEqual(prev, obj)
+ self.assertEqual(1, obj.x)
+ self.assertEqual(2, obj.y)
+ self.assertEqual(4, obj.z)
+ self.assertEqual(2, obj.id)
class PostGenerationTestCase(unittest.TestCase):
@@ -867,7 +1259,7 @@ class PostGenerationTestCase(unittest.TestCase):
class TestObjectFactory(factory.Factory):
one = 1
- @factory.post_generation()
+ @factory.post_generation
def incr_one(self, _create, _increment):
self.one += 1
@@ -879,11 +1271,32 @@ class PostGenerationTestCase(unittest.TestCase):
self.assertEqual(3, obj.one)
self.assertFalse(hasattr(obj, 'incr_one'))
+ def test_post_generation_hook(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+
+ one = 1
+
+ @factory.post_generation
+ def incr_one(self, _create, _increment):
+ self.one += 1
+ return 42
+
+ @classmethod
+ def _after_postgeneration(cls, obj, create, results):
+ obj.create = create
+ obj.results = results
+
+ obj = TestObjectFactory.build()
+ self.assertEqual(2, obj.one)
+ self.assertFalse(obj.create)
+ self.assertEqual({'incr_one': 42}, obj.results)
+
def test_post_generation_extraction(self):
class TestObjectFactory(factory.Factory):
one = 1
- @factory.post_generation()
+ @factory.post_generation
def incr_one(self, _create, increment=1):
self.one += increment
@@ -952,6 +1365,305 @@ class PostGenerationTestCase(unittest.TestCase):
# RelatedFactory received "parent" object
self.assertEqual(obj, obj.related.three)
+ def test_related_factory_no_name(self):
+ relateds = []
+ class TestRelatedObject(object):
+ def __init__(self, obj=None, one=None, two=None):
+ relateds.append(self)
+ self.one = one
+ self.two = two
+ self.three = obj
+
+ class TestRelatedObjectFactory(factory.Factory):
+ FACTORY_FOR = TestRelatedObject
+ one = 1
+ two = factory.LazyAttribute(lambda o: o.one + 1)
+
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = 3
+ two = 2
+ three = factory.RelatedFactory(TestRelatedObjectFactory)
+
+ obj = TestObjectFactory.build()
+ # Normal fields
+ self.assertEqual(3, obj.one)
+ self.assertEqual(2, obj.two)
+ # RelatedFactory was built
+ self.assertIsNone(obj.three)
+ self.assertEqual(1, len(relateds))
+ related = relateds[0]
+ self.assertEqual(1, related.one)
+ self.assertEqual(2, related.two)
+ self.assertIsNone(related.three)
+
+ obj = TestObjectFactory.build(three__one=3)
+ # Normal fields
+ self.assertEqual(3, obj.one)
+ self.assertEqual(2, obj.two)
+ # RelatedFactory was build
+ self.assertIsNone(obj.three)
+ self.assertEqual(2, len(relateds))
+
+ related = relateds[1]
+ self.assertEqual(3, related.one)
+ self.assertEqual(4, related.two)
+
+
+class CircularTestCase(unittest.TestCase):
+ def test_example(self):
+ sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
+
+ from .cyclic import foo
+ f = foo.FooFactory.build(bar__foo=None)
+ self.assertEqual(42, f.x)
+ self.assertEqual(13, f.bar.y)
+ self.assertIsNone(f.bar.foo)
+
+ from .cyclic import bar
+ b = bar.BarFactory.build(foo__bar__foo__bar=None)
+ self.assertEqual(13, b.y)
+ self.assertEqual(42, b.foo.x)
+ self.assertEqual(13, b.foo.bar.y)
+ self.assertEqual(42, b.foo.bar.foo.x)
+ self.assertIsNone(b.foo.bar.foo.bar)
+
+
+class DictTestCase(unittest.TestCase):
+ def test_empty_dict(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.Dict({})
+
+ o = TestObjectFactory()
+ self.assertEqual({}, o.one)
+
+ def test_naive_dict(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.Dict({'a': 1})
+
+ o = TestObjectFactory()
+ self.assertEqual({'a': 1}, o.one)
+
+ def test_sequence_dict(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.Dict({'a': factory.Sequence(lambda n: n + 2)})
+
+ o1 = TestObjectFactory()
+ o2 = TestObjectFactory()
+
+ self.assertEqual({'a': 2}, o1.one)
+ self.assertEqual({'a': 3}, o2.one)
+
+ def test_dict_override(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.Dict({'a': 1})
+
+ o = TestObjectFactory(one__a=2)
+ self.assertEqual({'a': 2}, o.one)
+
+ def test_dict_extra_key(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.Dict({'a': 1})
+
+ o = TestObjectFactory(one__b=2)
+ self.assertEqual({'a': 1, 'b': 2}, o.one)
+
+ def test_dict_merged_fields(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ two = 13
+ one = factory.Dict({
+ 'one': 1,
+ 'two': 2,
+ 'three': factory.SelfAttribute('two'),
+ })
+
+ o = TestObjectFactory(one__one=42)
+ self.assertEqual({'one': 42, 'two': 2, 'three': 2}, o.one)
+
+ def test_nested_dicts(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = 1
+ two = factory.Dict({
+ 'one': 3,
+ 'two': factory.SelfAttribute('one'),
+ 'three': factory.Dict({
+ 'one': 5,
+ 'two': factory.SelfAttribute('..one'),
+ 'three': factory.SelfAttribute('...one'),
+ }),
+ })
+
+ o = TestObjectFactory()
+ self.assertEqual(1, o.one)
+ self.assertEqual({
+ 'one': 3,
+ 'two': 3,
+ 'three': {
+ 'one': 5,
+ 'two': 3,
+ 'three': 1,
+ },
+ }, o.two)
+
+
+class ListTestCase(unittest.TestCase):
+ def test_empty_list(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.List([])
+
+ o = TestObjectFactory()
+ self.assertEqual([], o.one)
+
+ def test_naive_list(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.List([1])
+
+ o = TestObjectFactory()
+ self.assertEqual([1], o.one)
+
+ def test_sequence_list(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.List([factory.Sequence(lambda n: n + 2)])
+
+ o1 = TestObjectFactory()
+ o2 = TestObjectFactory()
+
+ self.assertEqual([2], o1.one)
+ self.assertEqual([3], o2.one)
+
+ def test_list_override(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.List([1])
+
+ o = TestObjectFactory(one__0=2)
+ self.assertEqual([2], o.one)
+
+ def test_list_extra_key(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = factory.List([1])
+
+ o = TestObjectFactory(one__1=2)
+ self.assertEqual([1, 2], o.one)
+
+ def test_list_merged_fields(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ two = 13
+ one = factory.List([
+ 1,
+ 2,
+ factory.SelfAttribute('1'),
+ ])
+
+ o = TestObjectFactory(one__0=42)
+ self.assertEqual([42, 2, 2], o.one)
+
+ def test_nested_lists(self):
+ class TestObjectFactory(factory.Factory):
+ FACTORY_FOR = TestObject
+ one = 1
+ two = factory.List([
+ 3,
+ factory.SelfAttribute('0'),
+ factory.List([
+ 5,
+ factory.SelfAttribute('..0'),
+ factory.SelfAttribute('...one'),
+ ]),
+ ])
+
+ o = TestObjectFactory()
+ self.assertEqual(1, o.one)
+ self.assertEqual([
+ 3,
+ 3,
+ [
+ 5,
+ 3,
+ 1,
+ ],
+ ], 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__':
unittest.main()
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 6fd6ee2..787164a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2010 Mark Sandstrom
-# Copyright (c) 2011 Raphaël Barrois
+# 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
diff --git a/tests/tools.py b/tests/tools.py
new file mode 100644
index 0000000..571899b
--- /dev/null
+++ b/tests/tools.py
@@ -0,0 +1,36 @@
+# -*- 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 functools
+import warnings
+
+
+def disable_warnings(fun):
+ @functools.wraps(fun)
+ def decorated(*args, **kwargs):
+ with warnings.catch_warnings():
+ warnings.simplefilter('ignore')
+ return fun(*args, **kwargs)
+ return decorated
+
+
diff --git a/tox.ini b/tox.ini
index 944070f..2200f56 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,9 +3,15 @@ envlist = py26,py27,pypy
[testenv]
commands=
- python setup.py test
+ python -W default setup.py test
[testenv:py26]
deps=
+ mock
unittest2
+
+[textenv:py27]
+
+deps=
+ mock