diff options
Diffstat (limited to 'docs')
-rw-r--r-- | docs/changelog.rst | 138 | ||||
-rw-r--r-- | docs/conf.py | 8 | ||||
-rw-r--r-- | docs/examples.rst | 18 | ||||
-rw-r--r-- | docs/fuzzy.rst | 39 | ||||
-rw-r--r-- | docs/ideas.rst | 5 | ||||
-rw-r--r-- | docs/introduction.rst | 41 | ||||
-rw-r--r-- | docs/orms.rst | 223 | ||||
-rw-r--r-- | docs/recipes.rst | 173 | ||||
-rw-r--r-- | docs/reference.rst | 335 |
9 files changed, 832 insertions, 148 deletions
diff --git a/docs/changelog.rst b/docs/changelog.rst index 47d1139..fa542f4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,17 +1,149 @@ ChangeLog ========= +.. _v2.6.0: + +2.6.0 (2015-10-20) +------------------ + +*New:* + + - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) + - Add support for random-yet-realistic values through `fake-factory <https://pypi.python.org/pypi/fake-factory>`_, + through the :class:`factory.Faker` class. + - :class:`factory.Iterator` no longer begins iteration of its argument at import time, + thus allowing to pass in a lazy iterator such as a Django queryset + (i.e ``factory.Iterator(models.MyThingy.objects.all())``). + - Simplify imports for ORM layers, now available through a simple ``factory`` import, + at ``factory.alchemy.SQLAlchemyModelFactory`` / ``factory.django.DjangoModelFactory`` / ``factory.mongoengine.MongoEngineFactory``. + +*Bugfix:* + + - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. + - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching + - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required + - :issue:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago. + +.. _v2.5.2: + +2.5.2 (2015-04-21) +------------------ + +*Bugfix:* + + - Add support for Django 1.7/1.8 + - Add support for mongoengine>=0.9.0 / pymongo>=2.1 + +.. _v2.5.1: + +2.5.1 (2015-03-27) +------------------ + +*Bugfix:* + + - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`) + - Allow passing declarations (e.g :class:`~factory.Sequence`) as parameters to :class:`~factory.django.FileField` + and :class:`~factory.django.ImageField`. + +.. _v2.5.0: + +2.5.0 (2015-03-26) +------------------ + +*New:* + + - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). + - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). + - Support non-default databases at the factory level (see :issue:`171`) + - Make :class:`factory.django.FileField` and :class:`factory.django.ImageField` non-post_generation, i.e normal fields also available in ``save()`` (see :issue:`141`). + +*Bugfix:* + + - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). + - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`). + + +*Deprecation:* + + - Remove deprecated features from :ref:`v2.4.0` + - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy; + this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. See https://github.com/rbarrois/factory_boy/commit/13d310f for technical details. + +.. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature. + This could trigger some bugs when tests expected a non-zero sequence reference. + +Upgrading +""""""""" + +.. warning:: Version 2.5.0 removes features that were marked as deprecated in :ref:`v2.4.0 <v2.4.0>`. + +All ``FACTORY_*``-style attributes are now declared in a ``class Meta:`` section: + +.. code-block:: python + + # Old-style, deprecated + class MyFactory(factory.Factory): + FACTORY_FOR = models.MyModel + FACTORY_HIDDEN_ARGS = ['a', 'b', 'c'] + + # New-style + class MyFactory(factory.Factory): + class Meta: + model = models.MyModel + exclude = ['a', 'b', 'c'] + +A simple shell command to upgrade the code would be: + +.. code-block:: sh + + # sed -i: inplace update + # grep -l: only file names, not matching lines + sed -i 's/FACTORY_FOR =/class Meta:\n model =/' $(grep -l FACTORY_FOR $(find . -name '*.py')) + +This takes care of all ``FACTORY_FOR`` occurences; the files containing other attributes to rename can be found with ``grep -R FACTORY .`` + + +.. _v2.4.1: + +2.4.1 (2014-06-23) +------------------ + +*Bugfix:* + + - Fix overriding deeply inherited attributes (set in one factory, overridden in a subclass, used in a sub-sub-class). .. _v2.4.0: -2.4.0 (master) --------------- +2.4.0 (2014-06-21) +------------------ *New:* - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov <https://github.com/ilya-pirogov>`_ (:issue:`120`) - - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov <https://github.com>`_ (:issue:`122`) + - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov <https://github.com/ilya-pirogov>`_ (:issue:`122`) - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`) + - Declare target model and other non-declaration fields in a ``class Meta`` section. + +*Deprecation:* + + - Use of ``FACTORY_FOR`` and other ``FACTORY`` class-level attributes is deprecated and will be removed in 2.5. + Those attributes should now declared within the :class:`class Meta <factory.FactoryOptions>` attribute: + + For :class:`factory.Factory`: + + * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` + * Rename :attr:`~factory.Factory.ABSTRACT_FACTORY` to :attr:`~factory.FactoryOptions.abstract` + * Rename :attr:`~factory.Factory.FACTORY_STRATEGY` to :attr:`~factory.FactoryOptions.strategy` + * Rename :attr:`~factory.Factory.FACTORY_ARG_PARAMETERS` to :attr:`~factory.FactoryOptions.inline_args` + * Rename :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` to :attr:`~factory.FactoryOptions.exclude` + + For :class:`factory.django.DjangoModelFactory`: + + * Rename :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` to :attr:`~factory.django.DjangoOptions.django_get_or_create` + + For :class:`factory.alchemy.SQLAlchemyModelFactory`: + + * Rename :attr:`~factory.alchemy.SQLAlchemyModelFactory.FACTORY_SESSION` to :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session` .. _v2.3.1: diff --git a/docs/conf.py b/docs/conf.py index 4f76d45..d5b86f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ master_doc = 'index' # General information about the project. project = u'Factory Boy' -copyright = u'2011-2013, Raphaël Barrois, Mark Sandstrom' +copyright = u'2011-2015, Raphaël Barrois, Mark Sandstrom' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -114,7 +114,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -247,7 +247,7 @@ intersphinx_mapping = { 'http://docs.djangoproject.com/en/dev/_objects/', ), 'sqlalchemy': ( - 'http://docs.sqlalchemy.org/en/rel_0_8/', - 'http://docs.sqlalchemy.org/en/rel_0_8/objects.inv', + 'http://docs.sqlalchemy.org/en/rel_0_9/', + 'http://docs.sqlalchemy.org/en/rel_0_9/objects.inv', ), } diff --git a/docs/examples.rst b/docs/examples.rst index aab990a..e7f6057 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -56,14 +56,16 @@ And now, we'll define the related factories: class AccountFactory(factory.Factory): - FACTORY_FOR = objects.Account + class Meta: + model = objects.Account username = factory.Sequence(lambda n: 'john%s' % n) email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username) class ProfileFactory(factory.Factory): - FACTORY_FOR = objects.Profile + class Meta: + model = objects.Profile account = factory.SubFactory(AccountFactory) gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) @@ -112,12 +114,9 @@ We can now use our factories, for tests: def test_get_profile_stats(self): profiles = [] - for _ in xrange(4): - profiles.append(factories.ProfileFactory()) - for _ in xrange(2): - profiles.append(factories.FemaleProfileFactory()) - for _ in xrange(2): - profiles.append(factories.ProfileFactory(planet='Tatooine')) + profiles.extend(factories.ProfileFactory.create_batch(4)) + profiles.extend(factories.FemaleProfileFactory.create_batch(2)) + profiles.extend(factories.ProfileFactory.create_batch(2, planet="Tatooine")) stats = business_logic.profile_stats(profiles) self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets) @@ -131,8 +130,7 @@ Or for fixtures: from . import factories def make_objects(): - for _ in xrange(50): - factories.ProfileFactory() + factories.ProfileFactory.create_batch(size=50) # Let's create a few, known objects. factories.ProfileFactory( diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 1480419..6b06608 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -8,6 +8,8 @@ Some tests may be interested in testing with fuzzy, random values. This is handled by the :mod:`factory.fuzzy` module, which provides a few random declarations. +.. note:: Use ``import factory.fuzzy`` to load this module. + FuzzyAttribute -------------- @@ -62,8 +64,11 @@ FuzzyChoice The :class:`FuzzyChoice` fuzzer yields random choices from the given iterable. - .. note:: The passed in :attr:`choices` will be converted into a list at - declaration time. + .. note:: The passed in :attr:`choices` will be converted into a list upon + first use, not at declaration time. + + This allows passing in, for instance, a Django queryset that will + only hit the database during the database, not at import time. .. attribute:: choices @@ -338,3 +343,33 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its The method responsible for generating random values. *Must* be overridden in subclasses. + + +Managing randomness +------------------- + +Using :mod:`random` in factories allows to "fuzz" a program efficiently. +However, it's sometimes required to *reproduce* a failing test. + +:mod:`factory.fuzzy` uses a separate instance of :class:`random.Random`, +and provides a few helpers for this: + +.. method:: get_random_state() + + Call :meth:`get_random_state` to retrieve the random generator's current + state. + +.. method:: set_random_state(state) + + Use :meth:`set_random_state` to set a custom state into the random generator + (fetched from :meth:`get_random_state` in a previous run, for instance) + +.. method:: reseed_random(seed) + + The :meth:`reseed_random` function allows to load a chosen seed into the random generator. + + +Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD** +use :obj:`factory.fuzzy._random` as a randomness source; this ensures that +data they generate can be regenerated using the simple state from +:meth:`get_random_state`. diff --git a/docs/ideas.rst b/docs/ideas.rst index 914e640..6e3962d 100644 --- a/docs/ideas.rst +++ b/docs/ideas.rst @@ -4,5 +4,6 @@ Ideas This is a list of future features that may be incorporated into factory_boy: -* **A 'options' attribute**: instead of adding more class-level constants, use a django-style ``class Meta`` Factory attribute with all options there - +* When a :class:`Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere +* Define a proper set of rules for the support of third-party ORMs +* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) diff --git a/docs/introduction.rst b/docs/introduction.rst index 86e2046..d00154d 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -18,10 +18,11 @@ Basic usage ----------- -Factories declare a set of attributes used to instantiate an object, whose class is defined in the FACTORY_FOR attribute: +Factories declare a set of attributes used to instantiate an object, whose class is defined in the ``class Meta``'s ``model`` attribute: - Subclass ``factory.Factory`` (or a more suitable subclass) -- Set its ``FACTORY_FOR`` attribute to the target class +- Add a ``class Meta:`` block +- Set its ``model`` attribute to the target class - Add defaults for keyword args to pass to the associated class' ``__init__`` method @@ -31,7 +32,8 @@ Factories declare a set of attributes used to instantiate an object, whose class from . import base class UserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + model = base.User firstname = "John" lastname = "Doe" @@ -56,7 +58,8 @@ A given class may be associated to many :class:`~factory.Factory` subclasses: .. code-block:: python class EnglishUserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + model = base.User firstname = "John" lastname = "Doe" @@ -64,7 +67,8 @@ A given class may be associated to many :class:`~factory.Factory` subclasses: class FrenchUserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + model = base.User firstname = "Jean" lastname = "Dupont" @@ -88,7 +92,8 @@ This is achieved with the :class:`~factory.Sequence` declaration: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + model = models.User username = factory.Sequence(lambda n: 'user%d' % n) @@ -104,7 +109,8 @@ This is achieved with the :class:`~factory.Sequence` declaration: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + model = models.User @factory.sequence def username(n): @@ -121,7 +127,8 @@ taking the object being built and returning the value for the field: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + model = models.User username = factory.Sequence(lambda n: 'user%d' % n) email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username) @@ -146,7 +153,8 @@ taking the object being built and returning the value for the field: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + model = models.User username = factory.Sequence(lambda n: 'user%d' % n) @@ -168,7 +176,8 @@ and update them with its own declarations: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + model = base.User firstname = "John" lastname = "Doe" @@ -209,13 +218,14 @@ Non-kwarg arguments Some classes take a few, non-kwarg arguments first. -This is handled by the :data:`~factory.Factory.FACTORY_ARG_PARAMETERS` attribute: +This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute: .. code-block:: python class MyFactory(factory.Factory): - FACTORY_FOR = MyClass - FACTORY_ARG_PARAMETERS = ('x', 'y') + class Meta: + model = MyClass + inline_args = ('x', 'y') x = 1 y = 2 @@ -251,7 +261,8 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the .. code-block:: python class MyFactory(factory.Factory): - FACTORY_FOR = MyClass + class Meta: + model = MyClass .. code-block:: pycon @@ -265,6 +276,6 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the <MyClass: X (saved)> -The default strategy can be changed by setting the class-level :attr:`~factory.Factory.FACTORY_STRATEGY` attribute. +The default strategy can be changed by setting the ``class Meta`` :attr:`~factory.FactoryOptions.strategy` attribute. diff --git a/docs/orms.rst b/docs/orms.rst index c893cac..af20917 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -15,7 +15,7 @@ Django The first versions of factory_boy were designed specifically for Django, -but the library has now evolved to be framework-independant. +but the library has now evolved to be framework-independent. Most features should thus feel quite familiar to Django users. @@ -32,15 +32,36 @@ All factories for a Django :class:`~django.db.models.Model` should use the This class provides the following features: - * The :attr:`~factory.Factory.FACTORY_FOR` attribute also supports the ``'app.Model'`` + * The :attr:`~factory.FactoryOptions.model` attribute also supports the ``'app.Model'`` syntax * :func:`~factory.Factory.create()` uses :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>` - * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value * When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration` attributes, the base object will be :meth:`saved <django.db.models.Model.save>` once all post-generation hooks have run. - .. attribute:: FACTORY_DJANGO_GET_OR_CREATE + +.. note:: With Django versions 1.8.0 to 1.8.3, it was no longer possible to call ``.build()`` + on a factory if this factory used a :class:`~factory.SubFactory` pointing + to another model: Django refused to set a :class:`~djang.db.models.ForeignKey` + to an unsaved :class:`~django.db.models.Model` instance. + + See https://code.djangoproject.com/ticket/10811 and https://code.djangoproject.com/ticket/25160 for details. + + +.. class:: DjangoOptions(factory.base.FactoryOptions) + + The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters: + + .. attribute:: database + + .. versionadded:: 2.5.0 + + All queries to the related model will be routed to the given database. + It defaults to ``'default'``. + + .. attribute:: django_get_or_create + + .. versionadded:: 2.4.0 Fields whose name are passed in this list will be used to perform a :meth:`Model.objects.get_or_create() <django.db.models.query.QuerySet.get_or_create>` @@ -49,8 +70,9 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. code-block:: python class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = 'myapp.User' # Equivalent to ``FACTORY_FOR = myapp.models.User`` - FACTORY_DJANGO_GET_OR_CREATE = ('username',) + class Meta: + model = 'myapp.User' # Equivalent to ``model = myapp.models.User`` + django_get_or_create = ('username',) username = 'john' @@ -80,11 +102,13 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. code-block:: python class MyAbstractModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.MyAbstractModel - ABSTRACT_FACTORY = True + class Meta: + model = models.MyAbstractModel + abstract = True class MyConcreteModelFactory(MyAbstractModelFactory): - FACTORY_FOR = models.MyConcreteModel + class Meta: + model = models.MyConcreteModel Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. @@ -102,7 +126,7 @@ Extra fields :param str from_path: Use data from the file located at ``from_path``, and keep its filename :param file from_file: Use the contents of the provided file object; use its filename - if available + if available, unless ``filename`` is also provided. :param bytes data: Use the provided bytes as file contents :param str filename: The filename for the FileField @@ -112,7 +136,8 @@ Extra fields .. code-block:: python class MyFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.MyModel + class Meta: + model = models.MyModel the_file = factory.django.FileField(filename='the_file.dat') @@ -149,7 +174,8 @@ Extra fields .. code-block:: python class MyFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.MyModel + class Meta: + model = models.MyModel the_image = factory.django.ImageField(color='blue') @@ -188,7 +214,8 @@ To work around this problem, use the :meth:`mute_signals()` decorator/context ma @factory.django.mute_signals(signals.pre_save, signals.post_save) class FooFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Foo + class Meta: + model = models.Foo # ... @@ -241,11 +268,39 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin * :func:`~factory.Factory.create()` builds an instance through ``__init__`` then saves it. - .. note:: If the :attr:`associated class <factory.Factory.FACTORY_FOR>` is a :class:`mongoengine.EmbeddedDocument`, + .. note:: If the :attr:`associated class <factory.FactoryOptions.model` is a :class:`mongoengine.EmbeddedDocument`, the :meth:`~MongoEngineFactory.create` function won't "save" it, since this wouldn't make sense. This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document. +A minimalist example: + +.. code-block:: python + + import mongoengine + + class Address(mongoengine.EmbeddedDocument): + street = mongoengine.StringField() + + class Person(mongoengine.Document): + name = mongoengine.StringField() + address = mongoengine.EmbeddedDocumentField(Address) + + import factory + + class AddressFactory(factory.mongoengine.MongoEngineFactory): + class Meta: + model = Address + + street = factory.Sequence(lambda n: 'street%d' % n) + + class PersonFactory(factory.mongoengine.MongoEngineFactory): + class Meta: + model = Person + + name = factory.Sequence(lambda n: 'name%d' % n) + address = factory.SubFactory(AddressFactory) + SQLAlchemy ---------- @@ -255,7 +310,7 @@ SQLAlchemy Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class. -To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_SESSION" class attribute. +To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:`Meta.sqlalchemy_session <SQLAlchemyOptions.sqlalchemy_session>` attribute. .. _SQLAlchemy: http://www.sqlalchemy.org/ @@ -266,13 +321,23 @@ To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_S This class provides the following features: * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add` - * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value - .. attribute:: FACTORY_SESSION - Fields whose SQLAlchemy session object are passed will be used to communicate with the database +.. class:: SQLAlchemyOptions(factory.base.FactoryOptions) + + In addition to the usual parameters available in :class:`class Meta <factory.base.FactoryOptions>`, + a :class:`SQLAlchemyModelFactory` also supports the following settings: + + .. attribute:: sqlalchemy_session -A (very) simple exemple: + 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 @@ -280,9 +345,8 @@ A (very) simple exemple: from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker - session = scoped_session(sessionmaker()) engine = create_engine('sqlite://') - session.configure(bind=engine) + session = scoped_session(sessionmaker(bind=engine)) Base = declarative_base() @@ -295,10 +359,12 @@ A (very) simple exemple: Base.metadata.create_all(engine) + import factory - class UserFactory(SQLAlchemyModelFactory): - FACTORY_FOR = User - FACTORY_SESSION = session # the SQLAlchemy session object + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = User + sqlalchemy_session = session # the SQLAlchemy session object id = factory.Sequence(lambda n: n) name = factory.Sequence(lambda n: u'User %d' % n) @@ -311,3 +377,112 @@ A (very) simple exemple: <User: User 1> >>> session.query(User).all() [<User: User 1>] + + +Managing sessions +""""""""""""""""" + +Since `SQLAlchemy`_ is a general purpose library, +there is no "global" session management system. + +The most common pattern when working with unit tests and ``factory_boy`` +is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: + +* The test runner configures some project-wide :class:`~sqlalchemy.orm.scoping.scoped_session` +* Each :class:`~SQLAlchemyModelFactory` subclass uses this + :class:`~sqlalchemy.orm.scoping.scoped_session` as its :attr:`~SQLAlchemyOptions.sqlalchemy_session` +* The :meth:`~unittest.TestCase.tearDown` method of tests calls + :meth:`Session.remove <sqlalchemy.orm.scoping.scoped_session.remove>` + to reset the session. + +.. note:: See the excellent :ref:`SQLAlchemy guide on scoped_session <sqlalchemy:unitofwork_contextual>` + for details of :class:`~sqlalchemy.orm.scoping.scoped_session`'s usage. + + The basic idea is that declarative parts of the code (including factories) + need a simple way to access the "current session", + but that session will only be created and configured at a later point. + + The :class:`~sqlalchemy.orm.scoping.scoped_session` handles this, + by virtue of only creating the session when a query is sent to the database. + + +Here is an example layout: + +- A global (test-only?) file holds the :class:`~sqlalchemy.orm.scoping.scoped_session`: + +.. code-block:: python + + # myprojet/test/common.py + + from sqlalchemy import orm + Session = orm.scoped_session(orm.sessionmaker()) + + +- All factory access it: + +.. code-block:: python + + # myproject/factories.py + + import factory + import factory.alchemy + + from . import models + from .test import common + + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = models.User + + # Use the not-so-global scoped_session + # Warning: DO NOT USE common.Session()! + sqlalchemy_session = common.Session + + name = factory.Sequence(lambda n: "User %d" % n) + + +- The test runner configures the :class:`~sqlalchemy.orm.scoping.scoped_session` when it starts: + +.. code-block:: python + + # myproject/test/runtests.py + + import sqlalchemy + + from . import common + + def runtests(): + engine = sqlalchemy.create_engine('sqlite://') + + # It's a scoped_session, and now is the time to configure it. + common.Session.configure(bind=engine) + + run_the_tests + + +- :class:`test cases <unittest.TestCase>` use this ``scoped_session``, + and clear it after each test (for isolation): + +.. code-block:: python + + # myproject/test/test_stuff.py + + import unittest + + from . import common + + class MyTest(unittest.TestCase): + + def setUp(self): + # Prepare a new, clean session + self.session = common.Session() + + def test_something(self): + u = factories.UserFactory() + self.assertEqual([u], self.session.query(User).all()) + + def tearDown(self): + # Rollback the session => no changes to the database + self.session.rollback() + # Remove it, so that the next test gets a new Session() + common.Session.remove() diff --git a/docs/recipes.rst b/docs/recipes.rst index 7a6bf23..df86bac 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -26,12 +26,36 @@ use the :class:`~factory.SubFactory` declaration: from . import models class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + model = models.User first_name = factory.Sequence(lambda n: "Agent %03d" % n) group = factory.SubFactory(GroupFactory) +Choosing from a populated table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the target of the :class:`~django.db.models.ForeignKey` should be +chosen from a pre-populated table +(e.g :class:`django.contrib.contenttypes.models.ContentType`), +simply use a :class:`factory.Iterator` on the chosen queryset: + +.. code-block:: python + + import factory, factory.django + from . import models + + class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.User + + language = factory.Iterator(models.Language.objects.all()) + +Here, ``models.Language.objects.all()`` won't be evaluated until the +first call to ``UserFactory``; thus avoiding DB queries at import time. + + Reverse dependencies (reverse ForeignKey) ----------------------------------------- @@ -53,7 +77,8 @@ use a :class:`~factory.RelatedFactory` declaration: # factories.py class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + model = models.User log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) @@ -75,7 +100,8 @@ factory_boy allows to define attributes of such profiles dynamically when creati .. code-block:: python class ProfileFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = my_models.Profile + class Meta: + model = my_models.Profile title = 'Dr' # We pass in profile=None to prevent UserFactory from creating another profile @@ -83,7 +109,8 @@ factory_boy allows to define attributes of such profiles dynamically when creati user = factory.SubFactory('app.factories.UserFactory', profile=None) class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = auth_models.User + class Meta: + model = auth_models.User username = factory.Sequence(lambda n: "user_%d" % n) @@ -121,8 +148,8 @@ factory_boy related factories. method. -Simple ManyToMany ------------------ +Simple Many-to-many relationship +-------------------------------- Building the adequate link between two models depends heavily on the use case; factory_boy doesn't provide a "all in one tools" as for :class:`~factory.SubFactory` @@ -140,17 +167,19 @@ hook: class User(models.Model): name = models.CharField() - groups = models.ManyToMany(Group) + groups = models.ManyToManyField(Group) # factories.py class GroupFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Group + class Meta: + model = models.Group name = factory.Sequence(lambda n: "Group #%s" % n) class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + model = models.User name = "John Doe" @@ -175,8 +204,8 @@ the ``groups`` declaration will add passed in groups to the set of groups for th user. -ManyToMany with a 'through' ---------------------------- +Many-to-many relation with a 'through' +-------------------------------------- If only one link is required, this can be simply performed with a :class:`RelatedFactory`. @@ -190,7 +219,7 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: class Group(models.Model): name = models.CharField() - members = models.ManyToMany(User, through='GroupLevel') + members = models.ManyToManyField(User, through='GroupLevel') class GroupLevel(models.Model): user = models.ForeignKey(User) @@ -200,17 +229,20 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: # factories.py class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + model = models.User name = "John Doe" class GroupFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Group + class Meta: + model = models.Group name = "Admins" class GroupLevelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.GroupLevel + class Meta: + model = models.GroupLevel user = factory.SubFactory(UserFactory) group = factory.SubFactory(GroupFactory) @@ -273,20 +305,23 @@ Here, we want: # factories.py class CountryFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Country + class Meta: + model = models.Country name = factory.Iterator(["France", "Italy", "Spain"]) lang = factory.Iterator(['fr', 'it', 'es']) class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + model = models.User name = "John" lang = factory.SelfAttribute('country.lang') country = factory.SubFactory(CountryFactory) class CompanyFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Company + class Meta: + model = models.Company name = "ACME, Inc." country = factory.SubFactory(CountryFactory) @@ -302,14 +337,110 @@ default :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>` .. code-block:: python class UserFactory(factory.DjangoModelFactory): - FACTORY_FOR = UserenaSignup + class Meta: + model = UserenaSignup + username = "l7d8s" email = "my_name@example.com" password = "my_password" @classmethod - def _create(cls, target_class, *args, **kwargs): + def _create(cls, model_class, *args, **kwargs): """Override the default ``_create`` with our custom call.""" - manager = cls._get_manager(target_class) + manager = cls._get_manager(model_class) # The default would use ``manager.create(*args, **kwargs)`` return manager.create_user(*args, **kwargs) + + +Forcing the sequence counter +---------------------------- + +A common pattern with factory_boy is to use a :class:`factory.Sequence` declaration +to provide varying values to attributes declared as unique. + +However, it is sometimes useful to force a given value to the counter, for instance +to ensure that tests are properly reproductible. + +factory_boy provides a few hooks for this: + + +Forcing the value on a per-call basis + In order to force the counter for a specific :class:`~factory.Factory` instantiation, + just pass the value in the ``__sequence=42`` parameter: + + .. code-block:: python + + class AccountFactory(factory.Factory): + class Meta: + model = Account + uid = factory.Sequence(lambda n: n) + name = "Test" + + .. code-block:: pycon + + >>> obj1 = AccountFactory(name="John Doe", __sequence=10) + >>> obj1.uid # Taken from the __sequence counter + 10 + >>> obj2 = AccountFactory(name="Jane Doe") + >>> obj2.uid # The base sequence counter hasn't changed + 1 + + +Resetting the counter globally + If all calls for a factory must start from a deterministic number, + use :meth:`factory.Factory.reset_sequence`; this will reset the counter + to its initial value (as defined by :meth:`factory.Factory._setup_next_sequence`). + + .. code-block:: pycon + + >>> AccountFactory().uid + 1 + >>> AccountFactory().uid + 2 + >>> AccountFactory.reset_sequence() + >>> AccountFactory().uid # Reset to the initial value + 1 + >>> AccountFactory().uid + 2 + + It is also possible to reset the counter to a specific value: + + .. code-block:: pycon + + >>> AccountFactory.reset_sequence(10) + >>> AccountFactory().uid + 10 + >>> AccountFactory().uid + 11 + + This recipe is most useful in a :class:`~unittest.TestCase`'s + :meth:`~unittest.TestCase.setUp` method. + + +Forcing the initial value for all projects + The sequence counter of a :class:`~factory.Factory` can also be set + automatically upon the first call through the + :meth:`~factory.Factory._setup_next_sequence` method; this helps when the + objects's attributes mustn't conflict with pre-existing data. + + A typical example is to ensure that running a Python script twice will create + non-conflicting objects, by setting up the counter to "max used value plus one": + + .. code-block:: python + + class AccountFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.Account + + @classmethod + def _setup_next_sequence(cls): + try: + return models.Accounts.objects.latest('uid').uid + 1 + except models.Account.DoesNotExist: + return 1 + + .. code-block:: pycon + + >>> Account.objects.create(uid=42, name="Blah") + >>> AccountFactory.create() # Sets up the account number based on the latest uid + <Account uid=43, name=Test> diff --git a/docs/reference.rst b/docs/reference.rst index 53584a0..b5ccd16 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -11,37 +11,54 @@ For internals and customization points, please refer to the :doc:`internals` sec The :class:`Factory` class -------------------------- -.. class:: Factory +.. class:: FactoryOptions + + .. versionadded:: 2.4.0 + + A :class:`Factory`'s behaviour can be tuned through a few settings. - The :class:`Factory` class is the base of factory_boy features. + For convenience, they are declared in a single ``class Meta`` attribute: - It accepts a few specific attributes (must be specified on class declaration): + .. code-block:: python + + class MyFactory(factory.Factory): + class Meta: + model = MyObject + abstract = False - .. attribute:: FACTORY_FOR + .. attribute:: model This optional attribute describes the class of objects to generate. If unset, it will be inherited from parent :class:`Factory` subclasses. - .. attribute:: ABSTRACT_FACTORY + .. versionadded:: 2.4.0 + + .. attribute:: abstract This attribute indicates that the :class:`Factory` subclass should not be used to generate objects, but instead provides some extra defaults. It will be automatically set to ``True`` if neither the :class:`Factory` - subclass nor its parents define the :attr:`~Factory.FACTORY_FOR` attribute. + subclass nor its parents define the :attr:`~FactoryOptions.model` attribute. - .. attribute:: FACTORY_ARG_PARAMETERS + .. warning:: This flag is reset to ``False`` when a :class:`Factory` subclasses + another one if a :attr:`~FactoryOptions.model` is set. + + .. versionadded:: 2.4.0 + + .. attribute:: inline_args Some factories require non-keyword arguments to their :meth:`~object.__init__`. - They should be listed, in order, in the :attr:`FACTORY_ARG_PARAMETERS` + They should be listed, in order, in the :attr:`inline_args` attribute: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User - FACTORY_ARG_PARAMETERS = ('login', 'email') + class Meta: + model = User + inline_args = ('login', 'email') login = 'john' email = factory.LazyAttribute(lambda o: '%s@example.com' % o.login) @@ -53,22 +70,25 @@ The :class:`Factory` class <User: john> >>> User('john', 'john@example.com', firstname="John") # actual call - .. attribute:: FACTORY_HIDDEN_ARGS + .. versionadded:: 2.4.0 + + .. attribute:: exclude While writing a :class:`Factory` for some object, it may be useful to have general fields helping defining others, but that should not be - passed to the target class; for instance, a field named 'now' that would + passed to the model class; for instance, a field named 'now' that would hold a reference time used by other objects. - Factory fields whose name are listed in :attr:`FACTORY_HIDDEN_ARGS` will + Factory fields whose name are listed in :attr:`exclude` will be removed from the set of args/kwargs passed to the underlying class; they can be any valid factory_boy declaration: .. code-block:: python class OrderFactory(factory.Factory): - FACTORY_FOR = Order - FACTORY_HIDDEN_ARGS = ('now',) + class Meta: + model = Order + exclude = ('now',) now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow()) started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1)) @@ -83,6 +103,60 @@ The :class:`Factory` class >>> OrderFactory(now=datetime.datetime(2013, 4, 1, 10)) <Order: started 2013-04-01 09:00:00, paid 2013-04-01 09:10:00> + .. versionadded:: 2.4.0 + + + .. attribute:: rename + + Sometimes, a model expect 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 + rules: the keys of the :attr:`rename` dict are those used in the + :class:`Factory` declarations, and their values the new name: + + .. code-block:: python + + class ImageFactory(factory.Factory): + # The model expects "attributes" + form_attributes = ['thumbnail', 'black-and-white'] + + class Meta: + model = Image + rename = {'form_attributes': 'attributes'} + + .. versionadded: 2.6.0 + + + .. attribute:: strategy + + Use this attribute to change the strategy used by a :class:`Factory`. + The default is :data:`CREATE_STRATEGY`. + + + +.. class:: Factory + + + **Class-level attributes:** + + .. attribute:: _meta + + .. versionadded:: 2.4.0 + + The :class:`FactoryOptions` instance attached to a :class:`Factory` class is available + as a :attr:`_meta` attribute. + + .. attribute:: _options_class + + .. versionadded:: 2.4.0 + + If a :class:`Factory` subclass needs to define additional, extra options, it has to + provide a custom :class:`FactoryOptions` subclass. + + A pointer to that custom class should be provided as :attr:`_options_class` so that + the :class:`Factory`-building metaclass can use it instead. + **Base functions:** @@ -162,7 +236,7 @@ The :class:`Factory` class The :meth:`_adjust_kwargs` extension point allows for late fields tuning. It is called once keyword arguments have been resolved and post-generation - items removed, but before the :attr:`FACTORY_ARG_PARAMETERS` extraction + items removed, but before the :attr:`~FactoryOptions.inline_args` extraction phase. .. code-block:: python @@ -177,7 +251,6 @@ The :class:`Factory` class .. OHAI_VIM** - .. classmethod:: _setup_next_sequence(cls) This method will compute the first value to use for the sequence counter @@ -189,19 +262,19 @@ The :class:`Factory` class Subclasses may fetch the next free ID from the database, for instance. - .. classmethod:: _build(cls, target_class, *args, **kwargs) + .. classmethod:: _build(cls, model_class, *args, **kwargs) .. OHAI_VIM* This class method is called whenever a new instance needs to be built. - It receives the target class (provided to :attr:`FACTORY_FOR`), and + It receives the model class (provided to :attr:`~FactoryOptions.model`), and the positional and keyword arguments to use for the class once all has been computed. Subclasses may override this for custom APIs. - .. classmethod:: _create(cls, target_class, *args, **kwargs) + .. classmethod:: _create(cls, model_class, *args, **kwargs) .. OHAI_VIM* @@ -214,10 +287,11 @@ The :class:`Factory` class .. code-block:: python class BaseBackendFactory(factory.Factory): - ABSTRACT_FACTORY = True # Optional + class Meta: + abstract = True # Optional - def _create(cls, target_class, *args, **kwargs): - obj = target_class(*args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + obj = model_class(*args, **kwargs) obj.save() return obj @@ -254,7 +328,7 @@ The :class:`Factory` class >>> SomeFactory._next_sequence 4 - Since subclasses of a non-:attr:`abstract <factory.Factory.ABSTRACT_FACTORY>` + Since subclasses of a non-:attr:`abstract <factory.FactoryOptions.abstract>` :class:`~factory.Factory` share the same sequence counter, special care needs to be taken when resetting the counter of such a subclass. @@ -293,7 +367,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. but not persisted to any datastore. It is usually a simple call to the :meth:`~object.__init__` method of the - :attr:`~Factory.FACTORY_FOR` class. + :attr:`~FactoryOptions.model` class. .. data:: CREATE_STRATEGY @@ -316,7 +390,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. when using the ``create`` strategy. That policy will be used if the - :attr:`associated class <Factory.FACTORY_FOR>` has an ``objects`` + :attr:`associated class <FactoryOptions.model>` has an ``objects`` attribute *and* the :meth:`~Factory._create` classmethod of the :class:`Factory` wasn't overridden. @@ -337,7 +411,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. data:: STUB_STRATEGY The 'stub' strategy is an exception in the factory_boy world: it doesn't return - an instance of the :attr:`~Factory.FACTORY_FOR` class, and actually doesn't + an instance of the :attr:`~FactoryOptions.model` class, and actually doesn't require one to be present. Instead, it returns an instance of :class:`StubObject` whose attributes have been @@ -359,7 +433,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. class:: StubFactory(Factory) - An :attr:`abstract <Factory.ABSTRACT_FACTORY>` :class:`Factory`, + An :attr:`abstract <FactoryOptions.abstract>` :class:`Factory`, with a default strategy set to :data:`STUB_STRATEGY`. @@ -400,6 +474,83 @@ factory_boy supports two main strategies for generating instances, plus stubs. Declarations ------------ + +Faker +""""" + +.. class:: Faker(provider, locale=None, **kwargs) + + .. OHAIVIM** + + In order to easily define realistic-looking factories, + use the :class:`Faker` attribute declaration. + + This is a wrapper around `fake-factory <https://pypi.python.org/pypi/fake-factory>`_; + its argument is the name of a ``fake-factory`` provider: + + .. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + name = factory.Faker('name') + + .. code-block:: pycon + + >>> user = UserFactory() + >>> user.name + 'Lucy Cechtelar' + + + .. attribute:: locale + + If a custom locale is required for one specific field, + use the ``locale`` parameter: + + .. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + name = factory.Faker('name', locale='fr_FR') + + .. code-block:: pycon + + >>> user = UserFactory() + >>> user.name + 'Jean Valjean' + + + .. classmethod:: override_default_locale(cls, locale) + + If the locale needs to be overridden for a whole test, + use :meth:`~factory.Faker.override_default_locale`: + + .. code-block:: pycon + + >>> with factory.Faker.override_default_locale('de_DE'): + ... UserFactory() + <User: Johannes Brahms> + + .. classmethod:: add_provider(cls, locale=None) + + Some projects may need to fake fields beyond those provided by ``fake-factory``; + in such cases, use :meth:`factory.Faker.add_provider` to declare additional providers + for those fields: + + .. code-block:: python + + factory.Faker.add_provider(SmileyProvider) + + class FaceFactory(factory.Factory): + class Meta: + model = Face + + smiley = factory.Faker('smiley') + + LazyAttribute """"""""""""" @@ -414,7 +565,8 @@ accept the object being built as sole argument, and return a value. .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User username = 'john' email = factory.LazyAttribute(lambda o: '%s@example.com' % o.username) @@ -449,7 +601,8 @@ return value of the method: .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + model = User name = u"Jean" @@ -481,13 +634,14 @@ This declaration takes a single argument, a function accepting a single paramete .. note:: An extra kwarg argument, ``type``, may be provided. - This feature is deprecated in 1.3.0 and will be removed in 2.0.0. + This feature was deprecated in 1.3.0 and will be removed in 2.0.0. .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + model = User phone = factory.Sequence(lambda n: '123-555-%04d' % n) @@ -512,7 +666,8 @@ be the sequence counter - this might be confusing: .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + model = User @factory.sequence def phone(n): @@ -537,7 +692,8 @@ The sequence counter is shared across all :class:`Sequence` attributes of the .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User phone = factory.Sequence(lambda n: '%04d' % n) office = factory.Sequence(lambda n: 'A23-B%03d' % n) @@ -561,7 +717,8 @@ sequence counter is shared: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User phone = factory.Sequence(lambda n: '123-555-%04d' % n) @@ -596,7 +753,8 @@ class-level value. .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User uid = factory.Sequence(int) @@ -631,7 +789,8 @@ It takes a single argument, a function whose two parameters are, in order: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User login = 'john' email = factory.LazyAttributeSequence(lambda o, n: '%s@s%d.example.com' % (o.login, n)) @@ -655,7 +814,8 @@ handles more complex cases: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User login = 'john' @@ -692,7 +852,8 @@ The :class:`SubFactory` attribute should be called with: .. code-block:: python class FooFactory(factory.Factory): - FACTORY_FOR = Foo + class Meta: + model = Foo bar = factory.SubFactory(BarFactory) # Not BarFactory() @@ -705,7 +866,8 @@ Definition # A standard factory class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User # Various fields first_name = 'John' @@ -714,7 +876,8 @@ Definition # A factory for an object with a 'User' field class CompanyFactory(factory.Factory): - FACTORY_FOR = Company + class Meta: + model = Company name = factory.Sequence(lambda n: 'FactoryBoyz' + 'z' * n) @@ -794,13 +957,15 @@ This issue can be handled by passing the absolute import path to the target .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User username = 'john' main_group = factory.SubFactory('users.factories.GroupFactory') class GroupFactory(factory.Factory): - FACTORY_FOR = Group + class Meta: + model = Group name = "MyGroup" owner = factory.SubFactory(UserFactory) @@ -828,7 +993,8 @@ That declaration takes a single argument, a dot-delimited path to the attribute .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + model = User birthdate = factory.Sequence(lambda n: datetime.date(2000, 1, 1) + datetime.timedelta(days=n)) birthmonth = factory.SelfAttribute('birthdate.month') @@ -854,13 +1020,15 @@ gains an "upward" semantic through the double-dot notation, as used in Python im .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User language = 'en' class CompanyFactory(factory.Factory): - FACTORY_FOR = Company + class Meta: + model = Company country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, language=factory.SelfAttribute('..country.language')) @@ -888,7 +1056,8 @@ through the :attr:`~containers.LazyStub.factory_parent` attribute of the passed- .. code-block:: python class CompanyFactory(factory.Factory): - FACTORY_FOR = Company + class Meta: + model = Company country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, language=factory.LazyAttribute(lambda user: user.factory_parent.country.language), @@ -966,7 +1135,8 @@ adequate value. .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User # CATEGORY_CHOICES is a list of (key, title) tuples category = factory.Iterator(User.CATEGORY_CHOICES, getter=lambda c: c[0]) @@ -987,7 +1157,8 @@ use the :func:`iterator` decorator: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User @factory.iterator def name(): @@ -1030,7 +1201,8 @@ with the :class:`Dict` and :class:`List` attributes: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User is_superuser = False roles = factory.Dict({ @@ -1066,7 +1238,8 @@ with the :class:`Dict` and :class:`List` attributes: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User flags = factory.List([ 'user', @@ -1113,10 +1286,11 @@ For instance, a :class:`PostGeneration` hook is declared as ``post``: .. code-block:: python class SomeFactory(factory.Factory): - FACTORY_FOR = SomeObject + class Meta: + model = SomeObject @post_generation - def post(self, create, extracted, **kwargs): + def post(obj, create, extracted, **kwargs): obj.set_origin(create) .. OHAI_VIM** @@ -1128,7 +1302,7 @@ When calling the factory, some arguments will be extracted for this method: - Any argument starting with ``post__XYZ`` will be extracted, its ``post__`` prefix removed, and added to the kwargs passed to the post-generation hook. -Extracted arguments won't be passed to the :attr:`~Factory.FACTORY_FOR` class. +Extracted arguments won't be passed to the :attr:`~FactoryOptions.model` class. Thus, in the following call: @@ -1142,7 +1316,7 @@ Thus, in the following call: ) The ``post`` hook will receive ``1`` as ``extracted`` and ``{'y': 3, 'z__t': 42}`` -as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory.FACTORY_FOR``. +as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory._meta.model``. RelatedFactory @@ -1184,7 +1358,8 @@ RelatedFactory .. code-block:: python class FooFactory(factory.Factory): - FACTORY_FOR = Foo + class Meta: + model = Foo bar = factory.RelatedFactory(BarFactory) # Not BarFactory() @@ -1192,13 +1367,15 @@ RelatedFactory .. code-block:: python class CityFactory(factory.Factory): - FACTORY_FOR = City + class Meta: + model = City capital_of = None name = "Toronto" class CountryFactory(factory.Factory): - FACTORY_FOR = Country + class Meta: + model = Country lang = 'fr' capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris") @@ -1235,12 +1412,29 @@ 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 """""""""""""" .. class:: PostGeneration(callable) -The :class:`PostGeneration` declaration performs actions once the target object +The :class:`PostGeneration` declaration performs actions once the model object has been generated. Its sole argument is a callable, that will be called once the base object has @@ -1260,7 +1454,8 @@ as ``callable(obj, create, extracted, **kwargs)``, where: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User login = 'john' make_mbox = factory.PostGeneration( @@ -1280,7 +1475,8 @@ A decorator is also provided, decorating a single method accepting the same .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User login = 'john' @@ -1316,7 +1512,7 @@ PostGenerationMethodCall .. attribute:: method_name - The name of the method to call on the :attr:`~Factory.FACTORY_FOR` object + The name of the method to call on the :attr:`~FactoryOptions.model` object .. attribute:: args @@ -1340,7 +1536,8 @@ attribute like below: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User username = 'user' password = factory.PostGenerationMethodCall('set_password', @@ -1390,7 +1587,8 @@ factory during instantiation. .. code-block:: python class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = User + class Meta: + model = User username = 'user' password = factory.PostGenerationMethodCall('set_password', @@ -1404,7 +1602,8 @@ example, if we declared the ``password`` attribute like the following, .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User username = 'user' password = factory.PostGenerationMethodCall('set_password', '', 'sha1') @@ -1467,7 +1666,8 @@ Lightweight factory declaration # This is equivalent to: class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + model = User login = 'john' email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) @@ -1486,7 +1686,8 @@ Lightweight factory declaration # This is equivalent to: class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + model = models.User login = 'john' email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) |