From a98ad593280fb7ddef0b8e019c6106da09b931dc Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Mon, 23 Nov 2015 23:53:04 -0500 Subject: Clarify sequence behavior on inheritance Sequences are only shared via inheritance if the model of the subclass is the same as or a subclass of the model of the parent class. Clarify the docs on this point. --- docs/reference.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 6398d9a..44300a5 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -711,8 +711,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 -- cgit v1.2.3 From 819ffaf9efe0d5a3eee85afc847ceb6969242833 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 8 Dec 2015 12:56:00 +1100 Subject: Test LazyValues handling CyclicDefinitionError --- tests/test_containers.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/test_containers.py b/tests/test_containers.py index 083b306..9107b0d 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -78,18 +78,40 @@ 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 evaluate(self, obj, container=None): + return 1 + getattr(obj, self.attrname) - stub = containers.LazyStub({'one': LazyAttr('two'), 'two': LazyAttr('one')}) + def test_cyclic_definition(self): + stub = containers.LazyStub({ + 'one': self.LazyAttr('two'), + 'two': self.LazyAttr('one'), + }) self.assertRaises(containers.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): + try: + return super(LazyAttrDefault, self).evaluate(obj, container) + except containers.CyclicDefinitionError: + return self.defvalue + + stub = containers.LazyStub({ + 'one': LazyAttrDefault('two', 10), + 'two': self.LazyAttr('one'), + }) + + self.assertEqual(10, stub.one) + self.assertEqual(11, stub.two) + def test_representation(self): class RandomObj(object): pass -- cgit v1.2.3 From f023e5a477668b8374a75c78e87d946b21a27f15 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 8 Dec 2015 12:57:24 +1100 Subject: Don't leave AttributeBuilder in an inconsistent state on exceptions When one of the LazyValues raises an exception, don't leave its name in __pending stack of the AttributeBuilder, preventing evaluation of any other LazyValues. --- factory/containers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/factory/containers.py b/factory/containers.py index 0ae354b..ec33ca1 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -102,8 +102,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 -- cgit v1.2.3 From 38f4a69db8f71cb52b9e7fd8d6e20e7d052a5b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 01:13:17 +0100 Subject: Add ChangeLog for 2.6.1 --- docs/changelog.rst | 9 +++++++++ docs/recipes.rst | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fa542f4..dc3f967 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.6.1: + +2.6.1 (2016-02-10) +------------------ + +*New:* + + - :issue:`262`: Allow optional forced flush on SQLAlchemy, courtesy of `Minjung `_. + .. _v2.6.0: 2.6.0 (2015-10-20) diff --git a/docs/recipes.rst b/docs/recipes.rst index df86bac..a627e8b 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``. -- cgit v1.2.3 From f2c075c40fd331b7d26a9db72aad249b2165eac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Fri, 12 Feb 2016 17:31:04 +0100 Subject: factory: LazyFunction to just call a function in the simplest case No need to wrap it in a lambda to strip the object argument from LazyAttribute or the sequence argument from Sequence. --- README.rst | 5 +++++ docs/examples.rst | 2 ++ docs/introduction.rst | 29 +++++++++++++++++++++++++++++ docs/reference.rst | 40 ++++++++++++++++++++++++++++++++++++++-- factory/__init__.py | 1 + factory/declarations.py | 17 +++++++++++++++++ tests/test_base.py | 11 +++++++---- tests/test_containers.py | 23 +++++++++++++++++++++++ 8 files changed, 122 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 4d114e5..762fedb 100644 --- a/README.rst +++ b/README.rst @@ -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 """"""""" 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/introduction.rst b/docs/introduction.rst index d00154d..9a16c39 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() + + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + + + +.. note:: For complex cases when you happen to write a specific function, + the :meth:`~factory.@lazy_attribute` decorator should be more appropriate. + + LazyAttribute ------------- diff --git a/docs/reference.rst b/docs/reference.rst index b5ccd16..9e01213 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -90,7 +90,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)) @@ -551,6 +551,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() + + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + + +Decorator +~~~~~~~~~ + +The class :class:`LazyFunction` does not provide a decorator. + +For complex cases, use :meth:`LazyAttribute.lazy_attribute` directly. + LazyAttribute """"""""""""" @@ -1041,7 +1077,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 diff --git a/factory/__init__.py b/factory/__init__.py index c8bc396..1fa581b 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -43,6 +43,7 @@ from .base import ( from .faker import Faker from .declarations import ( + LazyFunction, LazyAttribute, Iterator, Sequence, diff --git a/factory/declarations.py b/factory/declarations.py index f0dbfe5..9ab7462 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. diff --git a/tests/test_base.py b/tests/test_base.py index 24f64e5..dd74e35 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -134,25 +134,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 +181,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..825e897 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -215,6 +215,29 @@ class AttributeBuilderTestCase(unittest.TestCase): ab = containers.AttributeBuilder(FakeFactory, extra={'one': seq2}) self.assertEqual({'one': 'yy1'}, ab.build(create=False)) + def test_lazy_function(self): + lf = declarations.LazyFunction(int) + + class FakeFactory(object): + @classmethod + def declarations(cls, extra): + d = {'one': 1, 'two': lf} + d.update(extra) + return d + + @classmethod + def _generate_next_sequence(cls): + return 1 + + ab = containers.AttributeBuilder(FakeFactory) + self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False)) + + 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) -- cgit v1.2.3 From 4b8008386804045c10d7bca10e2de0464cd5a85b Mon Sep 17 00:00:00 2001 From: yamaneko Date: Sat, 13 Feb 2016 18:18:39 +0900 Subject: Remove a nonexisting argumen "tz" --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 6b06608..fde1af1 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -199,7 +199,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. -- cgit v1.2.3 From efc8a7e873aaab5170e1dc2477aaf92c6fb59fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Feb 2016 00:00:03 +0100 Subject: fuzzy: Minor cleanup in BaseFuzzyDateTime The ``_now()`` method wasn't declared on the base class, only in its subclasses. --- factory/fuzzy.py | 3 +++ 1 file changed, 3 insertions(+) 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, -- cgit v1.2.3 From 1e98b18af457b8dc5e795be8d00c226c78682942 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Fri, 19 Feb 2016 00:14:33 -0800 Subject: Switch Travis builds to the container-based infrastructure https://docs.travis-ci.com/user/migrating-from-legacy/ --- .travis.yml | 1 + 1 file changed, 1 insertion(+) 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: -- cgit v1.2.3 From 639b3a3763194374a71d2049a2da7d2e12cb1b1f Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Sun, 21 Feb 2016 15:14:58 -0800 Subject: Add note about deprecate/remove Fuzzy attributes Full discussion in https://github.com/rbarrois/factory_boy/issues/271 Wanted to get something mentioned in the docs immediately. --- docs/fuzzy.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index fde1af1..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 `_ equivalents. Further + discussion here: + ``_ + Some tests may be interested in testing with fuzzy, random values. This is handled by the :mod:`factory.fuzzy` module, which provides a few -- cgit v1.2.3 From fb608987a4fb61ff6198a6497359ec00058b9253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 23 Feb 2016 00:53:44 +0100 Subject: Add test for "build as dict" trick (See #68). --- tests/test_using.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 0a893c1..3ef5403 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -292,6 +292,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): -- cgit v1.2.3 From 1210a06717fd5ebc866c977c30ae204822bbc4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 23 Feb 2016 00:59:45 +0100 Subject: docs: Add a proper recipe for dumping to dict This trick should help with #68. --- docs/recipes.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index a627e8b..a3df7be 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -444,3 +444,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 + + +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'} -- cgit v1.2.3 From 5071655e34ba7d51e42454a0fa0463bc4d7c9e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Fri, 4 Mar 2016 18:19:07 +0100 Subject: Stop advertising factory_boy supports Python 2.6 Support was already gone with literal sets. --- README.rst | 2 +- setup.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 762fedb..a08d37f 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 `_ | 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 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", -- cgit v1.2.3 From dc1de7297b9b508afad9386285aebd57ac3f11f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 12 Mar 2016 12:45:34 +0100 Subject: django: Clarify behavior around ``abstract=True`` See issue #280. --- tests/test_django.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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): -- cgit v1.2.3 From 094a66fb0e6a70c15cc7cbdee5d40ba5e128c433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 12 Mar 2016 12:47:25 +0100 Subject: docs: Del obsolete note on abstract Django models That section described code required when factory_boy was automagically computing sequence numbers from the current PK value. Closes #280 --- docs/orms.rst | 16 ---------------- 1 file changed, 16 deletions(-) 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 [, ] -.. 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 """""""""""" -- cgit v1.2.3 From eea28cce1544021f3d152782c9932a20402d6240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:11:58 +0200 Subject: Refactor: move error defs to a dedicated module. --- factory/__init__.py | 7 +++++-- factory/base.py | 31 ++++++++----------------------- factory/containers.py | 8 ++------ factory/errors.py | 42 ++++++++++++++++++++++++++++++++++++++++++ tests/test_base.py | 11 ++++++----- tests/test_containers.py | 3 ++- 6 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 factory/errors.py diff --git a/factory/__init__.py b/factory/__init__.py index 1fa581b..ccd71cd 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -32,14 +32,17 @@ 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 ( diff --git a/factory/base.py b/factory/base.py index 0f2af59..1ddb742 100644 --- a/factory/base.py +++ b/factory/base.py @@ -24,6 +24,7 @@ import logging from . import containers from . import declarations +from . import errors from . import utils logger = logging.getLogger('factory.generate') @@ -35,22 +36,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 +67,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): @@ -296,12 +281,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() @@ -477,7 +462,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 +665,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 +680,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..c591988 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: @@ -112,7 +109,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: 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/tests/test_base.py b/tests/test_base.py index dd74e35..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): diff --git a/tests/test_containers.py b/tests/test_containers.py index 825e897..20c773a 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 @@ -88,7 +89,7 @@ class LazyStubTestCase(unittest.TestCase): stub = containers.LazyStub({'one': LazyAttr('two'), 'two': LazyAttr('one')}) - self.assertRaises(containers.CyclicDefinitionError, getattr, stub, 'one') + self.assertRaises(errors.CyclicDefinitionError, getattr, stub, 'one') def test_representation(self): class RandomObj(object): -- cgit v1.2.3 From c77962de7dd7206ccab85b44da173832acbf5921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:13:34 +0200 Subject: Add a new Params section to factories. This handles parameters that alter the declarations of a factory. A few technical notes: - A parameter's outcome may alter other parameters - In order to fix that, we perform a (simple) cyclic definition detection at class declaration time. - Parameters may only be either naked values or ComplexParameter subclasses - Parameters are never passed to the underlying class --- docs/reference.rst | 46 +++++++++++++++++ factory/base.py | 44 ++++++++++++++++- factory/containers.py | 108 ++++++++++++++++++++++++++++++++++++---- factory/declarations.py | 33 +++++++++++++ factory/utils.py | 4 +- tests/test_containers.py | 125 +++++++++-------------------------------------- tests/test_using.py | 9 ++++ 7 files changed, 256 insertions(+), 113 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index e2f63db..8550f88 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1299,6 +1299,52 @@ with the :class:`Dict` and :class:`List` attributes: argument, if another type (tuple, set, ...) is required. +Parameters +"""""""""" + +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(duration='long') + + + +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). + + + Post-generation hooks """"""""""""""""""""" diff --git a/factory/base.py b/factory/base.py index 1ddb742..282e3b1 100644 --- a/factory/base.py +++ b/factory/base.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import collections import logging from . import containers @@ -92,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) @@ -106,6 +108,7 @@ class FactoryMetaClass(type): meta=attrs_meta, base_meta=base_meta, base_factory=base_factory, + params=attrs_params, ) return new_class @@ -148,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. @@ -186,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 @@ -204,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): @@ -211,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.""" @@ -242,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__) @@ -439,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) diff --git a/factory/containers.py b/factory/containers.py index c591988..d3f39c4 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -117,6 +117,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.""" @@ -125,7 +188,7 @@ class LazyValue(object): raise NotImplementedError("This is an abstract method.") -class OrderedDeclarationWrapper(LazyValue): +class DeclarationWrapper(LazyValue): """Lazy wrapper around an OrderedDeclaration. Attributes: @@ -136,7 +199,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 @@ -166,7 +229,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. """ @@ -179,20 +242,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. @@ -210,9 +300,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 9ab7462..ad1f72f 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -440,6 +440,39 @@ 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 [] + + +# 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/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/tests/test_containers.py b/tests/test_containers.py index 20c773a..a308353 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -103,97 +103,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)) @@ -201,34 +160,14 @@ 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_function(self): lf = declarations.LazyFunction(int) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1, 'two': lf} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': 1, 'two': lf}) ab = containers.AttributeBuilder(FakeFactory) self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False)) @@ -241,17 +180,7 @@ class AttributeBuilderTestCase(unittest.TestCase): def test_lazy_attribute(self): la = declarations.LazyAttribute(lambda a: a.one * 2) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1, 'two': la} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': 1, 'two': la}) ab = containers.AttributeBuilder(FakeFactory) self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False)) @@ -267,18 +196,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_using.py b/tests/test_using.py index 3ef5403..67db3bc 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -40,6 +40,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 -- cgit v1.2.3 From 03c40fd80707ad4837523a07cdf3f82564ab0259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:14:06 +0200 Subject: Add Traits (Closes #251). Based on a boolean flag, those will alter the definitions of the current factory, taking precedence over pre-defined behavior but overridden by callsite-level arguments. --- README.rst | 21 ++--- docs/introduction.rst | 55 ++++++++++++ docs/reference.rst | 230 ++++++++++++++++++++++++++++++++++++++---------- factory/__init__.py | 1 + factory/declarations.py | 16 ++++ tests/test_using.py | 114 ++++++++++++++++++++++++ 6 files changed, 382 insertions(+), 55 deletions(-) diff --git a/README.rst b/README.rst index a08d37f..0e1f000 100644 --- a/README.rst +++ b/README.rst @@ -292,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 """"""""""""""""""""" @@ -326,16 +337,6 @@ This will yield messages similar to those (artificial indentation): BaseFactory: Generating tests.test_using.TestModel2Factory(two=) -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/introduction.rst b/docs/introduction.rst index 9a16c39..5b535c9 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -266,6 +266,61 @@ This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute: +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) + 2012-03-03> + >>> RentalFactory(duration=10) + 2012-12-26> + + +When many fields should be updated based on a flag, use :class:`Traits ` 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() + + >>> OrderFactory(shipped=True) + + + Strategies ---------- diff --git a/docs/reference.rst b/docs/reference.rst index 8550f88..ad68faf 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -11,6 +11,9 @@ For internals and customization points, please refer to the :doc:`internals` sec The :class:`Factory` class -------------------------- +Meta options +"""""""""""" + .. class:: FactoryOptions .. versionadded:: 2.4.0 @@ -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 ` 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(duration='long') + + + +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(state='pending') + >>> OrderFactory(shipped=True) + + + +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) + + + +:class:`Traits ` 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) + + + + +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) + + + +.. note:: When overriding a :class:`Trait`, the whole declaration **MUST** be replaced. + + .. _strategies: Strategies @@ -1299,51 +1484,6 @@ with the :class:`Dict` and :class:`List` attributes: argument, if another type (tuple, set, ...) is required. -Parameters -"""""""""" - -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(duration='long') - - - -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). - - Post-generation hooks """"""""""""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index ccd71cd..ad9da80 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -52,6 +52,7 @@ from .declarations import ( Sequence, LazyAttributeSequence, SelfAttribute, + Trait, ContainerAttribute, SubFactory, Dict, diff --git a/factory/declarations.py b/factory/declarations.py index ad1f72f..895f2ac 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -469,6 +469,22 @@ class ComplexParameter(object): 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 # =============== diff --git a/tests/test_using.py b/tests/test_using.py index 67db3bc..eaeb8da 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 @@ -1114,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): -- cgit v1.2.3 From c22729d03d291814ae196ce7652954db9e42ed97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:48:10 +0200 Subject: Plan for 2.7.0 release. --- docs/changelog.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index dc3f967..b1fd314 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,21 @@ ChangeLog ========= +.. _v2.7.0: + +2.7.0 (2016-04-03) +------------------ + +*New:* + + - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambada parameters, + thanks to `Hervé Cauwelier `_. + - :issue:`251`: Add :ref:`parameterized factories ` and :class:`traits ` + +*Removed:* + + - :issue:`278`: Formally drop support for Python2.6 + .. _v2.6.1: 2.6.1 (2016-02-10) -- cgit v1.2.3 From 5836329889ac45034978c69b6a6f7de4b0b5b75d Mon Sep 17 00:00:00 2001 From: Samuel Paccoud Date: Sun, 3 Apr 2016 09:42:40 +0200 Subject: Add documentation and test for subfactory using "factory_parent" attribute Add documentation on how to use a LazyAttribute in a SubFactory and poke the "factory_parent" attribute to indirectly derive the value of a field on the child factory from a field on the parent factory. This commit adds an example to recipes that explains how it can be done. It also adds a test to make sure that this feature continues to work as is now described in the documentation. --- docs/recipes.rst | 14 ++++++++++++++ tests/test_using.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index a3df7be..fe18f50 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -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 ---------------------- diff --git a/tests/test_using.py b/tests/test_using.py index eaeb8da..0ce29e9 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1268,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): -- cgit v1.2.3 From b401c3d2eac2aa9b73815034b44af75f65e6d7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 6 Apr 2016 23:51:15 +0200 Subject: Refactor test setup/commands, enable tox. Closes #273. --- MANIFEST.in | 2 ++ Makefile | 42 ++++++++++++------------------------------ dev_requirements.txt | 13 ------------- requirements_dev.txt | 14 ++++++++++++++ requirements_test.txt | 2 ++ tox.ini | 35 +++++++++++++++++++++++++++++++++++ 6 files changed, 65 insertions(+), 43 deletions(-) delete mode 100644 dev_requirements.txt create mode 100644 requirements_dev.txt create mode 100644 requirements_test.txt create mode 100644 tox.ini diff --git a/MANIFEST.in b/MANIFEST.in index 3dfc1be..22f4a5c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include README.rst +include requirements*.txt + include docs/Makefile recursive-include docs *.py *.rst include docs/_static/.keep_dir diff --git a/Makefile b/Makefile index da8ac88..de0243f 100644 --- a/Makefile +++ b/Makefile @@ -6,55 +6,37 @@ 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: 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/dev_requirements.txt b/dev_requirements.txt deleted file mode 100644 index c78aa9d..0000000 --- a/dev_requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ --r requirements.txt --r examples/requirements.txt - -coverage -Django -Pillow -SQLAlchemy -mongoengine -mock -wheel - -Sphinx -sphinx_rtd_theme diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..a7706c6 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,14 @@ +-e . +-r requirements_test.txt +-r examples/requirements.txt + +coverage +Django +Pillow +SQLAlchemy +mongoengine +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/tox.ini b/tox.ini new file mode 100644 index 0000000..bbcf538 --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[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] +whitelist_externals = make +commands = make lint -- cgit v1.2.3 From 94a9b2cf9ddd7be181c984793a5404e2b95a779e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 6 Apr 2016 23:51:41 +0200 Subject: Cleanup manifest. Also, use check_manifest to check for MANIFEST.in / git mismatches. --- MANIFEST.in | 16 ++++++++++------ Makefile | 1 + tox.ini | 4 ++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 22f4a5c..19b0f8c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,12 @@ -include README.rst +include ChangeLog LICENSE README.rst include requirements*.txt -include docs/Makefile -recursive-include docs *.py *.rst -include docs/_static/.keep_dir -prune docs/_build -recursive-include tests *.py *.data +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 de0243f..8f48c6b 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ example-test: $(MAKE) -C $(EXAMPLES_DIR) test lint: + check-manifest pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ coverage: diff --git a/tox.ini b/tox.ini index bbcf538..4ccade6 100644 --- a/tox.ini +++ b/tox.ini @@ -31,5 +31,9 @@ whitelist_externals = make commands = make example-test [testenv:lint] +deps = + pylint + check_manifest + whitelist_externals = make commands = make lint -- cgit v1.2.3 From fbcf1285b9b756ef7b8d8cf7fcfe8ad245f79bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 7 Apr 2016 00:16:14 +0200 Subject: Fix a17b036: Error messages have moved. --- tests/test_containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_containers.py b/tests/test_containers.py index ea6ddff..1b87b2e 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -102,7 +102,7 @@ class LazyStubTestCase(unittest.TestCase): def evaluate(self, obj, container=None): try: return super(LazyAttrDefault, self).evaluate(obj, container) - except containers.CyclicDefinitionError: + except errors.CyclicDefinitionError: return self.defvalue stub = containers.LazyStub({ -- cgit v1.2.3 From d9098a809db170f8ffc52efe3e5c6e7f48aa2893 Mon Sep 17 00:00:00 2001 From: Rich Rauenzahn Date: Tue, 12 Apr 2016 14:57:28 -0700 Subject: Add custom error message when django_get_or_create is missing an input. --- factory/django.py | 7 +++++++ 1 file changed, 7 insertions(+) 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 -- cgit v1.2.3 From ae43bc4c116e6e740f62c6ce4b134e62e21fa682 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 19 Apr 2016 11:55:22 -0700 Subject: Fix typo --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index ad68faf..a060f75 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -111,7 +111,7 @@ Meta options .. 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 -- cgit v1.2.3 From f1ed74e06dfb6851bc691ebfd8135c875154ad50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 19 Apr 2016 22:54:36 +0200 Subject: Release version 2.7.0 --- docs/changelog.rst | 3 ++- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b1fd314..2341dfa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. _v2.7.0: -2.7.0 (2016-04-03) +2.7.0 (2016-04-19) ------------------ *New:* @@ -11,6 +11,7 @@ ChangeLog - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambada parameters, thanks to `Hervé Cauwelier `_. - :issue:`251`: Add :ref:`parameterized factories ` and :class:`traits ` + - :issue:`256`, :issue:`292`: Improve error messages in corner cases *Removed:* diff --git a/factory/__init__.py b/factory/__init__.py index ad9da80..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 ' -- cgit v1.2.3