diff options
author | Christopher Baines <mail@cbaines.net> | 2016-05-11 09:49:51 +0100 |
---|---|---|
committer | Christopher Baines <mail@cbaines.net> | 2016-05-11 09:49:51 +0100 |
commit | d54b452452a04f8109ef8dc48c9b46d1cb99f476 (patch) | |
tree | a31e97328932c485d80e3be808c9e3572d955c39 /docs | |
parent | 36ef8928c06a4bba5249442cd832a73ece1570c3 (diff) | |
parent | f1ed74e06dfb6851bc691ebfd8135c875154ad50 (diff) | |
download | factory-boy-d54b452452a04f8109ef8dc48c9b46d1cb99f476.tar factory-boy-d54b452452a04f8109ef8dc48c9b46d1cb99f476.tar.gz |
Merge tag 'v2.7.0' into debian/unstable
Release of factory_boy 2.7.0
Diffstat (limited to 'docs')
-rw-r--r-- | docs/changelog.rst | 25 | ||||
-rw-r--r-- | docs/examples.rst | 2 | ||||
-rw-r--r-- | docs/fuzzy.rst | 8 | ||||
-rw-r--r-- | docs/introduction.rst | 84 | ||||
-rw-r--r-- | docs/orms.rst | 16 | ||||
-rw-r--r-- | docs/recipes.rst | 47 | ||||
-rw-r--r-- | docs/reference.rst | 233 |
7 files changed, 392 insertions, 23 deletions
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 """"""""""""""""""""" |