summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--MANIFEST.in18
-rw-r--r--Makefile43
-rw-r--r--README.rst59
-rw-r--r--docs/changelog.rst24
-rw-r--r--docs/conf.py2
-rw-r--r--docs/examples.rst2
-rw-r--r--docs/fuzzy.rst8
-rw-r--r--docs/introduction.rst84
-rw-r--r--docs/orms.rst24
-rw-r--r--docs/recipes.rst47
-rw-r--r--docs/reference.rst248
-rw-r--r--factory/__init__.py11
-rw-r--r--factory/alchemy.py3
-rw-r--r--factory/base.py75
-rw-r--r--factory/compat.py8
-rw-r--r--factory/containers.py116
-rw-r--r--factory/declarations.py66
-rw-r--r--factory/errors.py42
-rw-r--r--factory/fuzzy.py5
-rw-r--r--factory/utils.py6
-rw-r--r--requirements_dev.txt (renamed from dev_requirements.txt)8
-rw-r--r--requirements_test.txt2
-rwxr-xr-xsetup.py2
-rw-r--r--tests/test_alchemy.py32
-rw-r--r--tests/test_base.py22
-rw-r--r--tests/test_containers.py129
-rw-r--r--tests/test_django.py16
-rw-r--r--tests/test_fuzzy.py13
-rw-r--r--tests/test_using.py186
-rw-r--r--tox.ini39
31 files changed, 1106 insertions, 236 deletions
diff --git a/.travis.yml b/.travis.yml
index e1600bd..938787b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,10 @@
+sudo: false
language: python
python:
- "2.7"
- "3.4"
+ - "3.5"
- "pypy"
script:
diff --git a/MANIFEST.in b/MANIFEST.in
index 3dfc1be..19b0f8c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,12 @@
-include README.rst
-include docs/Makefile
-recursive-include docs *.py *.rst
-include docs/_static/.keep_dir
-prune docs/_build
-recursive-include tests *.py *.data
+include ChangeLog LICENSE README.rst
+include requirements*.txt
+
+graft factory
+
+prune docs
+prune examples
+prune tests
+
+global-exclude .py[cod] __pycache__
+
+exclude Makefile tox.ini .pylintrc
diff --git a/Makefile b/Makefile
index 35f635c..8f48c6b 100644
--- a/Makefile
+++ b/Makefile
@@ -6,55 +6,38 @@ EXAMPLES_DIR=examples
# Use current python binary instead of system default.
COVERAGE = python $(shell which coverage)
-# Dependencies
-DJANGO ?= 1.8
-NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
-
-ALCHEMY ?= 1.0
-NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
-
-MONGOENGINE ?= 0.9
-NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
-
-REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt
-EXAMPLES_REQ_FILES = $(shell find $(EXAMPLES_DIR) -name requirements.txt)
-
all: default
default:
-install-deps: $(REQ_FILE)
- pip install --upgrade pip setuptools
- pip install --upgrade -r $<
- pip freeze
-
-$(REQ_FILE): dev_requirements.txt requirements.txt $(EXAMPLES_REQ_FILES)
- grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@
- echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@
- echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@
- echo "mongoengine>=$(MONGOENGINE),<$(NEXT_MONGOENGINE)" >> $@
-
-
clean:
find . -type f -name '*.pyc' -delete
find . -type f -path '*/__pycache__/*' -delete
find . -type d -empty -delete
- @rm -f auto_dev_requirements_*
@rm -rf tmp_test/
-test: install-deps example-test
- python -W default setup.py test
+install-deps:
+ pip install --upgrade pip setuptools
+ pip install --upgrade -r requirements_dev.txt
+ pip freeze
+
+testall:
+ tox
+
+test:
+ python -Wdefault -m unittest $(TESTS_DIR)
example-test:
$(MAKE) -C $(EXAMPLES_DIR) test
-pylint:
+lint:
+ check-manifest
pylint --rcfile=.pylintrc --report=no $(PACKAGE)/
-coverage: install-deps
+coverage:
$(COVERAGE) erase
$(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test
$(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py"
diff --git a/README.rst b/README.rst
index 9b82406..0e1f000 100644
--- a/README.rst
+++ b/README.rst
@@ -1,20 +1,24 @@
factory_boy
===========
-.. image:: https://pypip.in/version/factory_boy/badge.svg
+.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master
+ :target: http://travis-ci.org/rbarrois/factory_boy/
+
+.. image:: https://img.shields.io/pypi/v/factory_boy.svg
:target: http://factoryboy.readthedocs.org/en/latest/changelog.html
:alt: Latest Version
-.. image:: https://pypip.in/py_versions/factory_boy/badge.svg
+.. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg
:target: https://pypi.python.org/pypi/factory_boy/
:alt: Supported Python versions
-.. image:: https://pypip.in/wheel/factory_boy/badge.svg
+.. image:: https://img.shields.io/pypi/wheel/factory_boy.svg
:target: https://pypi.python.org/pypi/factory_boy/
:alt: Wheel status
-.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master
- :target: http://travis-ci.org/rbarrois/factory_boy/
+.. image:: https://img.shields.io/pypi/l/factory_boy.svg
+ :target: https://pypi.python.org/pypi/factory_boy/
+ :alt: License
factory_boy is a fixtures replacement based on thoughtbot's `factory_girl <http://github.com/thoughtbot/factory_girl>`_.
@@ -74,8 +78,9 @@ Links
* Documentation: http://factoryboy.readthedocs.org/
* Repository: https://github.com/rbarrois/factory_boy
* Package: https://pypi.python.org/pypi/factory_boy/
+* Mailing-list: `factoryboy@googlegroups.com <mailto:factoryboy@googlegroups.com>`_ | https://groups.google.com/forum/#!forum/factoryboy
-factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library.
+factory_boy supports Python 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library.
Download
@@ -181,7 +186,7 @@ It is also possible to create a bunch of objects in a single call:
Realistic, random values
""""""""""""""""""""""""
-Tests look better with random yet realistic values.
+Demos look better with random yet realistic values; and those realistic values can also help discover bugs.
For this, factory_boy relies on the excellent `fake-factory <https://pypi.python.org/pypi/fake-factory>`_ library:
.. code-block:: python
@@ -199,6 +204,10 @@ For this, factory_boy relies on the excellent `fake-factory <https://pypi.python
<User: Lucy Murray>
+.. note:: Use of fully randomized data in tests is quickly a problem for reproducing broken builds.
+ To that purpose, factory_boy provides helpers to handle the random seeds it uses.
+
+
Lazy Attributes
"""""""""""""""
@@ -217,6 +226,7 @@ These "lazy" attributes can be added as follows:
first_name = 'Joe'
last_name = 'Blow'
email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower())
+ date_joined = factory.LazyFunction(datetime.now)
.. code-block:: pycon
@@ -224,6 +234,10 @@ These "lazy" attributes can be added as follows:
"joe.blow@example.com"
+.. note:: ``LazyAttribute`` calls the function with the object being constructed as an argument, when
+ ``LazyFunction`` does not send any argument.
+
+
Sequences
"""""""""
@@ -278,6 +292,17 @@ The associated object's strategy will be used:
True
+ORM Support
+"""""""""""
+
+factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses:
+
+* Django, with ``factory.django.DjangoModelFactory``
+* Mogo, with ``factory.mogo.MogoFactory``
+* MongoEngine, with ``factory.mongoengine.MongoEngineFactory``
+* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory``
+
+
Debugging factory_boy
"""""""""""""""""""""
@@ -312,22 +337,13 @@ This will yield messages similar to those (artificial indentation):
BaseFactory: Generating tests.test_using.TestModel2Factory(two=<tests.test_using.TestModel object at 0x1e15410>)
-ORM Support
-"""""""""""
-
-factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses:
-
-* Django, with ``factory.django.DjangoModelFactory``
-* Mogo, with ``factory.mogo.MogoFactory``
-* MongoEngine, with ``factory.mongoengine.MongoEngineFactory``
-* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory``
-
Contributing
------------
factory_boy is distributed under the MIT License.
Issues should be opened through `GitHub Issues <http://github.com/rbarrois/factory_boy/issues/>`_; whenever possible, a pull request should be included.
+Questions and suggestions are welcome on the `mailing-list <mailto:factoryboy@googlegroups.com>`_.
All pull request should pass the test suite, which can be launched simply with:
@@ -348,7 +364,7 @@ To test with a specific framework version, you may use:
.. code-block:: sh
- $ make DJANGO=1.7 test
+ $ make DJANGO=1.9 test
Valid options are:
@@ -357,6 +373,13 @@ Valid options are:
* ``ALCHEMY`` for ``SQLAlchemy``
+To avoid running ``mongoengine`` tests (e.g no mongo server installed), run:
+
+.. code-block:: sh
+
+ $ make SKIP_MONGOENGINE=1 test
+
+
Contents, indices and tables
----------------------------
diff --git a/docs/changelog.rst b/docs/changelog.rst
index fa542f4..b1fd314 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,30 @@
ChangeLog
=========
+.. _v2.7.0:
+
+2.7.0 (2016-04-03)
+------------------
+
+*New:*
+
+ - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambada parameters,
+ thanks to `Hervé Cauwelier <https://github.com/bors-ltd>`_.
+ - :issue:`251`: Add :ref:`parameterized factories <parameters>` and :class:`traits <factory.Trait>`
+
+*Removed:*
+
+ - :issue:`278`: Formally drop support for Python2.6
+
+.. _v2.6.1:
+
+2.6.1 (2016-02-10)
+------------------
+
+*New:*
+
+ - :issue:`262`: Allow optional forced flush on SQLAlchemy, courtesy of `Minjung <https://github.com/Minjung>`_.
+
.. _v2.6.0:
2.6.0 (2015-10-20)
diff --git a/docs/conf.py b/docs/conf.py
index c3512e0..d5b86f4 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -114,7 +114,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'default'
+html_theme = 'sphinx_rtd_theme'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
diff --git a/docs/examples.rst b/docs/examples.rst
index e7f6057..6f26b7e 100644
--- a/docs/examples.rst
+++ b/docs/examples.rst
@@ -49,6 +49,7 @@ And now, we'll define the related factories:
.. code-block:: python
+ import datetime
import factory
import random
@@ -61,6 +62,7 @@ And now, we'll define the related factories:
username = factory.Sequence(lambda n: 'john%s' % n)
email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username)
+ date_joined = factory.LazyFunction(datetime.datetime.now)
class ProfileFactory(factory.Factory):
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
index 6b06608..5b03ec6 100644
--- a/docs/fuzzy.rst
+++ b/docs/fuzzy.rst
@@ -3,6 +3,12 @@ Fuzzy attributes
.. module:: factory.fuzzy
+.. note:: Now that FactoryBoy includes the :class:`factory.Faker` class, most of
+ these built-in fuzzers are deprecated in favor of their
+ `Faker <http://www.joke2k.net/faker/>`_ equivalents. Further
+ discussion here:
+ `<https://github.com/rbarrois/factory_boy/issues/271/>`_
+
Some tests may be interested in testing with fuzzy, random values.
This is handled by the :mod:`factory.fuzzy` module, which provides a few
@@ -199,7 +205,7 @@ FuzzyDate
FuzzyDateTime
-------------
-.. class:: FuzzyDateTime(start_dt[, end_dt], tz=UTC, force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None)
+.. class:: FuzzyDateTime(start_dt[, end_dt], force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None)
The :class:`FuzzyDateTime` fuzzer generates random timezone-aware datetime within a given
inclusive range.
diff --git a/docs/introduction.rst b/docs/introduction.rst
index d00154d..5b535c9 100644
--- a/docs/introduction.rst
+++ b/docs/introduction.rst
@@ -117,6 +117,35 @@ This is achieved with the :class:`~factory.Sequence` declaration:
return 'user%d' % n
+LazyFunction
+------------
+
+In simple cases, calling a function is enough to compute the value. If that function doesn't depend on the object
+being built, use :class:`~factory.LazyFunction` to call that function; it should receive a function taking no
+argument and returning the value for the field:
+
+.. code-block:: python
+
+ class LogFactory(factory.Factory):
+ class Meta:
+ model = models.Log
+
+ timestamp = factory.LazyFunction(datetime.now)
+
+.. code-block:: pycon
+
+ >>> LogFactory()
+ <Log: log at 2016-02-12 17:02:34>
+
+ >>> # The LazyFunction can be overriden
+ >>> LogFactory(timestamp=now - timedelta(days=1))
+ <Log: log at 2016-02-11 17:02:34>
+
+
+.. note:: For complex cases when you happen to write a specific function,
+ the :meth:`~factory.@lazy_attribute` decorator should be more appropriate.
+
+
LazyAttribute
-------------
@@ -237,6 +266,61 @@ This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute:
<MyClass(1, 4, z=3)>
+Altering a factory's behaviour: parameters and traits
+-----------------------------------------------------
+
+Some classes are better described with a few, simple parameters, that aren't fields on the actual model.
+In that case, use a :attr:`~factory.Factory.Params` declaration:
+
+.. code-block:: python
+
+ class RentalFactory(factory.Factory):
+ class Meta:
+ model = Rental
+
+ begin = factory.fuzzy.FuzzyDate(start_date=datetime.date(2000, 1, 1))
+ end = factory.LazyAttribute(lambda o: o.begin + o.duration)
+
+ class Params:
+ duration = 12
+
+.. code-block:: pycon
+
+ >>> RentalFactory(duration=0)
+ <Rental: 2012-03-03 -> 2012-03-03>
+ >>> RentalFactory(duration=10)
+ <Rental: 2008-12-16 -> 2012-12-26>
+
+
+When many fields should be updated based on a flag, use :class:`Traits <factory.Trait>` instead:
+
+.. code-block:: python
+
+ class OrderFactory(factory.Factory):
+ status = 'pending'
+ shipped_by = None
+ shipped_on = None
+
+ class Meta:
+ model = Order
+
+ class Params:
+ shipped = factory.Trait(
+ status='shipped',
+ shipped_by=factory.SubFactory(EmployeeFactory),
+ shipped_on=factory.LazyFunction(datetime.date.today),
+ )
+
+A trait is toggled by a single boolean value:
+
+.. code-block:: pycon
+
+ >>> OrderFactory()
+ <Order: pending>
+ >>> OrderFactory(shipped=True)
+ <Order: shipped by John Doe on 2016-04-02>
+
+
Strategies
----------
diff --git a/docs/orms.rst b/docs/orms.rst
index 9b209bc..fb3543d 100644
--- a/docs/orms.rst
+++ b/docs/orms.rst
@@ -15,7 +15,7 @@ Django
The first versions of factory_boy were designed specifically for Django,
-but the library has now evolved to be framework-independant.
+but the library has now evolved to be framework-independent.
Most features should thus feel quite familiar to Django users.
@@ -96,22 +96,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the
[<User: john>, <User: jack>]
-.. note:: If a :class:`DjangoModelFactory` relates to an :obj:`~django.db.models.Options.abstract`
- model, be sure to declare the :class:`DjangoModelFactory` as abstract:
-
- .. code-block:: python
-
- class MyAbstractModelFactory(factory.django.DjangoModelFactory):
- class Meta:
- model = models.MyAbstractModel
- abstract = True
-
- class MyConcreteModelFactory(MyAbstractModelFactory):
- class Meta:
- model = models.MyConcreteModel
-
- Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model.
-
Extra fields
""""""""""""
@@ -126,7 +110,7 @@ Extra fields
:param str from_path: Use data from the file located at ``from_path``,
and keep its filename
:param file from_file: Use the contents of the provided file object; use its filename
- if available
+ if available, unless ``filename`` is also provided.
:param bytes data: Use the provided bytes as file contents
:param str filename: The filename for the FileField
@@ -333,6 +317,10 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:
SQLAlchemy session to use to communicate with the database when creating
an object through this :class:`SQLAlchemyModelFactory`.
+ .. attribute:: force_flush
+
+ Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`.
+
A (very) simple example:
.. code-block:: python
diff --git a/docs/recipes.rst b/docs/recipes.rst
index df86bac..fe18f50 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -88,7 +88,7 @@ When a :class:`UserFactory` is instantiated, factory_boy will call
Example: Django's Profile
-"""""""""""""""""""""""""
+~~~~~~~~~~~~~~~~~~~~~~~~~
Django (<1.5) provided a mechanism to attach a ``Profile`` to a ``User`` instance,
using a :class:`~django.db.models.OneToOneField` from the ``Profile`` to the ``User``.
@@ -327,7 +327,21 @@ Here, we want:
country = factory.SubFactory(CountryFactory)
owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country'))
+If the value of a field on the child factory is indirectly derived from a field on the parent factory, you will need to use LazyAttribute and poke the "factory_parent" attribute.
+This time, we want the company owner to live in a country neighboring the country of the company:
+
+.. code-block:: python
+
+ class CompanyFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.Company
+
+ name = "ACME, Inc."
+ country = factory.SubFactory(CountryFactory)
+ owner = factory.SubFactory(UserFactory,
+ country=factory.LazyAttribute(lambda o: get_random_neighbor(o.factory_parent.country)))
+
Custom manager methods
----------------------
@@ -444,3 +458,34 @@ Forcing the initial value for all projects
>>> Account.objects.create(uid=42, name="Blah")
>>> AccountFactory.create() # Sets up the account number based on the latest uid
<Account uid=43, name=Test>
+
+
+Converting a factory's output to a dict
+---------------------------------------
+
+In order to inject some data to, say, a REST API, it can be useful to fetch the factory's data
+as a dict.
+
+Internally, a factory will:
+
+1. Merge declarations and overrides from all sources (class definition, call parameters, ...)
+2. Resolve them into a dict
+3. Pass that dict as keyword arguments to the model's ``build`` / ``create`` function
+
+
+In order to get a dict, we'll just have to swap the model; the easiest way is to use
+:meth:`factory.build`:
+
+.. code-block:: python
+
+ class UserFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.User
+
+ first_name = factory.Sequence(lambda n: "Agent %03d" % n)
+ username = factory.Faker('username')
+
+.. code-block:: pycon
+
+ >>> factory.build(dict, FACTORY_CLASS=UserFactory)
+ {'first_name': "Agent 001", 'username': 'john_doe'}
diff --git a/docs/reference.rst b/docs/reference.rst
index 6398d9a..ad68faf 100644
--- a/docs/reference.rst
+++ b/docs/reference.rst
@@ -11,6 +11,9 @@ For internals and customization points, please refer to the :doc:`internals` sec
The :class:`Factory` class
--------------------------
+Meta options
+""""""""""""
+
.. class:: FactoryOptions
.. versionadded:: 2.4.0
@@ -90,7 +93,7 @@ The :class:`Factory` class
model = Order
exclude = ('now',)
- now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow())
+ now = factory.LazyFunction(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))
@@ -135,11 +138,16 @@ The :class:`Factory` class
+Attributes and methods
+""""""""""""""""""""""
+
+
.. class:: Factory
**Class-level attributes:**
+ .. attribute:: Meta
.. attribute:: _meta
.. versionadded:: 2.4.0
@@ -147,6 +155,14 @@ The :class:`Factory` class
The :class:`FactoryOptions` instance attached to a :class:`Factory` class is available
as a :attr:`_meta` attribute.
+ .. attribute:: Params
+
+ .. versionadded:: 2.7.0
+
+ The extra parameters attached to a :class:`Factory` are declared through a :attr:`Params`
+ class.
+ See :ref:`the "Parameters" section <parameters>` for more information.
+
.. attribute:: _options_class
.. versionadded:: 2.4.0
@@ -353,6 +369,175 @@ The :class:`Factory` class
factory in the chain.
+.. _parameters:
+
+Parameters
+""""""""""
+
+.. versionadded:: 2.7.0
+
+Some models have many fields that can be summarized by a few parameters; for instance,
+a train with many cars — each complete with serial number, manufacturer, ...;
+or an order that can be pending/shipped/received, with a few fields to describe each step.
+
+When building instances of such models, a couple of parameters can be enough to determine
+all other fields; this is handled by the :class:`~Factory.Params` section of a :class:`Factory` declaration.
+
+
+Simple parameters
+~~~~~~~~~~~~~~~~~
+
+Some factories only need little data:
+
+.. code-block:: python
+
+ class ConferenceFactory(factory.Factory):
+ class Meta:
+ model = Conference
+
+ class Params:
+ duration = 'short' # Or 'long'
+
+ start_date = factory.fuzzy.FuzzyDate()
+ end_date = factory.LazyAttribute(
+ lambda o: o.start_date + datetime.timedelta(days=2 if o.duration == 'short' else 7)
+ )
+ sprints_start = factory.LazyAttribute(
+ lambda o: o.end_date - datetime.timedelta(days=0 if o.duration == 'short' else 1)
+ )
+
+.. code-block:: pycon
+
+ >>> Conference(duration='short')
+ <Conference: DUTH 2015 (2015-11-05 - 2015-11-08, sprints 2015-11-08)>
+ >>> Conference(duration='long')
+ <Conference: DjangoConEU 2016 (2016-03-30 - 2016-04-03, sprints 2016-04-02)>
+
+
+Any simple parameter provided to the :class:`Factory.Params` section is available to the whole factory,
+but not passed to the final class (similar to the :attr:`~FactoryOptions.exclude` behavior).
+
+
+Traits
+~~~~~~
+
+.. class:: Trait(**kwargs)
+
+ .. OHAI VIM**
+
+ .. versionadded:: 2.7.0
+
+ A trait's parameters are the fields it sohuld alter when enabled.
+
+
+For more complex situations, it is helpful to override a few fields at once:
+
+.. code-block:: python
+
+ class OrderFactory(factory.Factory):
+ class Meta:
+ model = Order
+
+ state = 'pending'
+ shipped_on = None
+ shipped_by = None
+
+ class Params:
+ shipped = factory.Trait(
+ state='shipped',
+ shipped_on=datetime.date.today,
+ shipped_by=factory.SubFactory(EmployeeFactory),
+ )
+
+Such a :class:`Trait` is activated or disabled by a single boolean field:
+
+
+.. code-block:: pycon
+
+ >>> OrderFactory()
+ <Order: pending>
+ Order(state='pending')
+ >>> OrderFactory(shipped=True)
+ <Order: shipped by John Doe on 2016-04-02>
+
+
+A :class:`Trait` can be enabled/disabled by a :class:`Factory` subclass:
+
+.. code-block:: python
+
+ class ShippedOrderFactory(OrderFactory):
+ shipped = True
+
+
+Values set in a :class:`Trait` can be overridden by call-time values:
+
+.. code-block:: pycon
+
+ >>> OrderFactory(shipped=True, shipped_on=last_year)
+ <Order: shipped by John Doe on 2015-04-20>
+
+
+:class:`Traits <Trait>` can be chained:
+
+.. code-block:: python
+
+ class OrderFactory(factory.Factory):
+ class Meta:
+ model = Order
+
+ # Can be pending/shipping/received
+ state = 'pending'
+ shipped_on = None
+ shipped_by = None
+ received_on = None
+ received_by = None
+
+ class Params:
+ shipped = factory.Trait(
+ state='shipped',
+ shipped_on=datetime.date.today,
+ shipped_by=factory.SubFactory(EmployeeFactory),
+ )
+ received = factory.Trait(
+ shipped=True,
+ state='received',
+ shipped_on=datetime.date.today - datetime.timedelta(days=4),
+ received_on=datetime.date.today,
+ received_by=factory.SubFactory(CustomerFactory),
+ )
+
+.. code-block:: pycon
+
+ >>> OrderFactory(received=True)
+ <Order: shipped by John Doe on 2016-03-20, received by Joan Smith on 2016-04-02>
+
+
+
+A :class:`Trait` might be overridden in :class:`Factory` subclasses:
+
+.. code-block:: python
+
+ class LocalOrderFactory(OrderFactory):
+
+ class Params:
+ received = factory.Trait(
+ shipped=True,
+ state='received',
+ shipped_on=datetime.date.today - datetime.timedelta(days=1),
+ received_on=datetime.date.today,
+ received_by=factory.SubFactory(CustomerFactory),
+ )
+
+
+.. code-block:: pycon
+
+ >>> LocalOrderFactory(received=True)
+ <Order: shipped by John Doe on 2016-04-01, received by Joan Smith on 2016-04-02>
+
+
+.. note:: When overriding a :class:`Trait`, the whole declaration **MUST** be replaced.
+
+
.. _strategies:
Strategies
@@ -551,6 +736,42 @@ Faker
smiley = factory.Faker('smiley')
+LazyFunction
+""""""""""""
+
+.. class:: LazyFunction(method_to_call)
+
+The :class:`LazyFunction` is the simplest case where the value of an attribute
+does not depend on the object being built.
+
+It takes as argument a method to call (function, lambda...); that method should
+not take any argument, though keyword arguments are safe but unused,
+and return a value.
+
+.. code-block:: python
+
+ class LogFactory(factory.Factory):
+ class Meta:
+ model = models.Log
+
+ timestamp = factory.LazyFunction(datetime.now)
+
+.. code-block:: pycon
+
+ >>> LogFactory()
+ <Log: log at 2016-02-12 17:02:34>
+
+ >>> # The LazyFunction can be overriden
+ >>> LogFactory(timestamp=now - timedelta(days=1))
+ <Log: log at 2016-02-11 17:02:34>
+
+Decorator
+~~~~~~~~~
+
+The class :class:`LazyFunction` does not provide a decorator.
+
+For complex cases, use :meth:`LazyAttribute.lazy_attribute` directly.
+
LazyAttribute
"""""""""""""
@@ -711,8 +932,9 @@ The sequence counter is shared across all :class:`Sequence` attributes of the
Inheritance
~~~~~~~~~~~
-When a :class:`Factory` inherits from another :class:`Factory`, their
-sequence counter is shared:
+When a :class:`Factory` inherits from another :class:`Factory` and the `model`
+of the subclass inherits from the `model` of the parent, the sequence counter
+is shared across the :class:`Factory` classes:
.. code-block:: python
@@ -1041,7 +1263,7 @@ gains an "upward" semantic through the double-dot notation, as used in Python im
>>> company.owner.language
'fr'
-Obviously, this "follow parents" hability also handles overriding some attributes on call:
+Obviously, this "follow parents" ability also handles overriding some attributes on call:
.. code-block:: pycon
@@ -1262,6 +1484,7 @@ with the :class:`Dict` and :class:`List` attributes:
argument, if another type (tuple, set, ...) is required.
+
Post-generation hooks
"""""""""""""""""""""
@@ -1412,6 +1635,23 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables
1
+.. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated.
+ This means that calls to :class:`factory.SelfAttribute` cannot go higher than this :class:`RelatedFactory`:
+
+ .. code-block:: python
+
+ class CountryFactory(factory.Factory):
+ class Meta:
+ model = Country
+
+ lang = 'fr'
+ capital_city = factory.RelatedFactory(CityFactory, 'capital_of',
+ # factory.SelfAttribute('..lang') will crash, since the context of
+ # ``CountryFactory`` has already been evaluated.
+ main_lang=factory.SelfAttribute('capital_of.lang'),
+ )
+
+
PostGeneration
""""""""""""""
diff --git a/factory/__init__.py b/factory/__init__.py
index 4a4a09f..ad9da80 100644
--- a/factory/__init__.py
+++ b/factory/__init__.py
@@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
-__version__ = '2.6.0'
+__version__ = '2.6.1'
__author__ = 'Raphaël Barrois <raphael.barrois+fboy@polytechnique.org>'
@@ -32,22 +32,27 @@ from .base import (
ListFactory,
StubFactory,
- FactoryError,
-
BUILD_STRATEGY,
CREATE_STRATEGY,
STUB_STRATEGY,
use_strategy,
)
+
+from .errors import (
+ FactoryError,
+)
+
from .faker import Faker
from .declarations import (
+ LazyFunction,
LazyAttribute,
Iterator,
Sequence,
LazyAttributeSequence,
SelfAttribute,
+ Trait,
ContainerAttribute,
SubFactory,
Dict,
diff --git a/factory/alchemy.py b/factory/alchemy.py
index 20da6cf..a9aab23 100644
--- a/factory/alchemy.py
+++ b/factory/alchemy.py
@@ -27,6 +27,7 @@ class SQLAlchemyOptions(base.FactoryOptions):
def _build_default_options(self):
return super(SQLAlchemyOptions, self)._build_default_options() + [
base.OptionDefault('sqlalchemy_session', None, inherit=True),
+ base.OptionDefault('force_flush', False, inherit=True),
]
@@ -43,4 +44,6 @@ class SQLAlchemyModelFactory(base.Factory):
session = cls._meta.sqlalchemy_session
obj = model_class(*args, **kwargs)
session.add(obj)
+ if cls._meta.force_flush:
+ session.flush()
return obj
diff --git a/factory/base.py b/factory/base.py
index 0f2af59..282e3b1 100644
--- a/factory/base.py
+++ b/factory/base.py
@@ -20,10 +20,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
+import collections
import logging
from . import containers
from . import declarations
+from . import errors
from . import utils
logger = logging.getLogger('factory.generate')
@@ -35,22 +37,6 @@ STUB_STRATEGY = 'stub'
-class FactoryError(Exception):
- """Any exception raised by factory_boy."""
-
-
-class AssociatedClassError(FactoryError):
- """Exception for Factory subclasses lacking Meta.model."""
-
-
-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):
@@ -82,7 +68,7 @@ class FactoryMetaClass(type):
elif cls._meta.strategy == STUB_STRATEGY:
return cls.stub(**kwargs)
else:
- raise UnknownStrategy('Unknown Meta.strategy: {0}'.format(
+ raise errors.UnknownStrategy('Unknown Meta.strategy: {0}'.format(
cls._meta.strategy))
def __new__(mcs, class_name, bases, attrs):
@@ -107,6 +93,7 @@ class FactoryMetaClass(type):
base_factory = None
attrs_meta = attrs.pop('Meta', None)
+ attrs_params = attrs.pop('Params', None)
base_meta = resolve_attribute('_meta', bases)
options_class = resolve_attribute('_options_class', bases, FactoryOptions)
@@ -121,6 +108,7 @@ class FactoryMetaClass(type):
meta=attrs_meta,
base_meta=base_meta,
base_factory=base_factory,
+ params=attrs_params,
)
return new_class
@@ -163,6 +151,8 @@ class FactoryOptions(object):
self.base_factory = None
self.declarations = {}
self.postgen_declarations = {}
+ self.parameters = {}
+ self.parameters_dependencies = {}
def _build_default_options(self):
""""Provide the default value for all allowed fields.
@@ -201,7 +191,7 @@ class FactoryOptions(object):
% (self.factory, ','.join(sorted(meta_attrs.keys()))))
def contribute_to_class(self, factory,
- meta=None, base_meta=None, base_factory=None):
+ meta=None, base_meta=None, base_factory=None, params=None):
self.factory = factory
self.base_factory = base_factory
@@ -219,6 +209,7 @@ class FactoryOptions(object):
continue
self.declarations.update(parent._meta.declarations)
self.postgen_declarations.update(parent._meta.postgen_declarations)
+ self.parameters.update(parent._meta.parameters)
for k, v in vars(self.factory).items():
if self._is_declaration(k, v):
@@ -226,6 +217,13 @@ class FactoryOptions(object):
if self._is_postgen_declaration(k, v):
self.postgen_declarations[k] = v
+ if params is not None:
+ for k, v in vars(params).items():
+ if not k.startswith('_'):
+ self.parameters[k] = v
+
+ self.parameters_dependencies = self._compute_parameter_dependencies(self.parameters)
+
def _get_counter_reference(self):
"""Identify which factory should be used for a shared counter."""
@@ -257,6 +255,32 @@ class FactoryOptions(object):
"""Captures instances of PostGenerationDeclaration."""
return isinstance(value, declarations.PostGenerationDeclaration)
+ def _compute_parameter_dependencies(self, parameters):
+ """Find out in what order parameters should be called."""
+ # Warning: parameters only provide reverse dependencies; we reverse them into standard dependencies.
+ # deep_revdeps: set of fields a field depend indirectly upon
+ deep_revdeps = collections.defaultdict(set)
+ # Actual, direct dependencies
+ deps = collections.defaultdict(set)
+
+ for name, parameter in parameters.items():
+ if isinstance(parameter, declarations.ComplexParameter):
+ field_revdeps = parameter.get_revdeps(parameters)
+ if not field_revdeps:
+ continue
+ deep_revdeps[name] = set.union(*(deep_revdeps[dep] for dep in field_revdeps))
+ deep_revdeps[name] |= set(field_revdeps)
+ for dep in field_revdeps:
+ deps[dep].add(name)
+
+ # Check for cyclical dependencies
+ cyclic = [name for name, field_deps in deep_revdeps.items() if name in field_deps]
+ if cyclic:
+ raise errors.CyclicDefinitionError(
+ "Cyclic definition detected on %s' Params around %s"
+ % (self.factory, ', '.join(cyclic)))
+ return deps
+
def __str__(self):
return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__)
@@ -296,12 +320,12 @@ class BaseFactory(object):
"""Factory base support for sequences, attributes and stubs."""
# Backwards compatibility
- UnknownStrategy = UnknownStrategy
- UnsupportedStrategy = UnsupportedStrategy
+ UnknownStrategy = errors.UnknownStrategy
+ UnsupportedStrategy = errors.UnsupportedStrategy
def __new__(cls, *args, **kwargs):
"""Would be called if trying to instantiate the class."""
- raise FactoryError('You cannot instantiate BaseFactory')
+ raise errors.FactoryError('You cannot instantiate BaseFactory')
_meta = FactoryOptions()
@@ -454,6 +478,9 @@ class BaseFactory(object):
# Remove 'hidden' arguments.
for arg in cls._meta.exclude:
del kwargs[arg]
+ # Remove parameters, if defined
+ for arg in cls._meta.parameters:
+ kwargs.pop(arg, None)
# Extract *args from **kwargs
args = tuple(kwargs.pop(key) for key in cls._meta.inline_args)
@@ -477,7 +504,7 @@ class BaseFactory(object):
attrs (dict): attributes to use for generating the object
"""
if cls._meta.abstract:
- raise FactoryError(
+ raise errors.FactoryError(
"Cannot generate instances of abstract factory %(f)s; "
"Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract "
"is either not set or False." % dict(f=cls.__name__))
@@ -680,7 +707,7 @@ Factory = FactoryMetaClass('Factory', (BaseFactory,), {
# Backwards compatibility
-Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201
+Factory.AssociatedClassError = errors.AssociatedClassError # pylint: disable=W0201
class StubFactory(Factory):
@@ -695,7 +722,7 @@ class StubFactory(Factory):
@classmethod
def create(cls, **kwargs):
- raise UnsupportedStrategy()
+ raise errors.UnsupportedStrategy()
class BaseDictFactory(Factory):
diff --git a/factory/compat.py b/factory/compat.py
index 785d174..737d91a 100644
--- a/factory/compat.py
+++ b/factory/compat.py
@@ -42,14 +42,6 @@ else: # pragma: no cover
from io import BytesIO
-if sys.version_info[:2] == (2, 6): # pragma: no cover
- def float_to_decimal(fl):
- return decimal.Decimal(str(fl))
-else: # pragma: no cover
- def float_to_decimal(fl):
- return decimal.Decimal(fl)
-
-
try: # pragma: no cover
# Python >= 3.2
UTC = datetime.timezone.utc
diff --git a/factory/containers.py b/factory/containers.py
index ec33ca1..4961115 100644
--- a/factory/containers.py
+++ b/factory/containers.py
@@ -25,13 +25,10 @@ import logging
logger = logging.getLogger(__name__)
from . import declarations
+from . import errors
from . import utils
-class CyclicDefinitionError(Exception):
- """Raised when cyclic definition were found."""
-
-
class LazyStub(object):
"""A generic container that only allows getting attributes.
@@ -93,7 +90,7 @@ class LazyStub(object):
attributes being computed.
"""
if name in self.__pending:
- raise CyclicDefinitionError(
+ raise errors.CyclicDefinitionError(
"Cyclic lazy attribute definition for %s; cycle found in %r." %
(name, self.__pending))
elif name in self.__values:
@@ -114,7 +111,6 @@ class LazyStub(object):
"The parameter %s is unknown. Evaluated attributes are %r, "
"definitions are %r." % (name, self.__values, self.__attrs))
-
def __setattr__(self, name, value):
"""Prevent setting attributes once __init__ is done."""
if not self.__initialized:
@@ -123,6 +119,69 @@ class LazyStub(object):
raise AttributeError('Setting of object attributes is not allowed')
+class DeclarationStack(object):
+ """An ordered stack of declarations.
+
+ This is intended to handle declaration precedence among different mutating layers.
+ """
+ def __init__(self, ordering):
+ self.ordering = ordering
+ self.layers = dict((name, {}) for name in self.ordering)
+
+ def __getitem__(self, key):
+ return self.layers[key]
+
+ def __setitem__(self, key, value):
+ assert key in self.ordering
+ self.layers[key] = value
+
+ def current(self):
+ """Retrieve the current, flattened declarations dict."""
+ result = {}
+ for layer in self.ordering:
+ result.update(self.layers[layer])
+ return result
+
+
+class ParameterResolver(object):
+ """Resolve a factory's parameter declarations."""
+ def __init__(self, parameters, deps):
+ self.parameters = parameters
+ self.deps = deps
+ self.declaration_stack = None
+
+ self.resolved = set()
+
+ def resolve_one(self, name):
+ """Compute one field is needed, taking dependencies into accounts."""
+ if name in self.resolved:
+ return
+
+ for dep in self.deps.get(name, ()):
+ self.resolve_one(dep)
+
+ self.compute(name)
+ self.resolved.add(name)
+
+ def compute(self, name):
+ """Actually compute the value for a given name."""
+ value = self.parameters[name]
+ if isinstance(value, declarations.ComplexParameter):
+ overrides = value.compute(name, self.declaration_stack.current())
+ else:
+ overrides = {name: value}
+ self.declaration_stack['overrides'].update(overrides)
+
+ def resolve(self, declaration_stack):
+ """Resolve parameters for a given declaration stack.
+
+ Modifies the stack in-place.
+ """
+ self.declaration_stack = declaration_stack
+ for name in self.parameters:
+ self.resolve_one(name)
+
+
class LazyValue(object):
"""Some kind of "lazy evaluating" object."""
@@ -131,7 +190,7 @@ class LazyValue(object):
raise NotImplementedError("This is an abstract method.")
-class OrderedDeclarationWrapper(LazyValue):
+class DeclarationWrapper(LazyValue):
"""Lazy wrapper around an OrderedDeclaration.
Attributes:
@@ -142,7 +201,7 @@ class OrderedDeclarationWrapper(LazyValue):
"""
def __init__(self, declaration, sequence, create, extra=None, **kwargs):
- super(OrderedDeclarationWrapper, self).__init__(**kwargs)
+ super(DeclarationWrapper, self).__init__(**kwargs)
self.declaration = declaration
self.sequence = sequence
self.create = create
@@ -172,7 +231,7 @@ class AttributeBuilder(object):
Attributes:
factory (base.Factory): the Factory for which attributes are being
built
- _attrs (DeclarationDict): the attribute declarations for the factory
+ _declarations (DeclarationDict): the attribute declarations for the factory
_subfields (dict): dict mapping an attribute name to a dict of
overridden default values for the related SubFactory.
"""
@@ -185,20 +244,47 @@ class AttributeBuilder(object):
self.factory = factory
self._containers = extra.pop('__containers', ())
- self._attrs = factory.declarations(extra)
+
+ initial_declarations = dict(factory._meta.declarations)
self._log_ctx = log_ctx
- initial_declarations = factory.declarations({})
+ # Parameters
+ # ----------
+ self._declarations = self.merge_declarations(initial_declarations, extra)
+
+ # Subfields
+ # ---------
+
attrs_with_subfields = [
k for k, v in initial_declarations.items()
- if self.has_subfields(v)]
+ if self.has_subfields(v)
+ ]
+ # Extract subfields; THIS MODIFIES self._declarations.
self._subfields = utils.multi_extract_dict(
- attrs_with_subfields, self._attrs)
+ attrs_with_subfields, self._declarations)
def has_subfields(self, value):
return isinstance(value, declarations.ParameteredAttribute)
+ def merge_declarations(self, initial, extra):
+ """Compute the final declarations, taking into account paramter-based overrides."""
+ # Precedence order:
+ # - Start with class-level declarations
+ # - Add overrides from parameters
+ # - Finally, use callsite-level declarations & values
+ declaration_stack = DeclarationStack(['initial', 'overrides', 'extra'])
+ declaration_stack['initial'] = initial.copy()
+ declaration_stack['extra'] = extra.copy()
+
+ # Actually compute the final stack
+ resolver = ParameterResolver(
+ parameters=self.factory._meta.parameters,
+ deps=self.factory._meta.parameters_dependencies,
+ )
+ resolver.resolve(declaration_stack)
+ return declaration_stack.current()
+
def build(self, create, force_sequence=None):
"""Build a dictionary of attributes.
@@ -216,9 +302,9 @@ class AttributeBuilder(object):
# Parse attribute declarations, wrapping SubFactory and
# OrderedDeclaration.
wrapped_attrs = {}
- for k, v in self._attrs.items():
+ for k, v in self._declarations.items():
if isinstance(v, declarations.OrderedDeclaration):
- v = OrderedDeclarationWrapper(v,
+ v = DeclarationWrapper(v,
sequence=sequence,
create=create,
extra=self._subfields.get(k, {}),
diff --git a/factory/declarations.py b/factory/declarations.py
index f0dbfe5..895f2ac 100644
--- a/factory/declarations.py
+++ b/factory/declarations.py
@@ -57,6 +57,23 @@ class OrderedDeclaration(object):
raise NotImplementedError('This is an abstract method')
+class LazyFunction(OrderedDeclaration):
+ """Simplest OrderedDeclaration computed by calling the given function.
+
+ Attributes:
+ function (function): a function without arguments and
+ returning the computed value.
+ """
+
+ def __init__(self, function, *args, **kwargs):
+ super(LazyFunction, self).__init__(*args, **kwargs)
+ self.function = function
+
+ def evaluate(self, sequence, obj, create, extra=None, containers=()):
+ logger.debug("LazyFunction: Evaluating %r on %r", self.function, obj)
+ return self.function()
+
+
class LazyAttribute(OrderedDeclaration):
"""Specific OrderedDeclaration computed using a lambda.
@@ -423,6 +440,55 @@ class List(SubFactory):
**params)
+# Parameters
+# ==========
+
+
+class ComplexParameter(object):
+ """A complex parameter, to be used in a Factory.Params section.
+
+ Must implement:
+ - A "compute" function, performing the actual declaration override
+ - Optionally, a get_revdeps() function (to compute other parameters it may alter)
+ """
+
+ def compute(self, field_name, declarations):
+ """Compute the overrides for this parameter.
+
+ Args:
+ - field_name (str): the field this parameter is installed at
+ - declarations (dict): the global factory declarations
+
+ Returns:
+ dict: the declarations to override
+ """
+ raise NotImplementedError()
+
+ def get_revdeps(self, parameters):
+ """Retrieve the list of other parameters modified by this one."""
+ return []
+
+
+class Trait(ComplexParameter):
+ """The simplest complex parameter, it enables a bunch of new declarations based on a boolean flag."""
+ def __init__(self, **overrides):
+ self.overrides = overrides
+
+ def compute(self, field_name, declarations):
+ if declarations.get(field_name):
+ return self.overrides
+ else:
+ return {}
+
+ def get_revdeps(self, parameters):
+ """This might alter fields it's injecting."""
+ return [param for param in parameters if param in self.overrides]
+
+
+# Post-generation
+# ===============
+
+
class ExtractionContext(object):
"""Private class holding all required context from extraction to postgen."""
def __init__(self, value=None, did_extract=False, extra=None, for_field=''):
diff --git a/factory/errors.py b/factory/errors.py
new file mode 100644
index 0000000..79d85f4
--- /dev/null
+++ b/factory/errors.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2011-2015 Raphaël Barrois
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+
+class FactoryError(Exception):
+ """Any exception raised by factory_boy."""
+
+
+class AssociatedClassError(FactoryError):
+ """Exception for Factory subclasses lacking Meta.model."""
+
+
+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."""
+
+
+class CyclicDefinitionError(FactoryError):
+ """Raised when a cyclical declaration occurs."""
+
+
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index 923d8b7..71d1884 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -164,7 +164,7 @@ class FuzzyDecimal(BaseFuzzyAttribute):
super(FuzzyDecimal, self).__init__(**kwargs)
def fuzz(self):
- base = compat.float_to_decimal(_random.uniform(self.low, self.high))
+ base = decimal.Decimal(str(_random.uniform(self.low, self.high)))
return base.quantize(decimal.Decimal(10) ** -self.precision)
@@ -217,6 +217,9 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute):
"""%s boundaries should have start <= end, got %r > %r""" % (
self.__class__.__name__, start_dt, end_dt))
+ def _now(self):
+ raise NotImplementedError()
+
def __init__(self, start_dt, end_dt=None,
force_year=None, force_month=None, force_day=None,
force_hour=None, force_minute=None, force_second=None,
diff --git a/factory/utils.py b/factory/utils.py
index 806b1ec..cfae4ec 100644
--- a/factory/utils.py
+++ b/factory/utils.py
@@ -35,7 +35,7 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()):
Args:
prefix (str): the prefix to use for lookups
- kwargs (dict): the dict from which values should be extracted
+ kwargs (dict): the dict from which values should be extracted; WILL BE MODIFIED.
pop (bool): whether to use pop (True) or get (False)
exclude (iterable): list of prefixed keys that shouldn't be extracted
@@ -68,7 +68,7 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()):
Args:
prefixes (str list): the prefixes to use for lookups
- kwargs (dict): the dict from which values should be extracted
+ kwargs (dict): the dict from which values should be extracted; WILL BE MODIFIED.
pop (bool): whether to use pop (True) or get (False)
exclude (iterable): list of prefixed keys that shouldn't be extracted
@@ -101,7 +101,7 @@ def import_object(module_name, attribute_name):
def _safe_repr(obj):
try:
obj_repr = repr(obj)
- except UnicodeError:
+ except Exception:
return '<bad_repr object at %s>' % id(obj)
try: # Convert to "text type" (= unicode)
diff --git a/dev_requirements.txt b/requirements_dev.txt
index 22261a1..a7706c6 100644
--- a/dev_requirements.txt
+++ b/requirements_dev.txt
@@ -1,4 +1,5 @@
--r requirements.txt
+-e .
+-r requirements_test.txt
-r examples/requirements.txt
coverage
@@ -6,5 +7,8 @@ Django
Pillow
SQLAlchemy
mongoengine
-mock
wheel
+tox
+
+Sphinx
+sphinx_rtd_theme
diff --git a/requirements_test.txt b/requirements_test.txt
new file mode 100644
index 0000000..e37a8fe
--- /dev/null
+++ b/requirements_test.txt
@@ -0,0 +1,2 @@
+mock
+pylint
diff --git a/setup.py b/setup.py
index 8ca7e4b..3a0e699 100755
--- a/setup.py
+++ b/setup.py
@@ -61,12 +61,12 @@ setup(
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.2",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Testing",
"Topic :: Software Development :: Libraries :: Python Modules",
diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py
index 9d7288a..5d8f275 100644
--- a/tests/test_alchemy.py
+++ b/tests/test_alchemy.py
@@ -23,6 +23,7 @@
import factory
from .compat import unittest
+import mock
try:
@@ -55,6 +56,16 @@ class StandardFactory(SQLAlchemyModelFactory):
foo = factory.Sequence(lambda n: 'foo%d' % n)
+class ForceFlushingStandardFactory(SQLAlchemyModelFactory):
+ class Meta:
+ model = models.StandardModel
+ sqlalchemy_session = mock.MagicMock()
+ force_flush = True
+
+ id = factory.Sequence(lambda n: n)
+ foo = factory.Sequence(lambda n: 'foo%d' % n)
+
+
class NonIntegerPkFactory(SQLAlchemyModelFactory):
class Meta:
model = models.NonIntegerPk
@@ -103,6 +114,27 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase):
@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.")
+class SQLAlchemyForceFlushTestCase(unittest.TestCase):
+ def setUp(self):
+ super(SQLAlchemyForceFlushTestCase, self).setUp()
+ ForceFlushingStandardFactory.reset_sequence(1)
+ ForceFlushingStandardFactory._meta.sqlalchemy_session.rollback()
+ ForceFlushingStandardFactory._meta.sqlalchemy_session.reset_mock()
+
+ def test_force_flush_called(self):
+ self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+ ForceFlushingStandardFactory.create()
+ self.assertTrue(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+
+ def test_force_flush_not_called(self):
+ ForceFlushingStandardFactory._meta.force_flush = False
+ self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+ ForceFlushingStandardFactory.create()
+ self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called)
+ ForceFlushingStandardFactory._meta.force_flush = True
+
+
+@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.")
class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase):
def setUp(self):
super(SQLAlchemyNonIntegerPkTestCase, self).setUp()
diff --git a/tests/test_base.py b/tests/test_base.py
index 24f64e5..a3b3704 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -24,6 +24,7 @@ import warnings
from factory import base
from factory import declarations
+from factory import errors
from .compat import unittest
@@ -63,7 +64,7 @@ class TestModel(FakeDjangoModel):
class SafetyTestCase(unittest.TestCase):
def test_base_factory(self):
- self.assertRaises(base.FactoryError, base.BaseFactory)
+ self.assertRaises(errors.FactoryError, base.BaseFactory)
class AbstractFactoryTestCase(unittest.TestCase):
@@ -88,8 +89,8 @@ class AbstractFactoryTestCase(unittest.TestCase):
class TestObjectFactory(base.Factory):
pass
- self.assertRaises(base.FactoryError, TestObjectFactory.build)
- self.assertRaises(base.FactoryError, TestObjectFactory.create)
+ self.assertRaises(errors.FactoryError, TestObjectFactory.build)
+ self.assertRaises(errors.FactoryError, TestObjectFactory.create)
def test_abstract_factory_not_inherited(self):
"""abstract=True isn't propagated to child classes."""
@@ -110,8 +111,8 @@ class AbstractFactoryTestCase(unittest.TestCase):
abstract = False
model = None
- self.assertRaises(base.FactoryError, TestObjectFactory.build)
- self.assertRaises(base.FactoryError, TestObjectFactory.create)
+ self.assertRaises(errors.FactoryError, TestObjectFactory.build)
+ self.assertRaises(errors.FactoryError, TestObjectFactory.create)
class OptionsTests(unittest.TestCase):
@@ -134,25 +135,28 @@ class OptionsTests(unittest.TestCase):
self.assertEqual(AbstractFactory, AbstractFactory._meta.counter_reference)
def test_declaration_collecting(self):
- lazy = declarations.LazyAttribute(lambda _o: 1)
+ lazy = declarations.LazyFunction(int)
+ lazy2 = declarations.LazyAttribute(lambda _o: 1)
postgen = declarations.PostGenerationDeclaration()
class AbstractFactory(base.Factory):
x = 1
y = lazy
+ y2 = lazy2
z = postgen
# Declarations aren't removed
self.assertEqual(1, AbstractFactory.x)
self.assertEqual(lazy, AbstractFactory.y)
+ self.assertEqual(lazy2, AbstractFactory.y2)
self.assertEqual(postgen, AbstractFactory.z)
# And are available in class Meta
- self.assertEqual({'x': 1, 'y': lazy}, AbstractFactory._meta.declarations)
+ self.assertEqual({'x': 1, 'y': lazy, 'y2': lazy2}, AbstractFactory._meta.declarations)
self.assertEqual({'z': postgen}, AbstractFactory._meta.postgen_declarations)
def test_inherited_declaration_collecting(self):
- lazy = declarations.LazyAttribute(lambda _o: 1)
+ lazy = declarations.LazyFunction(int)
lazy2 = declarations.LazyAttribute(lambda _o: 2)
postgen = declarations.PostGenerationDeclaration()
postgen2 = declarations.PostGenerationDeclaration()
@@ -178,7 +182,7 @@ class OptionsTests(unittest.TestCase):
self.assertEqual({'z': postgen, 'b': postgen2}, OtherFactory._meta.postgen_declarations)
def test_inherited_declaration_shadowing(self):
- lazy = declarations.LazyAttribute(lambda _o: 1)
+ lazy = declarations.LazyFunction(int)
lazy2 = declarations.LazyAttribute(lambda _o: 2)
postgen = declarations.PostGenerationDeclaration()
postgen2 = declarations.PostGenerationDeclaration()
diff --git a/tests/test_containers.py b/tests/test_containers.py
index 9107b0d..ea6ddff 100644
--- a/tests/test_containers.py
+++ b/tests/test_containers.py
@@ -23,6 +23,7 @@
from factory import base
from factory import containers
from factory import declarations
+from factory import errors
from .compat import unittest
@@ -91,7 +92,7 @@ class LazyStubTestCase(unittest.TestCase):
'two': self.LazyAttr('one'),
})
- self.assertRaises(containers.CyclicDefinitionError, getattr, stub, 'one')
+ self.assertRaises(errors.CyclicDefinitionError, getattr, stub, 'one')
def test_cyclic_definition_rescue(self):
class LazyAttrDefault(self.LazyAttr):
@@ -124,97 +125,56 @@ class LazyStubTestCase(unittest.TestCase):
class AttributeBuilderTestCase(unittest.TestCase):
- def test_empty(self):
- """Tests building attributes from an empty definition."""
+
+ def make_fake_factory(self, decls):
+ class Meta:
+ declarations = decls
+ parameters = {}
+ parameters_dependencies = {}
class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- return extra
+ _meta = Meta
@classmethod
def _generate_next_sequence(cls):
return 1
+ return FakeFactory
+
+ def test_empty(self):
+ """Tests building attributes from an empty definition."""
+
+ FakeFactory = self.make_fake_factory({})
ab = containers.AttributeBuilder(FakeFactory)
self.assertEqual({}, ab.build(create=False))
def test_factory_defined(self):
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': 1}
- d.update(extra)
- return d
-
- @classmethod
- def _generate_next_sequence(cls):
- return 1
-
+ FakeFactory = self.make_fake_factory({'one': 1})
ab = containers.AttributeBuilder(FakeFactory)
+
self.assertEqual({'one': 1}, ab.build(create=False))
def test_extended(self):
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': 1}
- d.update(extra)
- return d
-
- @classmethod
- def _generate_next_sequence(cls):
- return 1
-
+ FakeFactory = self.make_fake_factory({'one': 1})
ab = containers.AttributeBuilder(FakeFactory, {'two': 2})
self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False))
def test_overridden(self):
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': 1}
- d.update(extra)
- return d
-
- @classmethod
- def _generate_next_sequence(cls):
- return 1
-
+ FakeFactory = self.make_fake_factory({'one': 1})
ab = containers.AttributeBuilder(FakeFactory, {'one': 2})
self.assertEqual({'one': 2}, ab.build(create=False))
def test_factory_defined_sequence(self):
seq = declarations.Sequence(lambda n: 'xx%d' % n)
-
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': seq}
- d.update(extra)
- return d
-
- @classmethod
- def _generate_next_sequence(cls):
- return 1
+ FakeFactory = self.make_fake_factory({'one': seq})
ab = containers.AttributeBuilder(FakeFactory)
self.assertEqual({'one': 'xx1'}, ab.build(create=False))
def test_additionnal_sequence(self):
seq = declarations.Sequence(lambda n: 'xx%d' % n)
-
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': 1}
- d.update(extra)
- return d
-
- @classmethod
- def _generate_next_sequence(cls):
- return 1
+ FakeFactory = self.make_fake_factory({'one': 1})
ab = containers.AttributeBuilder(FakeFactory, extra={'two': seq})
self.assertEqual({'one': 1, 'two': 'xx1'}, ab.build(create=False))
@@ -222,34 +182,27 @@ class AttributeBuilderTestCase(unittest.TestCase):
def test_replaced_sequence(self):
seq = declarations.Sequence(lambda n: 'xx%d' % n)
seq2 = declarations.Sequence(lambda n: 'yy%d' % n)
-
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': seq}
- d.update(extra)
- return d
-
- @classmethod
- def _generate_next_sequence(cls):
- return 1
+ FakeFactory = self.make_fake_factory({'one': seq})
ab = containers.AttributeBuilder(FakeFactory, extra={'one': seq2})
self.assertEqual({'one': 'yy1'}, ab.build(create=False))
- def test_lazy_attribute(self):
- la = declarations.LazyAttribute(lambda a: a.one * 2)
+ def test_lazy_function(self):
+ lf = declarations.LazyFunction(int)
+ FakeFactory = self.make_fake_factory({'one': 1, 'two': lf})
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': 1, 'two': la}
- d.update(extra)
- return d
+ ab = containers.AttributeBuilder(FakeFactory)
+ self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False))
- @classmethod
- def _generate_next_sequence(cls):
- return 1
+ ab = containers.AttributeBuilder(FakeFactory, {'one': 4})
+ self.assertEqual({'one': 4, 'two': 0}, ab.build(create=False))
+
+ ab = containers.AttributeBuilder(FakeFactory, {'one': 4, 'three': lf})
+ self.assertEqual({'one': 4, 'two': 0, 'three': 0}, ab.build(create=False))
+
+ def test_lazy_attribute(self):
+ la = declarations.LazyAttribute(lambda a: a.one * 2)
+ FakeFactory = self.make_fake_factory({'one': 1, 'two': la})
ab = containers.AttributeBuilder(FakeFactory)
self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False))
@@ -265,18 +218,12 @@ class AttributeBuilderTestCase(unittest.TestCase):
pass
sf = declarations.SubFactory(FakeInnerFactory)
-
- class FakeFactory(object):
- @classmethod
- def declarations(cls, extra):
- d = {'one': sf, 'two': 2}
- d.update(extra)
- return d
+ FakeFactory = self.make_fake_factory({'one': sf, 'two': 2})
ab = containers.AttributeBuilder(FakeFactory, {'one__blah': 1, 'two__bar': 2})
self.assertTrue(ab.has_subfields(sf))
self.assertEqual(['one'], list(ab._subfields.keys()))
- self.assertEqual(2, ab._attrs['two__bar'])
+ self.assertEqual(2, ab._declarations['two__bar'])
def test_sub_factory(self):
pass
diff --git a/tests/test_django.py b/tests/test_django.py
index 103df91..08349b9 100644
--- a/tests/test_django.py
+++ b/tests/test_django.py
@@ -362,6 +362,22 @@ class DjangoAbstractBaseSequenceTestCase(django_test.TestCase):
obj = ConcreteGrandSonFactory()
self.assertEqual(1, obj.pk)
+ def test_optional_abstract(self):
+ """Users need not describe the factory for an abstract model as abstract."""
+ class AbstractBaseFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = models.AbstractBase
+
+ foo = factory.Sequence(lambda n: "foo%d" % n)
+
+ class ConcreteSonFactory(AbstractBaseFactory):
+ class Meta:
+ model = models.ConcreteSon
+
+ obj = ConcreteSonFactory()
+ self.assertEqual(1, obj.pk)
+ self.assertEqual("foo0", obj.foo)
+
@unittest.skipIf(django is None, "Django not installed.")
class DjangoRelatedFieldTestCase(django_test.TestCase):
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index 3f9c434..4c3873a 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -189,6 +189,19 @@ class FuzzyDecimalTestCase(unittest.TestCase):
self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res)
+ @unittest.skipIf(compat.PY2, "decimal.FloatOperation was added in Py3")
+ def test_no_approximation(self):
+ """We should not go through floats in our fuzzy calls unless actually needed."""
+ fuzz = fuzzy.FuzzyDecimal(0, 10)
+
+ decimal_context = decimal.getcontext()
+ old_traps = decimal_context.traps[decimal.FloatOperation]
+ try:
+ decimal_context.traps[decimal.FloatOperation] = True
+ fuzz.evaluate(2, None, None)
+ finally:
+ decimal_context.traps[decimal.FloatOperation] = old_traps
+
class FuzzyDateTestCase(unittest.TestCase):
@classmethod
diff --git a/tests/test_using.py b/tests/test_using.py
index c7d2b85..0ce29e9 100644
--- a/tests/test_using.py
+++ b/tests/test_using.py
@@ -27,6 +27,7 @@ import sys
import warnings
import factory
+from factory import errors
from .compat import is_python2, unittest
from . import tools
@@ -40,6 +41,15 @@ class TestObject(object):
self.four = four
self.five = five
+ def as_dict(self):
+ return dict(
+ one=self.one,
+ two=self.two,
+ three=self.three,
+ four=self.four,
+ five=self.five,
+ )
+
class FakeModel(object):
@classmethod
@@ -292,6 +302,19 @@ class SimpleBuildTestCase(unittest.TestCase):
self.assertEqual(obj.three, 5)
self.assertEqual(obj.four, None)
+ def test_build_to_dict(self):
+ # We have a generic factory
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ one = 'one'
+ two = factory.LazyAttribute(lambda o: o.one * 2)
+
+ # Now, get a dict out of it
+ obj = factory.build(dict, FACTORY_CLASS=TestObjectFactory)
+ self.assertEqual({'one': 'one', 'two': 'oneone'}, obj)
+
class UsingFactoryTestCase(unittest.TestCase):
def test_attribute(self):
@@ -1092,6 +1115,119 @@ class KwargAdjustTestCase(unittest.TestCase):
self.assertEqual(42, obj.attributes)
+class TraitTestCase(unittest.TestCase):
+ def test_traits(self):
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ class Params:
+ even = factory.Trait(two=True, four=True)
+ odd = factory.Trait(one=True, three=True, five=True)
+
+ obj1 = TestObjectFactory()
+ self.assertEqual(obj1.as_dict(),
+ dict(one=None, two=None, three=None, four=None, five=None))
+
+ obj2 = TestObjectFactory(even=True)
+ self.assertEqual(obj2.as_dict(),
+ dict(one=None, two=True, three=None, four=True, five=None))
+
+ obj3 = TestObjectFactory(odd=True)
+ self.assertEqual(obj3.as_dict(),
+ dict(one=True, two=None, three=True, four=None, five=True))
+
+ obj4 = TestObjectFactory(even=True, odd=True)
+ self.assertEqual(obj4.as_dict(),
+ dict(one=True, two=True, three=True, four=True, five=True))
+
+ obj5 = TestObjectFactory(odd=True, two=True)
+ self.assertEqual(obj5.as_dict(),
+ dict(one=True, two=True, three=True, four=None, five=True))
+
+ def test_traits_inheritance(self):
+ """A trait can be set in an inherited class."""
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ class Params:
+ even = factory.Trait(two=True, four=True)
+ odd = factory.Trait(one=True, three=True, five=True)
+
+ class EvenObjectFactory(TestObjectFactory):
+ even = True
+
+ # Simple call
+ obj1 = EvenObjectFactory()
+ self.assertEqual(obj1.as_dict(),
+ dict(one=None, two=True, three=None, four=True, five=None))
+
+ # Force-disable it
+ obj2 = EvenObjectFactory(even=False)
+ self.assertEqual(obj2.as_dict(),
+ dict(one=None, two=None, three=None, four=None, five=None))
+
+ def test_traits_override(self):
+ """Override a trait in a subclass."""
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ class Params:
+ even = factory.Trait(two=True, four=True)
+ odd = factory.Trait(one=True, three=True, five=True)
+
+ class WeirdMathFactory(TestObjectFactory):
+ class Params:
+ # Here, one is even.
+ even = factory.Trait(two=True, four=True, one=True)
+
+ obj = WeirdMathFactory(even=True)
+ self.assertEqual(obj.as_dict(),
+ dict(one=True, two=True, three=None, four=True, five=None))
+
+ def test_traits_chaining(self):
+ """Use a trait to enable other traits."""
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ class Params:
+ even = factory.Trait(two=True, four=True)
+ odd = factory.Trait(one=True, three=True, five=True)
+ full = factory.Trait(even=True, odd=True)
+
+ # Setting "full" should enable all fields.
+ obj = TestObjectFactory(full=True)
+ self.assertEqual(obj.as_dict(),
+ dict(one=True, two=True, three=True, four=True, five=True))
+
+ # Does it break usual patterns?
+ obj1 = TestObjectFactory()
+ self.assertEqual(obj1.as_dict(),
+ dict(one=None, two=None, three=None, four=None, five=None))
+
+ obj2 = TestObjectFactory(even=True)
+ self.assertEqual(obj2.as_dict(),
+ dict(one=None, two=True, three=None, four=True, five=None))
+
+ obj3 = TestObjectFactory(odd=True)
+ self.assertEqual(obj3.as_dict(),
+ dict(one=True, two=None, three=True, four=None, five=True))
+
+ def test_prevent_cyclic_traits(self):
+
+ with self.assertRaises(errors.CyclicDefinitionError):
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+
+ class Params:
+ a = factory.Trait(b=True, one=True)
+ b = factory.Trait(a=True, two=True)
+
+
class SubFactoryTestCase(unittest.TestCase):
def test_sub_factory(self):
class TestModel2(FakeModel):
@@ -1132,6 +1268,26 @@ class SubFactoryTestCase(unittest.TestCase):
self.assertEqual('x0x', test_model.two.one)
self.assertEqual('x0xx0x', test_model.two.two)
+ def test_sub_factory_with_lazy_fields_access_factory_parent(self):
+ class TestModel2(FakeModel):
+ pass
+
+ class TestModelFactory(FakeModelFactory):
+ class Meta:
+ model = TestModel
+ one = 3
+
+ class TestModel2Factory(FakeModelFactory):
+ class Meta:
+ model = TestModel2
+ one = 'parent'
+ child = factory.SubFactory(TestModelFactory,
+ one=factory.LazyAttribute(lambda o: '%s child' % o.factory_parent.one),
+ )
+
+ test_model = TestModel2Factory()
+ self.assertEqual('parent child', test_model.child.one)
+
def test_sub_factory_and_sequence(self):
class TestObject(object):
def __init__(self, **kwargs):
@@ -1924,6 +2080,36 @@ class PostGenerationTestCase(unittest.TestCase):
self.assertEqual(3, related.one)
self.assertEqual(4, related.two)
+ def test_related_factory_selfattribute(self):
+ class TestRelatedObject(object):
+ def __init__(self, obj=None, one=None, two=None):
+ obj.related = self
+ self.one = one
+ self.two = two
+ self.three = obj
+
+ class TestRelatedObjectFactory(factory.Factory):
+ class Meta:
+ model = TestRelatedObject
+ one = 1
+ two = factory.LazyAttribute(lambda o: o.one + 1)
+
+ class TestObjectFactory(factory.Factory):
+ class Meta:
+ model = TestObject
+ one = 3
+ two = 2
+ three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj',
+ two=factory.SelfAttribute('obj.two'),
+ )
+
+ obj = TestObjectFactory.build(two=4)
+ self.assertEqual(3, obj.one)
+ self.assertEqual(4, obj.two)
+ self.assertEqual(1, obj.related.one)
+ self.assertEqual(4, obj.related.two)
+
+
class RelatedFactoryExtractionTestCase(unittest.TestCase):
def setUp(self):
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..4ccade6
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,39 @@
+[tox]
+envlist =
+ py{27,34}-django{17,18,19}
+ py{27,34}-alchemy10
+ py{27,34}-mongoengine010
+ examples
+ lint
+
+toxworkdir = {env:TOX_WORKDIR:.tox}
+
+[testenv]
+deps =
+ -rrequirements_test.txt
+ django17: Django>=1.7,<1.8
+ django18: Django>=1.8,<1.9
+ django19: Django>=1.9,<1.10
+ django{17,18,19}: Pillow
+ alchemy10: SQLAlchemy>=1.0,<1.1
+ mongoengine010: mongoengine>=0.10,<0.11
+
+whitelist_externals = make
+commands = make test
+
+[testenv:examples]
+basepython = python3.4
+deps =
+ -rrequirements_test.txt
+ -rexamples/requirements.txt
+
+whitelist_externals = make
+commands = make example-test
+
+[testenv:lint]
+deps =
+ pylint
+ check_manifest
+
+whitelist_externals = make
+commands = make lint