summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristopher Baines <mail@cbaines.net>2016-05-11 09:49:51 +0100
committerChristopher Baines <mail@cbaines.net>2016-05-11 09:49:51 +0100
commitd54b452452a04f8109ef8dc48c9b46d1cb99f476 (patch)
treea31e97328932c485d80e3be808c9e3572d955c39
parent36ef8928c06a4bba5249442cd832a73ece1570c3 (diff)
parentf1ed74e06dfb6851bc691ebfd8135c875154ad50 (diff)
downloadfactory-boy-d54b452452a04f8109ef8dc48c9b46d1cb99f476.tar
factory-boy-d54b452452a04f8109ef8dc48c9b46d1cb99f476.tar.gz
Merge tag 'v2.7.0' into debian/unstable
Release of factory_boy 2.7.0
-rw-r--r--.travis.yml1
-rw-r--r--MANIFEST.in18
-rw-r--r--Makefile43
-rw-r--r--README.rst28
-rw-r--r--docs/changelog.rst25
-rw-r--r--docs/examples.rst2
-rw-r--r--docs/fuzzy.rst8
-rw-r--r--docs/introduction.rst84
-rw-r--r--docs/orms.rst16
-rw-r--r--docs/recipes.rst47
-rw-r--r--docs/reference.rst233
-rw-r--r--factory/__init__.py11
-rw-r--r--factory/base.py75
-rw-r--r--factory/containers.py122
-rw-r--r--factory/declarations.py66
-rw-r--r--factory/django.py7
-rw-r--r--factory/errors.py42
-rw-r--r--factory/fuzzy.py3
-rw-r--r--factory/utils.py4
-rw-r--r--requirements_dev.txt (renamed from dev_requirements.txt)5
-rw-r--r--requirements_test.txt2
-rwxr-xr-xsetup.py1
-rw-r--r--tests/test_base.py22
-rw-r--r--tests/test_containers.py163
-rw-r--r--tests/test_django.py16
-rw-r--r--tests/test_using.py156
-rw-r--r--tox.ini39
27 files changed, 1014 insertions, 225 deletions
diff --git a/.travis.yml b/.travis.yml
index ff805b0..938787b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,4 @@
+sudo: false
language: python
python:
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 da8ac88..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.9
-NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
-
-ALCHEMY ?= 1.0
-NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
-
-MONGOENGINE ?= 0.10
-NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))")
-
-REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt
-EXAMPLES_REQ_FILES = $(shell find $(EXAMPLES_DIR) -name requirements.txt)
-
all: default
default:
-install-deps: $(REQ_FILE)
- pip install --upgrade pip setuptools
- pip install --upgrade -r $<
- pip freeze
-
-$(REQ_FILE): dev_requirements.txt requirements.txt $(EXAMPLES_REQ_FILES)
- grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@
- echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@
- echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@
- echo "mongoengine>=$(MONGOENGINE),<$(NEXT_MONGOENGINE)" >> $@
-
-
clean:
find . -type f -name '*.pyc' -delete
find . -type f -path '*/__pycache__/*' -delete
find . -type d -empty -delete
- @rm -f auto_dev_requirements_*
@rm -rf tmp_test/
-test: 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 4d114e5..0e1f000 100644
--- a/README.rst
+++ b/README.rst
@@ -80,7 +80,7 @@ Links
* 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 to 3.5, 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
@@ -226,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
@@ -233,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
"""""""""
@@ -287,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
"""""""""""""""""""""
@@ -321,16 +337,6 @@ 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
------------
diff --git a/docs/changelog.rst b/docs/changelog.rst
index fa542f4..2341dfa 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,6 +1,31 @@
ChangeLog
=========
+.. _v2.7.0:
+
+2.7.0 (2016-04-19)
+------------------
+
+*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>`
+ - :issue:`256`, :issue:`292`: Improve error messages in corner cases
+
+*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/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 af20917..fb3543d 100644
--- a/docs/orms.rst
+++ b/docs/orms.rst
@@ -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
""""""""""""
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 b5ccd16..a060f75 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))
@@ -108,7 +111,7 @@ The :class:`Factory` class
.. attribute:: rename
- Sometimes, a model expect a field with a name already used by one
+ Sometimes, a model expects a field with a name already used by one
of :class:`Factory`'s methods.
In this case, the :attr:`rename` attributes allows to define renaming
@@ -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
"""""""""""""""""""""
diff --git a/factory/__init__.py b/factory/__init__.py
index c8bc396..421e1ab 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.1'
+__version__ = '2.7.0'
__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/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/containers.py b/factory/containers.py
index 0ae354b..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:
@@ -102,8 +99,10 @@ class LazyStub(object):
val = self.__attrs[name]
if isinstance(val, LazyValue):
self.__pending.append(name)
- val = val.evaluate(self, self.__containers)
- last = self.__pending.pop()
+ try:
+ val = val.evaluate(self, self.__containers)
+ finally:
+ last = self.__pending.pop()
assert name == last
self.__values[name] = val
return val
@@ -112,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:
@@ -121,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."""
@@ -129,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:
@@ -140,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
@@ -170,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.
"""
@@ -183,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.
@@ -214,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/django.py b/factory/django.py
index b3c508c..43434c2 100644
--- a/factory/django.py
+++ b/factory/django.py
@@ -29,6 +29,8 @@ import types
import logging
import functools
+from . import errors
+
"""factory_boy extensions for use with the Django framework."""
try:
@@ -157,6 +159,11 @@ class DjangoModelFactory(base.Factory):
key_fields = {}
for field in cls._meta.django_get_or_create:
+ if field not in kwargs:
+ raise errors.FactoryError(
+ "django_get_or_create - "
+ "Unable to find initialization value for '%s' in factory %s" %
+ (field, cls.__name__))
key_fields[field] = kwargs.pop(field)
key_fields['defaults'] = kwargs
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 a7e834c..71d1884 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -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 15dba0a..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
diff --git a/dev_requirements.txt b/requirements_dev.txt
index c78aa9d..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,8 +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 003dd08..3a0e699 100755
--- a/setup.py
+++ b/setup.py
@@ -61,7 +61,6 @@ 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",
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 083b306..1b87b2e 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
@@ -78,17 +79,39 @@ class LazyStubTestCase(unittest.TestCase):
self.assertEqual(2, stub.factory_parent.rank)
self.assertEqual(1, stub.factory_parent.factory_parent.rank)
- def test_cyclic_definition(self):
- class LazyAttr(containers.LazyValue):
- def __init__(self, attrname):
- self.attrname = attrname
+ class LazyAttr(containers.LazyValue):
+ def __init__(self, attrname):
+ self.attrname = attrname
+
+ def evaluate(self, obj, container=None):
+ return 1 + getattr(obj, self.attrname)
+ def test_cyclic_definition(self):
+ stub = containers.LazyStub({
+ 'one': self.LazyAttr('two'),
+ 'two': self.LazyAttr('one'),
+ })
+
+ self.assertRaises(errors.CyclicDefinitionError, getattr, stub, 'one')
+
+ def test_cyclic_definition_rescue(self):
+ class LazyAttrDefault(self.LazyAttr):
+ def __init__(self, attname, defvalue):
+ super(LazyAttrDefault, self).__init__(attname)
+ self.defvalue = defvalue
def evaluate(self, obj, container=None):
- return 1 + getattr(obj, self.attrname)
+ try:
+ return super(LazyAttrDefault, self).evaluate(obj, container)
+ except errors.CyclicDefinitionError:
+ return self.defvalue
- stub = containers.LazyStub({'one': LazyAttr('two'), 'two': LazyAttr('one')})
+ stub = containers.LazyStub({
+ 'one': LazyAttrDefault('two', 10),
+ 'two': self.LazyAttr('one'),
+ })
- self.assertRaises(containers.CyclicDefinitionError, getattr, stub, 'one')
+ self.assertEqual(10, stub.one)
+ self.assertEqual(11, stub.two)
def test_representation(self):
class RandomObj(object):
@@ -102,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))
@@ -200,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))
@@ -243,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_using.py b/tests/test_using.py
index 0a893c1..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):
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