From 595c47244b0bfce3023c14908c0cc6b6bb4e0aec Mon Sep 17 00:00:00 2001 From: anentropic Date: Mon, 3 Feb 2014 14:19:42 +0000 Subject: Make safe repr more safe --- factory/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/utils.py b/factory/utils.py index 276977a..7b48a1e 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -101,7 +101,7 @@ def import_object(module_name, attribute_name): def _safe_repr(obj): try: obj_repr = repr(obj) - except UnicodeError: + except: return '' % id(obj) try: # Convert to "text type" (= unicode) -- cgit v1.2.3 From e320cabc58bdb637f24d4b20df8c318a7420a55e Mon Sep 17 00:00:00 2001 From: anentropic Date: Thu, 13 Mar 2014 18:59:32 +0000 Subject: Update utils.py --- factory/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/utils.py b/factory/utils.py index 7b48a1e..6f0c763 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -101,7 +101,7 @@ def import_object(module_name, attribute_name): def _safe_repr(obj): try: obj_repr = repr(obj) - except: + except Exception: return '' % id(obj) try: # Convert to "text type" (= unicode) -- cgit v1.2.3 From 82988e154391cc37f81eb398bbfa91f30f524349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 21 Aug 2014 09:28:51 +0200 Subject: tests: Update to Django new 'duplicate file' mechanism. --- tests/test_django.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 41a26cf..fd9c876 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -430,7 +430,8 @@ class DjangoFileFieldTestCase(unittest.TestCase): o2 = WithFileFactory.build(afile=o1.afile) self.assertIsNone(o2.pk) self.assertEqual(b'example_data\n', o2.afile.read()) - self.assertEqual('django/example_1.data', o2.afile.name) + self.assertNotEqual('django/example.data', o2.afile.name) + self.assertRegexpMatches(o2.afile.name, r'django/example_\w+.data') def test_no_file(self): o = WithFileFactory.build(afile=None) @@ -547,7 +548,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): self.assertIsNone(o2.pk) # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o2.animage.read())) - self.assertEqual('django/example_1.jpeg', o2.animage.name) + self.assertNotEqual('django/example.jpeg', o2.animage.name) + self.assertRegexpMatches(o2.animage.name, r'django/example_\w+.jpeg') def test_no_file(self): o = WithImageFactory.build(animage=None) -- cgit v1.2.3 From 1a00eef263f787a9f3d98fbaa43ec30ad9ac4071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 22:53:02 +0200 Subject: Fix test running without django (Closes #161). --- tests/test_django.py | 101 +++++++++++++++++++++++---------------------------- 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index fd9c876..95e0256 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -57,21 +57,9 @@ if django is not None: from django.test import utils as django_test_utils from django.db.models import signals from .djapp import models -else: # pragma: no cover - django_test = unittest - - class Fake(object): - pass - models = Fake() - models.StandardModel = Fake - models.StandardSon = None - models.AbstractBase = Fake - models.ConcreteSon = Fake - models.NonIntegerPk = Fake - models.WithFile = Fake - models.WithImage = Fake - models.WithSignals = Fake +else: + django_test = unittest test_state = {} @@ -98,72 +86,73 @@ def tearDownModule(): django_test_utils.teardown_test_environment() -class StandardFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.StandardModel +if django is not None: + class StandardFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) -class StandardFactoryWithPKField(factory.django.DjangoModelFactory): - class Meta: - model = models.StandardModel - django_get_or_create = ('pk',) + class StandardFactoryWithPKField(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + django_get_or_create = ('pk',) - foo = factory.Sequence(lambda n: "foo%d" % n) - pk = None + foo = factory.Sequence(lambda n: "foo%d" % n) + pk = None -class NonIntegerPkFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.NonIntegerPk + class NonIntegerPkFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.NonIntegerPk - foo = factory.Sequence(lambda n: "foo%d" % n) - bar = '' + foo = factory.Sequence(lambda n: "foo%d" % n) + bar = '' -class AbstractBaseFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.AbstractBase - abstract = True + class AbstractBaseFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.AbstractBase + abstract = True - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) -class ConcreteSonFactory(AbstractBaseFactory): - class Meta: - model = models.ConcreteSon + class ConcreteSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteSon -class AbstractSonFactory(AbstractBaseFactory): - class Meta: - model = models.AbstractSon + class AbstractSonFactory(AbstractBaseFactory): + class Meta: + model = models.AbstractSon -class ConcreteGrandSonFactory(AbstractBaseFactory): - class Meta: - model = models.ConcreteGrandSon + class ConcreteGrandSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteGrandSon -class WithFileFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithFile + class WithFileFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithFile - if django is not None: - afile = factory.django.FileField() + if django is not None: + afile = factory.django.FileField() -class WithImageFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithImage + class WithImageFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithImage - if django is not None: - animage = factory.django.ImageField() + if django is not None: + animage = factory.django.ImageField() -class WithSignalsFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithSignals + class WithSignalsFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithSignals @unittest.skipIf(django is None, "Django not installed.") -- cgit v1.2.3 From 251c9ef081bdb9233b4885d23501afc3b26324c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 22:54:26 +0200 Subject: Fix typo in docs (Closes #157). --- docs/examples.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index ee521e3..e7f6057 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -114,9 +114,9 @@ We can now use our factories, for tests: def test_get_profile_stats(self): profiles = [] - profiles.extend(factories.ProfileFactory.batch_create(4)) - profiles.extend(factories.FemaleProfileFactory.batch_create(2)) - profiles.extend(factories.ProfileFactory.batch_create(2, planet="Tatooine")) + profiles.extend(factories.ProfileFactory.create_batch(4)) + profiles.extend(factories.FemaleProfileFactory.create_batch(2)) + profiles.extend(factories.ProfileFactory.create_batch(2, planet="Tatooine")) stats = business_logic.profile_stats(profiles) self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets) @@ -130,7 +130,7 @@ Or for fixtures: from . import factories def make_objects(): - factories.ProfileFactory.batch_create(size=50) + factories.ProfileFactory.create_batch(size=50) # Let's create a few, known objects. factories.ProfileFactory( -- cgit v1.2.3 From 5e4edb44daf54a4c34703319bdb9467ca7b3de8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 23:47:14 +0200 Subject: Django1.7 is out, let's not test it on Python2.6 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2bfb978..eceada3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ script: install: - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - pip install Django sqlalchemy --use-mirrors + - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install Django<1.7 --use-mirrors; else pip install Django; fi + - pip install sqlalchemy --use-mirrors - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi notifications: -- cgit v1.2.3 From 25bd44c30007d5babecefed651827431569ee1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 23:49:47 +0200 Subject: Fix support for Django 1.7. --- tests/djapp/settings.py | 1 + tests/test_django.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index c1b79b0..c051faf 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -41,5 +41,6 @@ INSTALLED_APPS = [ 'tests.djapp' ] +MIDDLEWARE_CLASSES = () SECRET_KEY = 'testing.' diff --git a/tests/test_django.py b/tests/test_django.py index 95e0256..874c272 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -61,6 +61,8 @@ if django is not None: else: django_test = unittest +if django is not None and django.VERSION >= (1, 7, 0): + django.setup() test_state = {} -- cgit v1.2.3 From f81aba229a7492b11a331407f60fba1bd4d6522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 23:50:50 +0200 Subject: Fix travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eceada3..3954bf5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ script: install: - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install Django<1.7 --use-mirrors; else pip install Django; fi + - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install "Django<1.7" --use-mirrors; else pip install Django; fi - pip install sqlalchemy --use-mirrors - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi -- cgit v1.2.3 From 70412551c545e94b27bef468cd248fce7a9cdd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 2 Nov 2014 16:32:16 +0100 Subject: Add tests for self-referential models (See #173). --- tests/cyclic/self_ref.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_using.py | 17 +++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/cyclic/self_ref.py diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py new file mode 100644 index 0000000..f18c989 --- /dev/null +++ b/tests/cyclic/self_ref.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2011-2013 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. + +"""Helper to test circular factory dependencies.""" + +import factory + +class TreeElement(object): + def __init__(self, name, parent): + self.parent = parent + self.name = name + + +class TreeElementFactory(factory.Factory): + class Meta: + model = TreeElement + + name = factory.Sequence(lambda n: "tree%s" % n) + parent = factory.SubFactory('tests.cyclic.self_ref.TreeElementFactory') diff --git a/tests/test_using.py b/tests/test_using.py index f18df4d..8d78789 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1938,6 +1938,23 @@ class CircularTestCase(unittest.TestCase): self.assertIsNone(b.foo.bar.foo.bar) +class SelfReferentialTests(unittest.TestCase): + def test_no_parent(self): + from .cyclic import self_ref + + obj = self_ref.TreeElementFactory(parent=None) + self.assertIsNone(obj.parent) + + def test_deep(self): + from .cyclic import self_ref + + obj = self_ref.TreeElementFactory(parent__parent__parent__parent=None) + self.assertIsNotNone(obj.parent) + self.assertIsNotNone(obj.parent.parent) + self.assertIsNotNone(obj.parent.parent.parent) + self.assertIsNone(obj.parent.parent.parent.parent) + + class DictTestCase(unittest.TestCase): def test_empty_dict(self): class TestObjectFactory(factory.Factory): -- cgit v1.2.3 From 827af8f13a1b768a75264874c73cc0e620177262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 16 Nov 2014 21:17:11 +0100 Subject: Add docs for manual sequence counter management --- docs/recipes.rst | 94 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index 72dacef..70eca46 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -327,3 +327,97 @@ default :meth:`Model.objects.create() ` manager = cls._get_manager(model_class) # The default would use ``manager.create(*args, **kwargs)`` return manager.create_user(*args, **kwargs) + + +Forcing the sequence counter +---------------------------- + +A common pattern with factory_boy is to use a :class:`factory.Sequence` declaration +to provide varying values to attributes declared as unique. + +However, it is sometimes useful to force a given value to the counter, for instance +to ensure that tests are properly reproductible. + +factory_boy provides a few hooks for this: + + +Forcing the value on a per-call basis + In order to force the counter for a specific :class:`~factory.Factory` instantiation, + just pass the value in the ``__sequence=42`` parameter: + + .. code-block:: python + + class AccountFactory(factory.Factory): + class Meta: + model = Account + uid = factory.Sequence(lambda n: n) + name = "Test" + + .. code-block:: pycon + + >>> obj1 = AccountFactory(name="John Doe", __sequence=10) + >>> obj1.uid # Taken from the __sequence counter + 10 + >>> obj2 = AccountFactory(name="Jane Doe") + >>> obj2.uid # The base sequence counter hasn't changed + 1 + + +Resetting the counter globally + If all calls for a factory must start from a deterministic number, + use :meth:`factory.Factory.reset_sequence`; this will reset the counter + to its initial value (as defined by :meth:`factory.Factory._setup_next_sequence`). + + .. code-block:: pycon + + >>> AccountFactory().uid + 1 + >>> AccountFactory().uid + 2 + >>> AccountFactory.reset_sequence() + >>> AccountFactory().uid # Reset to the initial value + 1 + >>> AccountFactory().uid + 2 + + It is also possible to reset the counter to a specific value: + + .. code-block:: pycon + + >>> AccountFactory.reset_sequence(10) + >>> AccountFactory().uid + 10 + >>> AccountFactory().uid + 11 + + This recipe is most useful in a :class:`~unittest.TestCase`'s + :meth:`~unittest.TestCase.setUp` method. + + +Forcing the initial value for all projects + The sequence counter of a :class:`~factory.Factory` can also be set + automatically upon the first call through the + :meth:`~factory.Factory._setup_next_sequence` method; this helps when the + objects's attributes mustn't conflict with pre-existing data. + + A typical example is to ensure that running a Python script twice will create + non-conflicting objects, by setting up the counter to "max used value plus one": + + .. code-block:: python + + class AccountFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.Account + + @classmethod + def _setup_next_sequence(cls): + try: + return models.Accounts.objects.latest('uid').uid + 1 + except models.Account.DoesNotExist: + return 1 + + .. code-block:: pycon + + >>> Account.objects.create(uid=42, name="Blah") + >>> AccountFactory.create() # Sets up the account number based on the latest uid + -- cgit v1.2.3 From 13d310fa14f4e4b9a559f8b7887f2a2492357013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 16 Nov 2014 22:34:29 +0100 Subject: Remove automagic pk-based sequence setup Related to issues #78, #92, #103, #111, #153, #170 The default value of all sequences is now 0; the automagic ``_setup_next_sequence`` behavior of Django/SQLAlchemy has been removed. This feature's only goal was to allow the following scenario: 1. Run a Python script that uses MyFactory.create() a couple of times (with a unique field based on the sequence counter) 2. Run the same Python script a second time Without the magical ``_setup_next_sequence``, the Sequence counter would be set to 0 at the beginning of each script run, so both runs would generate objects with the same values for the unique field ; thus conflicting and crashing. The above behavior having only a very limited use and bringing various issues (hitting the database on ``build()``, problems with non-integer or composite primary key columns, ...), it has been removed. It could still be emulated through custom ``_setup_next_sequence`` methods, or by calling ``MyFactory.reset_sequence()``. --- docs/changelog.rst | 13 ++++++++++++- docs/orms.rst | 2 -- factory/alchemy.py | 12 ------------ factory/django.py | 15 --------------- tests/test_alchemy.py | 22 +++++++++++----------- tests/test_django.py | 40 ++++++++++++++++++++-------------------- tests/test_using.py | 20 +++++++------------- 7 files changed, 50 insertions(+), 74 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7d77f7f..018ec60 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,17 @@ ChangeLog ========= +.. _v2.5.0: + +2.5.0 (master) +-------------- + +*Deprecation:* + + - Remove deprecated features from :ref:`v2.4.0` + - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy; + this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. + .. _v2.4.1: 2.4.1 (2014-06-23) @@ -19,7 +30,7 @@ ChangeLog *New:* - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) - - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) + - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`) - Declare target model and other non-declaration fields in a ``class Meta`` section. diff --git a/docs/orms.rst b/docs/orms.rst index 2aa27b2..88d49e9 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -35,7 +35,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the * The :attr:`~factory.FactoryOptions.model` attribute also supports the ``'app.Model'`` syntax * :func:`~factory.Factory.create()` uses :meth:`Model.objects.create() ` - * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value * When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration` attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. @@ -284,7 +283,6 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: This class provides the following features: * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add` - * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value .. attribute:: FACTORY_SESSION diff --git a/factory/alchemy.py b/factory/alchemy.py index 3c91411..2cd28bb 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -43,18 +43,6 @@ class SQLAlchemyModelFactory(base.Factory): 'FACTORY_SESSION': 'sqlalchemy_session', }) - @classmethod - def _setup_next_sequence(cls, *args, **kwargs): - """Compute the next available PK, based on the 'pk' database field.""" - session = cls._meta.sqlalchemy_session - model = cls._meta.model - pk = getattr(model, model.__mapper__.primary_key[0].name) - max_pk = session.query(max(pk)).one()[0] - if isinstance(max_pk, int): - return max_pk + 1 if max_pk else 1 - else: - return 1 - @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" diff --git a/factory/django.py b/factory/django.py index 2b6c463..c58a6e2 100644 --- a/factory/django.py +++ b/factory/django.py @@ -109,21 +109,6 @@ class DjangoModelFactory(base.Factory): except AttributeError: return model_class.objects - @classmethod - def _setup_next_sequence(cls): - """Compute the next available PK, based on the 'pk' database field.""" - - model = cls._get_model_class() # pylint: disable=E1101 - manager = cls._get_manager(model) - - try: - return 1 + manager.values_list('pk', flat=True - ).order_by('-pk')[0] - except (IndexError, TypeError): - # IndexError: No instance exist yet - # TypeError: pk isn't an integer type - return 1 - @classmethod def _get_or_create(cls, model_class, *args, **kwargs): """Create an instance of the model through objects.get_or_create.""" diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index b9222eb..2deb418 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -88,18 +88,18 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase): StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo2', std2.foo) - self.assertEqual(2, std2.id) + self.assertEqual('foo0', std2.foo) + self.assertEqual(0, std2.id) def test_pk_force_value(self): std1 = StandardFactory.create(id=10) - self.assertEqual('foo1', std1.foo) # sequence was set before pk + self.assertEqual('foo1', std1.foo) # sequence and pk are unrelated self.assertEqual(10, std1.id) StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo11', std2.foo) - self.assertEqual(11, std2.id) + self.assertEqual('foo0', std2.foo) # Sequence doesn't care about pk + self.assertEqual(0, std2.id) @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") @@ -111,22 +111,22 @@ class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def test_first(self): nonint = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint.id) + self.assertEqual('foo0', nonint.id) def test_many(self): nonint1 = NonIntegerPkFactory.build() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint1.id) - self.assertEqual('foo2', nonint2.id) + self.assertEqual('foo0', nonint1.id) + self.assertEqual('foo1', nonint2.id) def test_creation(self): nonint1 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint1.id) + self.assertEqual('foo0', nonint1.id) NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint2.id) + self.assertEqual('foo0', nonint2.id) def test_force_pk(self): nonint1 = NonIntegerPkFactory.create(id='foo10') @@ -134,4 +134,4 @@ class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint2.id) + self.assertEqual('foo0', nonint2.id) diff --git a/tests/test_django.py b/tests/test_django.py index 874c272..0cbef19 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -174,32 +174,32 @@ class DjangoPkSequenceTestCase(django_test.TestCase): def test_pk_first(self): std = StandardFactory.build() - self.assertEqual('foo1', std.foo) + self.assertEqual('foo0', std.foo) def test_pk_many(self): std1 = StandardFactory.build() std2 = StandardFactory.build() - self.assertEqual('foo1', std1.foo) - self.assertEqual('foo2', std2.foo) + self.assertEqual('foo0', std1.foo) + self.assertEqual('foo1', std2.foo) def test_pk_creation(self): std1 = StandardFactory.create() - self.assertEqual('foo1', std1.foo) + self.assertEqual('foo0', std1.foo) self.assertEqual(1, std1.pk) StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo2', std2.foo) + self.assertEqual('foo0', std2.foo) self.assertEqual(2, std2.pk) def test_pk_force_value(self): std1 = StandardFactory.create(pk=10) - self.assertEqual('foo1', std1.foo) # sequence was set before pk + self.assertEqual('foo0', std1.foo) # sequence is unrelated to pk self.assertEqual(10, std1.pk) StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo11', std2.foo) + self.assertEqual('foo0', std2.foo) self.assertEqual(11, std2.pk) @@ -212,12 +212,12 @@ class DjangoPkForceTestCase(django_test.TestCase): def test_no_pk(self): std = StandardFactoryWithPKField() self.assertIsNotNone(std.pk) - self.assertEqual('foo1', std.foo) + self.assertEqual('foo0', std.foo) def test_force_pk(self): std = StandardFactoryWithPKField(pk=42) self.assertIsNotNone(std.pk) - self.assertEqual('foo1', std.foo) + self.assertEqual('foo0', std.foo) def test_reuse_pk(self): std1 = StandardFactoryWithPKField(foo='bar') @@ -286,9 +286,9 @@ class DjangoModelLoadingTestCase(django_test.TestCase): self.assertEqual(models.StandardModel, e1.__class__) self.assertEqual(models.StandardSon, e2.__class__) self.assertEqual(models.StandardModel, e3.__class__) - self.assertEqual(1, e1.foo) - self.assertEqual(2, e2.foo) - self.assertEqual(3, e3.foo) + self.assertEqual(0, e1.foo) + self.assertEqual(1, e2.foo) + self.assertEqual(2, e3.foo) @unittest.skipIf(django is None, "Django not installed.") @@ -299,23 +299,23 @@ class DjangoNonIntegerPkTestCase(django_test.TestCase): def test_first(self): nonint = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint.foo) + self.assertEqual('foo0', nonint.foo) def test_many(self): nonint1 = NonIntegerPkFactory.build() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint1.foo) - self.assertEqual('foo2', nonint2.foo) + self.assertEqual('foo0', nonint1.foo) + self.assertEqual('foo1', nonint2.foo) def test_creation(self): nonint1 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint1.foo) - self.assertEqual('foo1', nonint1.pk) + self.assertEqual('foo0', nonint1.foo) + self.assertEqual('foo0', nonint1.pk) NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint2.foo) + self.assertEqual('foo0', nonint2.foo) def test_force_pk(self): nonint1 = NonIntegerPkFactory.create(pk='foo10') @@ -324,8 +324,8 @@ class DjangoNonIntegerPkTestCase(django_test.TestCase): NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint2.foo) - self.assertEqual('foo1', nonint2.pk) + self.assertEqual('foo0', nonint2.foo) + self.assertEqual('foo0', nonint2.pk) @unittest.skipIf(django is None, "Django not installed.") diff --git a/tests/test_using.py b/tests/test_using.py index 8d78789..8aba8b6 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1490,12 +1490,6 @@ class BetterFakeModelManager(object): instance.id = 2 return instance, True - def values_list(self, *args, **kwargs): - return self - - def order_by(self, *args, **kwargs): - return [1] - class BetterFakeModel(object): @classmethod @@ -1618,14 +1612,14 @@ class DjangoModelFactoryTestCase(unittest.TestCase): o1 = TestModelFactory() o2 = TestModelFactory() - self.assertEqual('foo_2', o1.a) - self.assertEqual('foo_3', o2.a) + self.assertEqual('foo_0', o1.a) + self.assertEqual('foo_1', o2.a) o3 = TestModelFactory.build() o4 = TestModelFactory.build() - self.assertEqual('foo_4', o3.a) - self.assertEqual('foo_5', o4.a) + self.assertEqual('foo_2', o3.a) + self.assertEqual('foo_3', o4.a) def test_no_get_or_create(self): class TestModelFactory(factory.django.DjangoModelFactory): @@ -1636,7 +1630,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): o = TestModelFactory() self.assertEqual(None, o._defaults) - self.assertEqual('foo_2', o.a) + self.assertEqual('foo_0', o.a) self.assertEqual(2, o.id) def test_get_or_create(self): @@ -1652,7 +1646,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): o = TestModelFactory() self.assertEqual({'c': 3, 'd': 4}, o._defaults) - self.assertEqual('foo_2', o.a) + self.assertEqual('foo_0', o.a) self.assertEqual(2, o.b) self.assertEqual(3, o.c) self.assertEqual(4, o.d) @@ -1672,7 +1666,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): o = TestModelFactory() self.assertEqual({}, o._defaults) - self.assertEqual('foo_2', o.a) + self.assertEqual('foo_0', o.a) self.assertEqual(2, o.b) self.assertEqual(3, o.c) self.assertEqual(4, o.d) -- cgit v1.2.3 From 336ea5ac8b2d922fb54f99edd55d4773dd126934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 18 Nov 2014 00:35:19 +0100 Subject: Remove deprecated features. This disables the ``FACTORY_FOR`` syntax and related parameters, that should be declared through ``class Meta``. --- docs/orms.rst | 9 --------- docs/reference.rst | 29 --------------------------- factory/alchemy.py | 5 ----- factory/base.py | 26 ------------------------ factory/declarations.py | 9 --------- factory/django.py | 5 ----- factory/helpers.py | 1 - tests/__init__.py | 1 - tests/test_declarations.py | 15 -------------- tests/test_deprecation.py | 49 ---------------------------------------------- tests/test_using.py | 2 +- 11 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 tests/test_deprecation.py diff --git a/docs/orms.rst b/docs/orms.rst index 88d49e9..e32eafa 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -39,11 +39,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. - .. attribute:: FACTORY_DJANGO_GET_OR_CREATE - - .. deprecated:: 2.4.0 - See :attr:`DjangoOptions.django_get_or_create`. - .. class:: DjangoOptions(factory.base.FactoryOptions) @@ -284,10 +279,6 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add` - .. attribute:: FACTORY_SESSION - - .. deprecated:: 2.4.0 - See :attr:`~SQLAlchemyOptions.sqlalchemy_session`. .. class:: SQLAlchemyOptions(factory.base.FactoryOptions) diff --git a/docs/reference.rst b/docs/reference.rst index b0dda50..5eea62c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -115,35 +115,6 @@ The :class:`Factory` class .. class:: Factory - .. note:: In previous versions, the fields of :class:`class Meta ` were - defined as class attributes on :class:`Factory`. This is now deprecated and will be removed - in 2.5.0. - - .. attribute:: FACTORY_FOR - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.model`. - - .. attribute:: ABSTRACT_FACTORY - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.abstract`. - - .. attribute:: FACTORY_ARG_PARAMETERS - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.inline_args`. - - .. attribute:: FACTORY_HIDDEN_ARGS - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.exclude`. - - .. attribute:: FACTORY_STRATEGY - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.strategy`. - **Class-level attributes:** diff --git a/factory/alchemy.py b/factory/alchemy.py index 2cd28bb..6408393 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -38,11 +38,6 @@ class SQLAlchemyModelFactory(base.Factory): class Meta: abstract = True - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() - _OLDSTYLE_ATTRIBUTES.update({ - 'FACTORY_SESSION': 'sqlalchemy_session', - }) - @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" diff --git a/factory/base.py b/factory/base.py index 9e07899..efff976 100644 --- a/factory/base.py +++ b/factory/base.py @@ -21,7 +21,6 @@ # THE SOFTWARE. import logging -import warnings from . import containers from . import declarations @@ -109,23 +108,6 @@ class FactoryMetaClass(type): attrs_meta = attrs.pop('Meta', None) - oldstyle_attrs = {} - converted_attrs = {} - for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items(): - if old_name in attrs: - oldstyle_attrs[old_name] = new_name - converted_attrs[new_name] = attrs.pop(old_name) - if oldstyle_attrs: - warnings.warn( - "Declaring any of %s at class-level is deprecated" - " and will be removed in the future. Please set them" - " as %s attributes of a 'class Meta' attribute." % ( - ', '.join(oldstyle_attrs.keys()), - ', '.join(oldstyle_attrs.values()), - ), - PendingDeprecationWarning, 2) - attrs_meta = type('Meta', (object,), converted_attrs) - base_meta = resolve_attribute('_meta', bases) options_class = resolve_attribute('_options_class', bases, FactoryOptions) @@ -322,14 +304,6 @@ class BaseFactory(object): _meta = FactoryOptions() - _OLDSTYLE_ATTRIBUTES = { - 'FACTORY_FOR': 'model', - 'ABSTRACT_FACTORY': 'abstract', - 'FACTORY_STRATEGY': 'strategy', - 'FACTORY_ARG_PARAMETERS': 'inline_args', - 'FACTORY_HIDDEN_ARGS': 'exclude', - } - # ID to use for the next 'declarations.Sequence' attribute. _counter = None diff --git a/factory/declarations.py b/factory/declarations.py index 5e7e734..f6f5846 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -22,7 +22,6 @@ import itertools -import warnings import logging from . import compat @@ -502,14 +501,6 @@ class RelatedFactory(PostGenerationDeclaration): def __init__(self, factory, factory_related_name='', **defaults): super(RelatedFactory, self).__init__() - if factory_related_name == '' and defaults.get('name') is not None: - warnings.warn( - "Usage of RelatedFactory(SomeFactory, name='foo') is deprecated" - " and will be removed in the future. Please use the" - " RelatedFactory(SomeFactory, 'foo') or" - " RelatedFactory(SomeFactory, factory_related_name='foo')" - " syntax instead", PendingDeprecationWarning, 2) - factory_related_name = defaults.pop('name') self.name = factory_related_name self.defaults = defaults diff --git a/factory/django.py b/factory/django.py index c58a6e2..7050366 100644 --- a/factory/django.py +++ b/factory/django.py @@ -84,11 +84,6 @@ class DjangoModelFactory(base.Factory): class Meta: abstract = True # Optional, but explicit. - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() - _OLDSTYLE_ATTRIBUTES.update({ - 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create', - }) - @classmethod def _load_model_class(cls, definition): diff --git a/factory/helpers.py b/factory/helpers.py index 19431df..b9cef6e 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -28,7 +28,6 @@ import logging from . import base from . import declarations -from . import django @contextlib.contextmanager diff --git a/tests/__init__.py b/tests/__init__.py index 855beea..5b6fc55 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,7 +4,6 @@ from .test_base import * from .test_containers import * from .test_declarations import * -from .test_deprecation import * from .test_django import * from .test_fuzzy import * from .test_helpers import * diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 86bc8b5..18a4cd4 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -22,7 +22,6 @@ import datetime import itertools -import warnings from factory import declarations from factory import helpers @@ -207,20 +206,6 @@ class FactoryWrapperTestCase(unittest.TestCase): datetime.date = orig_date -class RelatedFactoryTestCase(unittest.TestCase): - - def test_deprecate_name(self): - with warnings.catch_warnings(record=True) as w: - - warnings.simplefilter('always') - f = declarations.RelatedFactory('datetime.date', name='blah') - - self.assertEqual('blah', f.name) - self.assertEqual(1, len(w)) - self.assertIn('RelatedFactory', str(w[0].message)) - self.assertIn('factory_related_name', str(w[0].message)) - - class PostGenerationMethodCallTestCase(unittest.TestCase): def setUp(self): self.obj = mock.MagicMock() diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py deleted file mode 100644 index a07cbf3..0000000 --- a/tests/test_deprecation.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 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. - -"""Tests for deprecated features.""" - -import warnings - -import factory - -from .compat import mock, unittest -from . import tools - - -class DeprecationTests(unittest.TestCase): - def test_factory_for(self): - class Foo(object): - pass - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - class FooFactory(factory.Factory): - FACTORY_FOR = Foo - - self.assertEqual(1, len(w)) - warning = w[0] - # Message is indeed related to the current file - # This is to ensure error messages are readable by end users. - self.assertIn(warning.filename, __file__) - self.assertIn('FACTORY_FOR', str(warning.message)) - self.assertIn('model', str(warning.message)) diff --git a/tests/test_using.py b/tests/test_using.py index 8aba8b6..7318f2e 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1798,7 +1798,7 @@ class PostGenerationTestCase(unittest.TestCase): model = TestObject one = 3 two = 2 - three = factory.RelatedFactory(TestRelatedObjectFactory, name='obj') + three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') obj = TestObjectFactory.build() # Normal fields -- cgit v1.2.3 From 7b31d60af0ab0678d04d7f50abc28ba7c4ccfcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 18 Nov 2014 00:45:07 +0100 Subject: Fix typo in docs --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 5eea62c..2516936 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -369,7 +369,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. when using the ``create`` strategy. That policy will be used if the - :attr:`associated class ` has an ``objects`` attribute *and* the :meth:`~Factory._create` classmethod of the :class:`Factory` wasn't overridden. -- cgit v1.2.3 From 392db861e585f12038f18f41e467ecfcab9d39b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 25 Nov 2014 23:46:28 +0100 Subject: Fix reference docs (Closes #166, #167). Use ``obj`` for ``@post_generation``-decorated methods, instead of ``self``: this makes it clearer that the ``obj`` is an instance of the model, and not of the ``Factory``. Thanks to @jamescooke & @NiklasMM for spotting the typo. --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 2516936..43433e0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1192,7 +1192,7 @@ For instance, a :class:`PostGeneration` hook is declared as ``post``: model = SomeObject @post_generation - def post(self, create, extracted, **kwargs): + def post(obj, create, extracted, **kwargs): obj.set_origin(create) .. OHAI_VIM** -- cgit v1.2.3 From f83c602874698427bdc141accd8fc14a9749d6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 6 Feb 2015 23:19:06 +0100 Subject: docs: Add explanations about SQLAlchemy's scoped_session. --- docs/conf.py | 4 +-- docs/orms.rst | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4f76d45..ce6730b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -247,7 +247,7 @@ intersphinx_mapping = { 'http://docs.djangoproject.com/en/dev/_objects/', ), 'sqlalchemy': ( - 'http://docs.sqlalchemy.org/en/rel_0_8/', - 'http://docs.sqlalchemy.org/en/rel_0_8/objects.inv', + 'http://docs.sqlalchemy.org/en/rel_0_9/', + 'http://docs.sqlalchemy.org/en/rel_0_9/objects.inv', ), } diff --git a/docs/orms.rst b/docs/orms.rst index e32eafa..ab813a2 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -298,9 +298,8 @@ A (very) simple exemple: from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker - session = scoped_session(sessionmaker()) engine = create_engine('sqlite://') - session.configure(bind=engine) + session = scoped_session(sessionmaker(bind=engine)) Base = declarative_base() @@ -330,3 +329,102 @@ A (very) simple exemple: >>> session.query(User).all() [] + + +Managing sessions +""""""""""""""""" + +Since `SQLAlchemy`_ is a general purpose library, +there is no "global" session management system. + +The most common pattern when working with unit tests and ``factory_boy`` +is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: + +* The test runner configures some project-wide :class:`~sqlalchemy.orm.scoping.scoped_session` +* Each :class:`~SQLAlchemyModelFactory` subclass uses this + :class:`~sqlalchemy.orm.scoping.scoped_session` as its :attr:`~SQLAlchemyOptions.sqlalchemy_session` +* The :meth:`~unittest.TestCase.tearDown` method of tests calls + :meth:`Session.remove ` + to reset the session. + + +Here is an example layout: + +- A global (test-only?) file holds the :class:`~sqlalchemy.orm.scoping.scoped_session`: + +.. code-block:: python + + # myprojet/test/common.py + + from sqlalchemy import orm + Session = orm.scoped_session(orm.sessionmaker()) + + +- All factory access it: + +.. code-block:: python + + # myproject/factories.py + + import factory + import factory.alchemy + + from . import models + from .test import common + + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = models.User + + # Use the not-so-global scoped_session + # Warning: DO NOT USE common.Session()! + sqlalchemy_session = common.Session + + name = factory.Sequence(lambda n: "User %d" % n) + + +- The test runner configures the :class:`~sqlalchemy.orm.scoping.scoped_session` when it starts: + +.. code-block:: python + + # myproject/test/runtests.py + + import sqlalchemy + + from . import common + + def runtests(): + engine = sqlalchemy.create_engine('sqlite://') + + # It's a scoped_session, we can configure it later + common.Session.configure(engine=engine) + + run_the_tests + + +- :class:`test cases ` use this ``scoped_session``, + and clear it after each test: + +.. code-block:: python + + # myproject/test/test_stuff.py + + import unittest + + from . import common + + class MyTest(unittest.TestCase): + + def setUp(self): + # Prepare a new, clean session + self.session = common.Session() + + def test_something(self): + u = factories.UserFactory() + self.assertEqual([u], self.session.query(User).all()) + + def tearDown(self): + # Rollback the session => no changes to the database + self.session.rollback() + # Remove it, so that the next test gets a new Session() + common.Session.remove() -- cgit v1.2.3 From d95bc982cd8480aa44e5282ab1284a9278049066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 6 Feb 2015 23:29:52 +0100 Subject: docs: Improve explanation of SQLAlchemy's scoped_session. --- docs/orms.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index ab813a2..9e4d106 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -347,6 +347,16 @@ is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: :meth:`Session.remove ` to reset the session. +.. note:: See the excellent :ref:`SQLAlchemy guide on scoped_session ` + for details of :class:`~sqlalchemy.orm.scoping.scoped_session`'s usage. + + The basic idea is that declarative parts of the code (including factories) + need a simple way to access the "current session", + but that session will only be created and configured at a later point. + + The :class:`~sqlalchemy.orm.scoping.scoped_session` handles this, + by virtue of only creating the session when a query is sent to the database. + Here is an example layout: @@ -396,14 +406,14 @@ Here is an example layout: def runtests(): engine = sqlalchemy.create_engine('sqlite://') - # It's a scoped_session, we can configure it later - common.Session.configure(engine=engine) + # It's a scoped_session, and now is the time to configure it. + common.Session.configure(bind=engine) run_the_tests - :class:`test cases ` use this ``scoped_session``, - and clear it after each test: + and clear it after each test (for isolation): .. code-block:: python -- cgit v1.2.3 From 97a88905b7f0f513bd480fe630e43798aba22c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 18 Feb 2015 22:00:01 +0100 Subject: Enable resetting factory.fuzzy's random generator (Closes #175, #185). Users may now call ``factory.fuzzy.get_random_state()`` to retrieve the current random generator's state (isolated from the one used in Python's ``random``). That state can then be reinjected with ``factory.fuzzy.set_random_state(state)``. --- docs/changelog.rst | 4 ++++ docs/fuzzy.rst | 30 ++++++++++++++++++++++++++++++ factory/fuzzy.py | 35 +++++++++++++++++++++++++++-------- tests/test_fuzzy.py | 50 ++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 018ec60..ebe9930 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,10 @@ ChangeLog 2.5.0 (master) -------------- +*New:* + + - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). + *Deprecation:* - Remove deprecated features from :ref:`v2.4.0` diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 1480419..0658652 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -338,3 +338,33 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its The method responsible for generating random values. *Must* be overridden in subclasses. + + +Managing randomness +------------------- + +Using :mod:`random` in factories allows to "fuzz" a program efficiently. +However, it's sometimes required to *reproduce* a failing test. + +:mod:`factory.fuzzy` uses a separate instance of :class:`random.Random`, +and provides a few helpers for this: + +.. method:: get_random_state() + + Call :meth:`get_random_state` to retrieve the random generator's current + state. + +.. method:: set_random_state(state) + + Use :meth:`set_random_state` to set a custom state into the random generator + (fetched from :meth:`get_random_state` in a previous run, for instance) + +.. method:: reseed_random(seed) + + The :meth:`reseed_random` function allows to load a chosen seed into the random generator. + + +Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD** +use :obj:`factory.fuzzy._random` foras a randomness source; this ensures that +data they generate can be regenerated using the simple state from +:meth:`get_random_state`. diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 94599b7..0137ba9 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -34,6 +34,25 @@ from . import compat from . import declarations +_random = random.Random() + + +def get_random_state(): + """Retrieve the state of factory.fuzzy's random generator.""" + return _random.getstate() + + +def set_random_state(state): + """Force-set the state of factory.fuzzy's random generator.""" + return _random.setstate(state) + + +def reseed_random(seed): + """Reseed factory.fuzzy's random generator.""" + r = random.Random(seed) + set_random_state(r.getstate()) + + class BaseFuzzyAttribute(declarations.OrderedDeclaration): """Base class for fuzzy attributes. @@ -81,7 +100,7 @@ class FuzzyText(BaseFuzzyAttribute): """ def __init__(self, prefix='', length=12, suffix='', - chars=string.ascii_letters, **kwargs): + chars=string.ascii_letters, **kwargs): super(FuzzyText, self).__init__(**kwargs) self.prefix = prefix self.suffix = suffix @@ -89,7 +108,7 @@ class FuzzyText(BaseFuzzyAttribute): self.chars = tuple(chars) # Unroll iterators def fuzz(self): - chars = [random.choice(self.chars) for _i in range(self.length)] + chars = [_random.choice(self.chars) for _i in range(self.length)] return self.prefix + ''.join(chars) + self.suffix @@ -101,7 +120,7 @@ class FuzzyChoice(BaseFuzzyAttribute): super(FuzzyChoice, self).__init__(**kwargs) def fuzz(self): - return random.choice(self.choices) + return _random.choice(self.choices) class FuzzyInteger(BaseFuzzyAttribute): @@ -119,7 +138,7 @@ class FuzzyInteger(BaseFuzzyAttribute): super(FuzzyInteger, self).__init__(**kwargs) def fuzz(self): - return random.randrange(self.low, self.high + 1, self.step) + return _random.randrange(self.low, self.high + 1, self.step) class FuzzyDecimal(BaseFuzzyAttribute): @@ -137,7 +156,7 @@ class FuzzyDecimal(BaseFuzzyAttribute): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - base = compat.float_to_decimal(random.uniform(self.low, self.high)) + base = compat.float_to_decimal(_random.uniform(self.low, self.high)) return base.quantize(decimal.Decimal(10) ** -self.precision) @@ -155,7 +174,7 @@ class FuzzyFloat(BaseFuzzyAttribute): super(FuzzyFloat, self).__init__(**kwargs) def fuzz(self): - return random.uniform(self.low, self.high) + return _random.uniform(self.low, self.high) class FuzzyDate(BaseFuzzyAttribute): @@ -175,7 +194,7 @@ class FuzzyDate(BaseFuzzyAttribute): self.end_date = end_date.toordinal() def fuzz(self): - return datetime.date.fromordinal(random.randint(self.start_date, self.end_date)) + return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date)) class BaseFuzzyDateTime(BaseFuzzyAttribute): @@ -215,7 +234,7 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute): delta = self.end_dt - self.start_dt microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400)) - offset = random.randint(0, microseconds) + offset = _random.randint(0, microseconds) result = self.start_dt + datetime.timedelta(microseconds=offset) if self.force_year is not None: diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 1caeb0a..fd32705 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -55,7 +55,7 @@ class FuzzyChoiceTestCase(unittest.TestCase): d = fuzzy.FuzzyChoice(options) - with mock.patch('random.choice', fake_choice): + with mock.patch('factory.fuzzy._random.choice', fake_choice): res = d.evaluate(2, None, False) self.assertEqual(6, res) @@ -93,7 +93,7 @@ class FuzzyIntegerTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyInteger(2, 8) - with mock.patch('random.randrange', fake_randrange): + with mock.patch('factory.fuzzy._random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((2 + 8 + 1) * 1, res) @@ -103,7 +103,7 @@ class FuzzyIntegerTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyInteger(8) - with mock.patch('random.randrange', fake_randrange): + with mock.patch('factory.fuzzy._random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((0 + 8 + 1) * 1, res) @@ -113,7 +113,7 @@ class FuzzyIntegerTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyInteger(5, 8, 3) - with mock.patch('random.randrange', fake_randrange): + with mock.patch('factory.fuzzy._random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((5 + 8 + 1) * 3, res) @@ -146,7 +146,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyDecimal(2.0, 8.0) - with mock.patch('random.uniform', fake_uniform): + with mock.patch('factory.fuzzy._random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('10.0'), res) @@ -156,7 +156,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyDecimal(8.0) - with mock.patch('random.uniform', fake_uniform): + with mock.patch('factory.fuzzy._random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('8.0'), res) @@ -166,7 +166,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyDecimal(8.0, precision=3) - with mock.patch('random.uniform', fake_uniform): + with mock.patch('factory.fuzzy._random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) @@ -214,7 +214,7 @@ class FuzzyDateTestCase(unittest.TestCase): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31) - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.date(2013, 1, 16), res) @@ -225,7 +225,7 @@ class FuzzyDateTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyDate(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.date(2013, 1, 2), res) @@ -332,7 +332,7 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31) - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 16), res) @@ -343,7 +343,7 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2), res) @@ -450,7 +450,7 @@ class FuzzyDateTimeTestCase(unittest.TestCase): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31) - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res) @@ -461,7 +461,7 @@ class FuzzyDateTimeTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyDateTime(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res) @@ -486,7 +486,7 @@ class FuzzyTextTestCase(unittest.TestCase): chars = ['a', 'b', 'c'] fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4) - with mock.patch('random.choice', fake_choice): + with mock.patch('factory.fuzzy._random.choice', fake_choice): res = fuzz.evaluate(2, None, False) self.assertEqual('preaaaapost', res) @@ -504,3 +504,25 @@ class FuzzyTextTestCase(unittest.TestCase): for char in res: self.assertIn(char, ['a', 'b', 'c']) + + +class FuzzyRandomTestCase(unittest.TestCase): + def test_seeding(self): + fuzz = fuzzy.FuzzyInteger(1, 1000) + + fuzzy.reseed_random(42) + value = fuzz.evaluate(sequence=1, obj=None, create=False) + + fuzzy.reseed_random(42) + value2 = fuzz.evaluate(sequence=1, obj=None, create=False) + self.assertEqual(value, value2) + + def test_reset_state(self): + fuzz = fuzzy.FuzzyInteger(1, 1000) + + state = fuzzy.get_random_state() + value = fuzz.evaluate(sequence=1, obj=None, create=False) + + fuzzy.set_random_state(state) + value2 = fuzz.evaluate(sequence=1, obj=None, create=False) + self.assertEqual(value, value2) -- cgit v1.2.3 From 93e37c2016b72e7ee66b02bfae329753ccfbe322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 18 Feb 2015 22:05:17 +0100 Subject: Remove sphinx markup from README.rst (Closes #180). That file should be readable by PyPI's RST parser, which doesn't support Sphinx constructs. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 32b93bd..cb38a9a 100644 --- a/README.rst +++ b/README.rst @@ -250,7 +250,7 @@ Debugging factory_boy Debugging factory_boy can be rather complex due to the long chains of calls. Detailed logging is available through the ``factory`` logger. -A helper, :meth:`factory.debug()`, is available to ease debugging: +A helper, `factory.debug()`, is available to ease debugging: .. code-block:: python -- cgit v1.2.3 From efa9d3c0d165a4c49def26b423711ed28eb2d264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:45:45 +0100 Subject: Fix typos in docs (Closes #159, closes #178, closes #188). --- README.rst | 2 +- docs/orms.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cb38a9a..c787ca8 100644 --- a/README.rst +++ b/README.rst @@ -159,7 +159,7 @@ It is also possible to create a bunch of objects in a single call: .. code-block:: pycon - >>> users = UserFactory.build(10, first_name="Joe") + >>> users = UserFactory.build_batch(10, first_name="Joe") >>> len(users) 10 >>> [user.first_name for user in users] diff --git a/docs/orms.rst b/docs/orms.rst index 9e4d106..a0afc40 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -290,7 +290,7 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. -A (very) simple exemple: +A (very) simple example: .. code-block:: python -- cgit v1.2.3 From b6f6ae48c796d722f2a0209963d525b2f8b8fe0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:47:42 +0100 Subject: Fix bad default value for Factory.declarations (Closes #162). --- factory/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/base.py b/factory/base.py index efff976..7215f00 100644 --- a/factory/base.py +++ b/factory/base.py @@ -409,7 +409,7 @@ class BaseFactory(object): retrieved DeclarationDict. """ decls = cls._meta.declarations.copy() - decls.update(extra_defs) + decls.update(extra_defs or {}) return decls @classmethod -- cgit v1.2.3 From c666411153ea9840b492f7abecf0cfa51e21dc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:48:43 +0100 Subject: Docs: fix default strategy (Closes #158). --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 43433e0..9fd2576 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -109,7 +109,7 @@ The :class:`Factory` class .. attribute:: strategy Use this attribute to change the strategy used by a :class:`Factory`. - The default is :data:`BUILD_STRATEGY`. + The default is :data:`CREATE_STRATEGY`. -- cgit v1.2.3 From 6b9a2b5d9aaa1f4fb06819240a7b243fcfd79943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:50:49 +0100 Subject: Logs: Allow non-integer sequences (Closes #148). As pointed by @glinmac. --- factory/declarations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index f6f5846..b3833ee 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -194,7 +194,7 @@ class Sequence(OrderedDeclaration): self.type = type def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("Sequence: Computing next value of %r for seq=%d", self.function, sequence) + logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, sequence) return self.function(self.type(sequence)) @@ -208,7 +208,7 @@ class LazyAttributeSequence(Sequence): of counter for the 'function' attribute. """ def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%d, obj=%r", + logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%r", self.function, sequence, obj) return self.function(obj, self.type(sequence)) -- cgit v1.2.3 From 40d4a4b13d4ca959879d1798f24d510fd7abf4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:07:19 +0100 Subject: Fix typo in FuzzyDateTime (Closes #189). Thanks to @shinuza for spotting this! --- factory/fuzzy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 0137ba9..564264e 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -289,10 +289,10 @@ class FuzzyDateTime(BaseFuzzyDateTime): def _check_bounds(self, start_dt, end_dt): if start_dt.tzinfo is None: raise ValueError( - "FuzzyDateTime only handles aware datetimes, got start=%r" + "FuzzyDateTime requires timezone-aware datetimes, got start=%r" % start_dt) if end_dt.tzinfo is None: raise ValueError( - "FuzzyDateTime only handles aware datetimes, got end=%r" + "FuzzyDateTime requires timezone-aware datetimes, got end=%r" % end_dt) super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt) -- cgit v1.2.3 From 69befae5fde1897cf68c4d44a146db5ba642c814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:22:03 +0100 Subject: Allow lazy evaluation of FuzzyChoice's iterators (Closes #184). This allows the following idiom: ``user = factory.fuzzy.FuzzyChoice(User.objects.all())`` Previously, the ``User.objects.all()`` queryset would have been evaluated *at import time*; it is now evaluated with the first use of the ``FuzzyChoice``. --- docs/changelog.rst | 1 + docs/fuzzy.rst | 7 +++++-- factory/fuzzy.py | 12 ++++++++++-- tests/test_fuzzy.py | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ebe9930..13fdd68 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog *New:* - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). + - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). *Deprecation:* diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 0658652..18978e4 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -62,8 +62,11 @@ FuzzyChoice The :class:`FuzzyChoice` fuzzer yields random choices from the given iterable. - .. note:: The passed in :attr:`choices` will be converted into a list at - declaration time. + .. note:: The passed in :attr:`choices` will be converted into a list upon + first use, not at declaration time. + + This allows passing in, for instance, a Django queryset that will + only hit the database during the database, not at import time. .. attribute:: choices diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 564264e..4e6a03d 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -113,13 +113,21 @@ class FuzzyText(BaseFuzzyAttribute): class FuzzyChoice(BaseFuzzyAttribute): - """Handles fuzzy choice of an attribute.""" + """Handles fuzzy choice of an attribute. + + Args: + choices (iterable): An iterable yielding options; will only be unrolled + on the first call. + """ def __init__(self, choices, **kwargs): - self.choices = list(choices) + self.choices = None + self.choices_generator = choices super(FuzzyChoice, self).__init__(**kwargs) def fuzz(self): + if self.choices is None: + self.choices = list(self.choices_generator) return _random.choice(self.choices) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index fd32705..c7e1106 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -74,6 +74,24 @@ class FuzzyChoiceTestCase(unittest.TestCase): res = d.evaluate(2, None, False) self.assertIn(res, [0, 1, 2]) + def test_lazy_generator(self): + class Gen(object): + def __init__(self, options): + self.options = options + self.unrolled = False + + def __iter__(self): + self.unrolled = True + return iter(self.options) + + opts = Gen([1, 2, 3]) + d = fuzzy.FuzzyChoice(opts) + self.assertFalse(opts.unrolled) + + res = d.evaluate(2, None, False) + self.assertIn(res, [1, 2, 3]) + self.assertTrue(opts.unrolled) + class FuzzyIntegerTestCase(unittest.TestCase): def test_definition(self): -- cgit v1.2.3 From 72fd943513b0e516f06c53b13ff35ca814b0a4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:37:54 +0100 Subject: Fix issues between mute_signals() and factory inheritance (Closes #183). Previously, if a factory was decorated with ``@mute_signals`` and one of its descendant called another one of its descendant, signals weren't unmuted properly. --- docs/changelog.rst | 4 ++++ factory/django.py | 9 +++++++-- tests/test_django.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 13fdd68..0cf8368 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,10 @@ ChangeLog - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). +*Bugfix:* + + - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). + *Deprecation:* - Remove deprecated features from :ref:`v2.4.0` diff --git a/factory/django.py b/factory/django.py index 7050366..e823ee9 100644 --- a/factory/django.py +++ b/factory/django.py @@ -269,6 +269,9 @@ class mute_signals(object): signal.receivers = receivers self.paused = {} + def copy(self): + return mute_signals(*self.signals) + def __call__(self, callable_obj): if isinstance(callable_obj, base.FactoryMetaClass): # Retrieve __func__, the *actual* callable object. @@ -277,7 +280,8 @@ class mute_signals(object): @classmethod @functools.wraps(generate_method) def wrapped_generate(*args, **kwargs): - with self: + # A mute_signals() object is not reentrant; use a copy everytime. + with self.copy(): return generate_method(*args, **kwargs) callable_obj._generate = wrapped_generate @@ -286,7 +290,8 @@ class mute_signals(object): else: @functools.wraps(callable_obj) def wrapper(*args, **kwargs): - with self: + # A mute_signals() object is not reentrant; use a copy everytime. + with self.copy(): return callable_obj(*args, **kwargs) return wrapper diff --git a/tests/test_django.py b/tests/test_django.py index 0cbef19..4653305 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -592,6 +592,27 @@ class PreventSignalsTestCase(unittest.TestCase): self.assertSignalsReactivated() + def test_class_decorator_with_subfactory(self): + @factory.django.mute_signals(signals.pre_save, signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithSignals + + @factory.post_generation + def post(obj, create, extracted, **kwargs): + if not extracted: + WithSignalsDecoratedFactory.create(post=42) + + # This will disable the signals (twice), create two objects, + # and reactivate the signals. + WithSignalsDecoratedFactory() + + self.assertEqual(self.handlers.pre_init.call_count, 2) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.assertSignalsReactivated() + def test_class_decorator_build(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): -- cgit v1.2.3 From 636ca46951d710a4b9d9fd61ec1da02294806d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:53:15 +0100 Subject: Add support for multidb with Django (Closes #171). The ``factory.django.DjangoModelFactory`` now takes an extra option: ``` class MyFactory(factory.django.DjangoModelFactory): class Meta: model = models.MyModel database = 'replica' ``` This will create all instances of ``models.Model`` in the ``'replica'`` database. --- docs/changelog.rst | 1 + docs/orms.rst | 9 ++++++++- factory/django.py | 8 ++++++-- tests/djapp/settings.py | 3 +++ tests/test_django.py | 10 ++++++++++ tests/test_using.py | 6 ++++++ 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0cf8368..c2da698 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). + - Support non-default databases at the factory level (see :issue:`171`) *Bugfix:* diff --git a/docs/orms.rst b/docs/orms.rst index a0afc40..5105e66 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -42,7 +42,14 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. class:: DjangoOptions(factory.base.FactoryOptions) - The ``class Meta`` on a :class:`~DjangoModelFactory` supports an extra parameter: + The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters: + + .. attribute:: database + + .. versionadded:: 2.5.0 + + All queries to the related model will be routed to the given database. + It defaults to ``'default'``. .. attribute:: django_get_or_create diff --git a/factory/django.py b/factory/django.py index e823ee9..ee5749a 100644 --- a/factory/django.py +++ b/factory/django.py @@ -56,6 +56,7 @@ class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), + base.OptionDefault('database', 'default', inherit=True), ] def _get_counter_reference(self): @@ -100,9 +101,12 @@ class DjangoModelFactory(base.Factory): raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) try: - return model_class._default_manager # pylint: disable=W0212 + manager = model_class._default_manager # pylint: disable=W0212 except AttributeError: - return model_class.objects + manager = model_class.objects + + manager = manager.using(cls._meta.database) + return manager @classmethod def _get_or_create(cls, model_class, *args, **kwargs): diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index c051faf..18e43dd 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -34,6 +34,9 @@ DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', }, + 'replica': { + 'ENGINE': 'django.db.backends.sqlite3', + }, } diff --git a/tests/test_django.py b/tests/test_django.py index 4653305..a8f1f77 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -165,6 +165,16 @@ class ModelTests(django_test.TestCase): self.assertRaises(factory.FactoryError, UnsetModelFactory.create) + def test_cross_database(self): + class OtherDBFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + database = 'replica' + + obj = OtherDBFactory() + self.assertFalse(models.StandardModel.objects.exists()) + self.assertEqual(obj, models.StandardModel.objects.using('replica').get()) + @unittest.skipIf(django is None, "Django not installed.") class DjangoPkSequenceTestCase(django_test.TestCase): diff --git a/tests/test_using.py b/tests/test_using.py index 7318f2e..1d7977f 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -69,6 +69,9 @@ class FakeModel(object): def order_by(self, *args, **kwargs): return [1] + def using(self, db): + return self + objects = FakeModelManager() def __init__(self, **kwargs): @@ -1490,6 +1493,9 @@ class BetterFakeModelManager(object): instance.id = 2 return instance, True + def using(self, db): + return self + class BetterFakeModel(object): @classmethod -- cgit v1.2.3 From a456a9e3f440e5f61497e97d75dd0a15efe71a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:04:41 +0100 Subject: Remove limitations of factory.StubFactory (Closes #131). ``StubFactory.build()`` is now supported, and maps to ``StubFactory.stub()``. --- docs/changelog.rst | 1 + factory/base.py | 2 +- tests/test_base.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c2da698..a6ca79e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,7 @@ ChangeLog *Bugfix:* - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). + - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`). *Deprecation:* diff --git a/factory/base.py b/factory/base.py index 7215f00..4c3e0ad 100644 --- a/factory/base.py +++ b/factory/base.py @@ -683,7 +683,7 @@ class StubFactory(Factory): @classmethod def build(cls, **kwargs): - raise UnsupportedStrategy() + return cls.stub(**kwargs) @classmethod def create(cls, **kwargs): diff --git a/tests/test_base.py b/tests/test_base.py index d1df58e..12031b9 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -403,7 +403,7 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertRaises(base.Factory.UnknownStrategy, TestModelFactory) - def test_stub_with_non_stub_strategy(self): + def test_stub_with_create_strategy(self): class TestModelFactory(base.StubFactory): class Meta: model = TestModel @@ -414,8 +414,18 @@ class FactoryDefaultStrategyTestCase(unittest.TestCase): self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) + def test_stub_with_build_strategy(self): + class TestModelFactory(base.StubFactory): + class Meta: + model = TestModel + + one = 'one' + TestModelFactory._meta.strategy = base.BUILD_STRATEGY - self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) + obj = TestModelFactory() + + # For stubs, build() is an alias of stub(). + self.assertFalse(isinstance(obj, TestModel)) def test_change_strategy(self): @base.use_strategy(base.CREATE_STRATEGY) @@ -454,6 +464,23 @@ class FactoryCreationTestCase(unittest.TestCase): self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY) + def test_stub_and_subfactory(self): + class StubA(base.StubFactory): + class Meta: + model = TestObject + + one = 'blah' + + class StubB(base.StubFactory): + class Meta: + model = TestObject + + stubbed = declarations.SubFactory(StubA, two='two') + + b = StubB() + self.assertEqual('blah', b.stubbed.one) + self.assertEqual('two', b.stubbed.two) + def test_custom_creation(self): class TestModelFactory(FakeModelFactory): class Meta: -- cgit v1.2.3 From 4e0e563c1c0d823d2869d340e2fa31ca8630d854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:31:56 +0100 Subject: Turn FileField/ImageField into normal fields (Closes #141). Previously, they ran as post_generation hooks, meaning that they couldn't be checked in a model's ``save()`` method, for instance. --- docs/changelog.rst | 1 + factory/django.py | 31 +++++++++--------------------- tests/test_django.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a6ca79e..c2731ef 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,7 @@ ChangeLog - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). - Support non-default databases at the factory level (see :issue:`171`) + - Make :class:`factory.django.FileField` and :class:`factory.django.ImageField` non-post_generation, i.e normal fields also available in ``save()`` (see :issue:`141`). *Bugfix:* diff --git a/factory/django.py b/factory/django.py index ee5749a..9d4cde9 100644 --- a/factory/django.py +++ b/factory/django.py @@ -144,24 +144,23 @@ class DjangoModelFactory(base.Factory): obj.save() -class FileField(declarations.PostGenerationDeclaration): +class FileField(declarations.ParameteredAttribute): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' def __init__(self, **defaults): require_django() - self.defaults = defaults - super(FileField, self).__init__() + super(FileField, self).__init__(**defaults) def _make_data(self, params): """Create data for the field.""" return params.get('data', b'') - def _make_content(self, extraction_context): + def _make_content(self, extra): path = '' params = dict(self.defaults) - params.update(extraction_context.extra) + params.update(extra) if params.get('from_path') and params.get('from_file'): raise ValueError( @@ -169,12 +168,7 @@ class FileField(declarations.PostGenerationDeclaration): "be non-empty when calling factory.django.FileField." ) - if extraction_context.did_extract: - # Should be a django.core.files.File - content = extraction_context.value - path = content.name - - elif params.get('from_path'): + if params.get('from_path'): path = params['from_path'] f = open(path, 'rb') content = django_files.File(f, name=path) @@ -196,19 +190,12 @@ class FileField(declarations.PostGenerationDeclaration): filename = params.get('filename', default_filename) return filename, content - def call(self, obj, create, extraction_context): + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Fill in the field.""" - if extraction_context.did_extract and extraction_context.value is None: - # User passed an empty value, don't fill - return - filename, content = self._make_content(extraction_context) - field_file = getattr(obj, extraction_context.for_field) - try: - field_file.save(filename, content, save=create) - finally: - content.file.close() - return field_file + filename, content = self._make_content(extra) + print("Returning file with filename=%r, contents=%r" % (filename, content)) + return django_files.File(content.file, filename) class ImageField(FileField): diff --git a/tests/test_django.py b/tests/test_django.py index a8f1f77..cf80edb 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -364,6 +364,9 @@ class DjangoFileFieldTestCase(unittest.TestCase): o = WithFileFactory.build() self.assertIsNone(o.pk) self.assertEqual(b'', o.afile.read()) + self.assertEqual('example.dat', o.afile.name) + + o.save() self.assertEqual('django/example.dat', o.afile.name) def test_default_create(self): @@ -375,19 +378,26 @@ class DjangoFileFieldTestCase(unittest.TestCase): def test_with_content(self): o = WithFileFactory.build(afile__data='foo') self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'foo', o.afile.read()) self.assertEqual('django/example.dat', o.afile.name) def test_with_file(self): with open(testdata.TESTFILE_PATH, 'rb') as f: o = WithFileFactory.build(afile__from_file=f) - self.assertIsNone(o.pk) + o.save() + self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) def test_with_path(self): o = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH) self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) @@ -397,7 +407,9 @@ class DjangoFileFieldTestCase(unittest.TestCase): afile__from_file=f, afile__from_path='' ) - self.assertIsNone(o.pk) + # Django only allocates the full path on save() + o.save() + self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) @@ -407,6 +419,9 @@ class DjangoFileFieldTestCase(unittest.TestCase): afile__from_file=None, ) self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) @@ -422,14 +437,21 @@ class DjangoFileFieldTestCase(unittest.TestCase): afile__filename='example.foo', ) self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.foo', o.afile.name) def test_existing_file(self): o1 = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH) + o1.save() + self.assertEqual('django/example.data', o1.afile.name) - o2 = WithFileFactory.build(afile=o1.afile) + o2 = WithFileFactory.build(afile__from_file=o1.afile) self.assertIsNone(o2.pk) + o2.save() + self.assertEqual(b'example_data\n', o2.afile.read()) self.assertNotEqual('django/example.data', o2.afile.name) self.assertRegexpMatches(o2.afile.name, r'django/example_\w+.data') @@ -453,6 +475,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): def test_default_build(self): o = WithImageFactory.build() self.assertIsNone(o.pk) + o.save() + self.assertEqual(100, o.animage.width) self.assertEqual(100, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -460,6 +484,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): def test_default_create(self): o = WithImageFactory.create() self.assertIsNotNone(o.pk) + o.save() + self.assertEqual(100, o.animage.width) self.assertEqual(100, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -467,6 +493,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): def test_with_content(self): o = WithImageFactory.build(animage__width=13, animage__color='red') self.assertIsNone(o.pk) + o.save() + self.assertEqual(13, o.animage.width) self.assertEqual(13, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -480,6 +508,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): def test_gif(self): o = WithImageFactory.build(animage__width=13, animage__color='blue', animage__format='GIF') self.assertIsNone(o.pk) + o.save() + self.assertEqual(13, o.animage.width) self.assertEqual(13, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -493,7 +523,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): def test_with_file(self): with open(testdata.TESTIMAGE_PATH, 'rb') as f: o = WithImageFactory.build(animage__from_file=f) - self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -501,6 +532,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): def test_with_path(self): o = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH) self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -511,7 +544,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): animage__from_file=f, animage__from_path='' ) - self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -522,6 +556,8 @@ class DjangoImageFieldTestCase(unittest.TestCase): animage__from_file=None, ) self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -538,15 +574,20 @@ class DjangoImageFieldTestCase(unittest.TestCase): animage__filename='example.foo', ) self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.foo', o.animage.name) def test_existing_file(self): o1 = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH) + o1.save() - o2 = WithImageFactory.build(animage=o1.animage) + o2 = WithImageFactory.build(animage__from_file=o1.animage) self.assertIsNone(o2.pk) + o2.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o2.animage.read())) self.assertNotEqual('django/example.jpeg', o2.animage.name) -- cgit v1.2.3 From 35f9ee112f5b3dfb799e24635d548fd228c98db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:40:07 +0100 Subject: Release v2.5.0 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c2731ef..4018e32 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.5.0: -2.5.0 (master) --------------- +2.5.0 (2015-03-26) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index 8fc8ef8..20b4d0f 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.4.1' +__version__ = '2.5.0' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 8a3127f394283b367f15f43328a1c8751982898f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:41:11 +0100 Subject: Get ready for next release. --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4018e32..0554eb7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,12 @@ ChangeLog ========= +.. _v2.5.1: + +2.5.1 (master) +-------------- + + .. _v2.5.0: 2.5.0 (2015-03-26) -- cgit v1.2.3 From a1e5ff13c0573feb95c810e7e27cd30de97b8f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:45:29 +0100 Subject: Update header years. --- LICENSE | 2 +- docs/conf.py | 2 +- factory/__init__.py | 2 +- factory/base.py | 2 +- factory/compat.py | 2 +- factory/containers.py | 2 +- factory/declarations.py | 2 +- factory/django.py | 2 +- factory/fuzzy.py | 2 +- factory/helpers.py | 2 +- factory/mogo.py | 2 +- factory/mongoengine.py | 2 +- factory/utils.py | 2 +- tests/__init__.py | 2 +- tests/compat.py | 2 +- tests/cyclic/bar.py | 2 +- tests/cyclic/foo.py | 2 +- tests/cyclic/self_ref.py | 2 +- tests/djapp/models.py | 2 +- tests/djapp/settings.py | 2 +- tests/test_alchemy.py | 2 +- tests/test_base.py | 2 +- tests/test_containers.py | 2 +- tests/test_declarations.py | 2 +- tests/test_django.py | 2 +- tests/test_fuzzy.py | 2 +- tests/test_helpers.py | 2 +- tests/test_using.py | 2 +- tests/test_utils.py | 2 +- tests/testdata/__init__.py | 2 +- tests/tools.py | 2 +- tests/utils.py | 2 +- 32 files changed, 32 insertions(+), 32 deletions(-) diff --git a/LICENSE b/LICENSE index 620dc61..d009218 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2010 Mark Sandstrom -Copyright (c) 2011-2013 Raphaël Barrois +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 diff --git a/docs/conf.py b/docs/conf.py index ce6730b..c3512e0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ master_doc = 'index' # General information about the project. project = u'Factory Boy' -copyright = u'2011-2013, Raphaël Barrois, Mark Sandstrom' +copyright = u'2011-2015, Raphaël Barrois, Mark Sandstrom' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/factory/__init__.py b/factory/__init__.py index 20b4d0f..ad313b3 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/base.py b/factory/base.py index 4c3e0ad..d48edd5 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/compat.py b/factory/compat.py index 7747b1a..785d174 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/containers.py b/factory/containers.py index 5116320..0ae354b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/declarations.py b/factory/declarations.py index b3833ee..8f2314a 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/django.py b/factory/django.py index 9d4cde9..7862d75 100644 --- a/factory/django.py +++ b/factory/django.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 4e6a03d..923d8b7 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/helpers.py b/factory/helpers.py index b9cef6e..60a4d75 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/mogo.py b/factory/mogo.py index 5541043..c6c3c19 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/mongoengine.py b/factory/mongoengine.py index e3ab99c..f50b727 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/factory/utils.py b/factory/utils.py index 276977a..6ecf9a7 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/__init__.py b/tests/__init__.py index 5b6fc55..c73165f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois from .test_base import * from .test_containers import * diff --git a/tests/compat.py b/tests/compat.py index ff96f13..167c185 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index a5e6bf1..b4f8e0c 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index 18de362..62e58c0 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py index f18c989..d98b3ab 100644 --- a/tests/cyclic/self_ref.py +++ b/tests/cyclic/self_ref.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 9b21181..35c765f 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index 18e43dd..1ef16d5 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 2deb418..9d7288a 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2013 Romain Command& +# Copyright (c) 2015 Romain Command& # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_base.py b/tests/test_base.py index 12031b9..24f64e5 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_containers.py b/tests/test_containers.py index bd7019e..083b306 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 18a4cd4..2601a38 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_django.py b/tests/test_django.py index cf80edb..2744032 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index c7e1106..3f9c434 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f5a66e5..bee66ca 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_using.py b/tests/test_using.py index 1d7977f..b7fea81 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/test_utils.py b/tests/test_utils.py index d321c2a..eed7a57 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py index 9956610..b534998 100644 --- a/tests/testdata/__init__.py +++ b/tests/testdata/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/tools.py b/tests/tools.py index 571899b..47f705c 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 diff --git a/tests/utils.py b/tests/utils.py index 215fc83..7a31ed2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# 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 -- cgit v1.2.3 From 140956f854b34164cce90bbaaa49255383a440c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:52:41 +0100 Subject: Clarify impacts of 2.5.0. --- docs/changelog.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0554eb7..326e8f1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,11 +25,16 @@ ChangeLog - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`). + *Deprecation:* - Remove deprecated features from :ref:`v2.4.0` - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy; - this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. + this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. See https://github.com/rbarrois/factory_boy/commit/13d310f for technical details. + +.. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature. + This could trigger some bugs when tests expected a non-zero sequence reference. + .. _v2.4.1: -- cgit v1.2.3 From d6f351c5af74ac659b4d3add916546d286ff4fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 13:44:15 +0100 Subject: Add upgrade instructions for 2.5.0 --- README.rst | 12 +++++++----- docs/changelog.rst | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index c787ca8..8bfbc24 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,8 @@ factory_boy .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ +Latest release: `2.5.0 `_ (includes breaking changes, see the `ChangeLog `_) + factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures @@ -281,12 +283,12 @@ This will yield messages similar to those (artificial indentation): ORM Support """"""""""" -factory_boy has specific support for a few ORMs, through specific :class:`~factory.Factory` subclasses: +factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses: -* Django, with :class:`~factory.django.DjangoModelFactory` -* Mogo, with :class:`~factory.mogo.MogoFactory` -* MongoEngine, with :class:`~factory.mongoengine.MongoEngineFactory` -* SQLAlchemy, with :class:`~factory.alchemy.SQLAlchemyModelFactory` +* Django, with ``factory.django.DjangoModelFactory`` +* Mogo, with ``factory.mogo.MogoFactory`` +* MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` +* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` Contributing ------------ diff --git a/docs/changelog.rst b/docs/changelog.rst index 326e8f1..a7ff050 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,36 @@ ChangeLog .. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature. This could trigger some bugs when tests expected a non-zero sequence reference. +Upgrading +""""""""" + +.. warning:: Version 2.5.0 removes features that were marked as deprecated in :ref:`v2.4.0 `. + +All ``FACTORY_*``-style attributes are now declared in a ``class Meta:`` section: + +.. code-block:: python + + # Old-style, deprecated + class MyFactory(factory.Factory): + FACTORY_FOR = models.MyModel + FACTORY_HIDDEN_ARGS = ['a', 'b', 'c'] + + # New-style + class MyFactory(factory.Factory): + class Meta: + model = models.MyModel + exclude = ['a', 'b', 'c'] + +A simple shell command to upgrade the code would be: + +.. code-block:: sh + + # sed -i: inplace update + # grep -l: only file names, not matching lines + sed -i 's/FACTORY_FOR =/class Meta:\n model =/' $(grep -l FACTORY_FOR $(find . -name '*.py')) + +This takes care of all ``FACTORY_FOR`` occurences; the files containing other attributes to rename can be found with ``grep -R FACTORY .`` + .. _v2.4.1: -- cgit v1.2.3 From 03ca4ecebc914f2e120e902b8fcbe2b526460ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 13:50:07 +0100 Subject: Remove debug prints --- factory/django.py | 1 - 1 file changed, 1 deletion(-) diff --git a/factory/django.py b/factory/django.py index 7862d75..eb07bfb 100644 --- a/factory/django.py +++ b/factory/django.py @@ -194,7 +194,6 @@ class FileField(declarations.ParameteredAttribute): """Fill in the field.""" filename, content = self._make_content(extra) - print("Returning file with filename=%r, contents=%r" % (filename, content)) return django_files.File(content.file, filename) -- cgit v1.2.3 From bdc1b815cfdf3028379c6c3f18c9c47ee8298a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 16:27:05 +0100 Subject: Respect default manager in DjangoModelFactory (Closes #192). The previous version tries to use ``cls._default_manager`` all the time, which breaks with ``manager.using(db_name)``. --- docs/changelog.rst | 3 +++ factory/django.py | 13 ++++++------- tests/djapp/models.py | 17 +++++++++++++++++ tests/test_django.py | 12 ++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a7ff050..cc4a1dc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,9 @@ ChangeLog 2.5.1 (master) -------------- +*Bugfix:* + + - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`) .. _v2.5.0: diff --git a/factory/django.py b/factory/django.py index eb07bfb..ba81f13 100644 --- a/factory/django.py +++ b/factory/django.py @@ -45,6 +45,8 @@ from .compat import BytesIO, is_string logger = logging.getLogger('factory.generate') +DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS + def require_django(): """Simple helper to ensure Django is available.""" @@ -56,7 +58,7 @@ class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), - base.OptionDefault('database', 'default', inherit=True), + base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True), ] def _get_counter_reference(self): @@ -100,12 +102,9 @@ class DjangoModelFactory(base.Factory): if model_class is None: raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) - try: - manager = model_class._default_manager # pylint: disable=W0212 - except AttributeError: - manager = model_class.objects - - manager = manager.using(cls._meta.database) + manager = model_class.objects + if cls._meta.database != DEFAULT_DB_ALIAS: + manager = manager.using(cls._meta.database) return manager @classmethod diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 35c765f..96ee5cf 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -87,3 +87,20 @@ else: class WithSignals(models.Model): foo = models.CharField(max_length=20) + + +class CustomQuerySet(models.QuerySet): + pass + + +class CustomManager(models.Manager): + + def create(self, arg=None, **kwargs): + return super(CustomManager, self).create(**kwargs) + + +class WithCustomManager(models.Model): + + foo = models.CharField(max_length=20) + + objects = CustomManager.from_queryset(CustomQuerySet)() diff --git a/tests/test_django.py b/tests/test_django.py index 2744032..9ac8f5c 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -157,6 +157,13 @@ if django is not None: model = models.WithSignals + class WithCustomManagerFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithCustomManager + + foo = factory.Sequence(lambda n: "foo%d" % n) + + @unittest.skipIf(django is None, "Django not installed.") class ModelTests(django_test.TestCase): def test_unset_model(self): @@ -706,5 +713,10 @@ class PreventSignalsTestCase(unittest.TestCase): self.assertSignalsReactivated() +class DjangoCustomManagerTestCase(django_test.TestCase): + + def test_extra_args(self): + model = WithCustomManagerFactory(arg='foo') + if __name__ == '__main__': # pragma: no cover unittest.main() -- cgit v1.2.3 From 5363951bb62ca90d971bf036851dea564204ed2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 17:00:32 +0100 Subject: Support declarations in FileField/ImageField. Previously, the declarations (``factory.Sequence`` & co) weren't properly computed. --- docs/changelog.rst | 2 ++ factory/django.py | 11 ++++++----- tests/djapp/models.py | 1 + tests/test_django.py | 11 +++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cc4a1dc..de9778b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ ChangeLog *Bugfix:* - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`) + - Allow passing declarations (e.g :class:`~factory.Sequence`) as parameters to :class:`~factory.django.FileField` + and :class:`~factory.django.ImageField`. .. _v2.5.0: diff --git a/factory/django.py b/factory/django.py index ba81f13..cbf7c10 100644 --- a/factory/django.py +++ b/factory/django.py @@ -147,6 +147,7 @@ class FileField(declarations.ParameteredAttribute): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' + EXTEND_CONTAINERS = True def __init__(self, **defaults): require_django() @@ -156,10 +157,8 @@ class FileField(declarations.ParameteredAttribute): """Create data for the field.""" return params.get('data', b'') - def _make_content(self, extra): + def _make_content(self, params): path = '' - params = dict(self.defaults) - params.update(extra) if params.get('from_path') and params.get('from_file'): raise ValueError( @@ -189,10 +188,12 @@ class FileField(declarations.ParameteredAttribute): filename = params.get('filename', default_filename) return filename, content - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def generate(self, sequence, obj, create, params): """Fill in the field.""" - filename, content = self._make_content(extra) + params.setdefault('__sequence', sequence) + params = base.DictFactory.simple_generate(create, **params) + filename, content = self._make_content(params) return django_files.File(content.file, filename) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 96ee5cf..1c1fd8e 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -79,6 +79,7 @@ if Image is not None: # PIL is available class WithImage(models.Model): animage = models.ImageField(upload_to=WITHFILE_UPLOAD_TO) + size = models.IntegerField(default=0) else: class WithImage(models.Model): diff --git a/tests/test_django.py b/tests/test_django.py index 9ac8f5c..ac52769 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -497,6 +497,17 @@ class DjangoImageFieldTestCase(unittest.TestCase): self.assertEqual(100, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) + def test_complex_create(self): + o = WithImageFactory.create( + size=10, + animage__filename=factory.Sequence(lambda n: 'img%d.jpg' % n), + __sequence=42, + animage__width=factory.SelfAttribute('..size'), + animage__height=factory.SelfAttribute('width'), + ) + self.assertIsNotNone(o.pk) + self.assertEqual('django/img42.jpg', o.animage.name) + def test_with_content(self): o = WithImageFactory.build(animage__width=13, animage__color='red') self.assertIsNone(o.pk) -- cgit v1.2.3 From 48a1e4a65968a911d530c87cf0bcb9f312927641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 17:48:38 +0100 Subject: Release v2.5.1 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index de9778b..0d12cb3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.5.1: -2.5.1 (master) --------------- +2.5.1 (2015-03-27) +------------------ *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index ad313b3..378035f 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.5.0' +__version__ = '2.5.1' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 71f5d76c5ecb2b88cd734c9f2611d7dfad1dd923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 17:57:10 +0100 Subject: Fix custom queryset tests for Django<1.7 --- tests/djapp/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 1c1fd8e..513c47c 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -90,10 +90,6 @@ class WithSignals(models.Model): foo = models.CharField(max_length=20) -class CustomQuerySet(models.QuerySet): - pass - - class CustomManager(models.Manager): def create(self, arg=None, **kwargs): @@ -104,4 +100,4 @@ class WithCustomManager(models.Model): foo = models.CharField(max_length=20) - objects = CustomManager.from_queryset(CustomQuerySet)() + objects = CustomManager() -- cgit v1.2.3 From c77d97d950bcf6fab0061519922c4800e06ff711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 2 Apr 2015 09:48:53 +0200 Subject: Fix imports for Django 1.8 --- tests/test_django.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index ac52769..9da99cc 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -53,7 +53,10 @@ if django is not None: from django import test as django_test from django.conf import settings from django.db import models as django_models - from django.test import simple as django_test_simple + if django.VERSION <= (1, 8, 0): + from django.test.simple import DjangoTestSuiteRunner + else: + from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner from django.test import utils as django_test_utils from django.db.models import signals from .djapp import models @@ -71,7 +74,7 @@ def setUpModule(): if django is None: # pragma: no cover raise unittest.SkipTest("Django not installed") django_test_utils.setup_test_environment() - runner = django_test_simple.DjangoTestSuiteRunner() + runner = DjangoTestSuiteRunner() runner_state = runner.setup_databases() test_state.update({ 'runner': runner, -- cgit v1.2.3 From ae5d46af448fc33ef74eee99c5a3d686c8d26e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 12 Apr 2015 12:14:29 +0200 Subject: Fix tests with latest pymongo/mongoengine. mongoengine>=0.9.0 and pymongo>=2.1 require extra parameters: - The server connection timeout was set too high - We have to define a ``read_preference``. --- tests/test_mongoengine.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 988c179..c0a019c 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -61,10 +61,20 @@ class MongoEngineTestCase(unittest.TestCase): db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') db_host = os.environ.get('MONGO_HOST', 'localhost') db_port = int(os.environ.get('MONGO_PORT', '27017')) + MONGOD_TIMEOUT_MS = 100 @classmethod def setUpClass(cls): - cls.db = mongoengine.connect(cls.db_name, host=cls.db_host, port=cls.db_port) + from pymongo import read_preferences as mongo_rp + cls.db = mongoengine.connect( + db=cls.db_name, + host=cls.db_host, + port=cls.db_port, + # PyMongo>=2.1 requires an explicit read_preference. + read_preference=mongo_rp.ReadPreference.PRIMARY, + # PyMongo>=2.1 has a 20s timeout, use 100ms instead + serverselectiontimeoutms=cls.MONGOD_TIMEOUT_MS, + ) @classmethod def tearDownClass(cls): -- cgit v1.2.3 From c58a190b12535bdcfc984b1be8b72a6a2c84a2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 12 Apr 2015 12:20:53 +0200 Subject: mongoengine: allow tuning the server timeout. So that it doesn't fail on ci... --- tests/test_mongoengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index c0a019c..1cf0cb5 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -61,7 +61,7 @@ class MongoEngineTestCase(unittest.TestCase): db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') db_host = os.environ.get('MONGO_HOST', 'localhost') db_port = int(os.environ.get('MONGO_PORT', '27017')) - MONGOD_TIMEOUT_MS = 100 + server_timeout_ms = int(os.environ.get('MONGO_TIMEOUT', '300')) @classmethod def setUpClass(cls): -- cgit v1.2.3 From 16b414d27d638fd76701f10fe338c67d7d9dfde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 12 Apr 2015 12:23:49 +0200 Subject: test_mongoengine: fix typo --- tests/test_mongoengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 1cf0cb5..6fa4125 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -73,7 +73,7 @@ class MongoEngineTestCase(unittest.TestCase): # PyMongo>=2.1 requires an explicit read_preference. read_preference=mongo_rp.ReadPreference.PRIMARY, # PyMongo>=2.1 has a 20s timeout, use 100ms instead - serverselectiontimeoutms=cls.MONGOD_TIMEOUT_MS, + serverselectiontimeoutms=cls.server_timeout_ms, ) @classmethod -- cgit v1.2.3 From e357919cdb52af96eb67148fd38dced34981821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 14 Apr 2015 00:20:33 +0200 Subject: Remove warnings with Django 1.7 (Closes #195). Builds upon pull request by @shinuza: - Properly import ``get_model`` - Run ``django.setup()`` before importing any models. --- factory/django.py | 17 +++++++++++++++-- tests/test_django.py | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/factory/django.py b/factory/django.py index cbf7c10..74e4fdb 100644 --- a/factory/django.py +++ b/factory/django.py @@ -32,8 +32,10 @@ import functools """factory_boy extensions for use with the Django framework.""" try: + import django from django.core import files as django_files except ImportError as e: # pragma: no cover + django = None django_files = None import_failure = e @@ -54,6 +56,18 @@ def require_django(): raise import_failure +if django is None: + def get_model(app, model): + raise import_failure + +elif django.VERSION[:2] < (1, 7): + from django.db.models.loading import get_model + +else: + from django import apps as django_apps + get_model = django_apps.apps.get_model + + class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ @@ -92,8 +106,7 @@ class DjangoModelFactory(base.Factory): if is_string(definition) and '.' in definition: app, model = definition.split('.', 1) - from django.db.models import loading as django_loading - return django_loading.get_model(app, model) + return get_model(app, model) return definition diff --git a/tests/test_django.py b/tests/test_django.py index 9da99cc..113caeb 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -50,6 +50,8 @@ from . import tools if django is not None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') + if django.VERSION >= (1, 7, 0): + django.setup() from django import test as django_test from django.conf import settings from django.db import models as django_models @@ -64,8 +66,6 @@ if django is not None: else: django_test = unittest -if django is not None and django.VERSION >= (1, 7, 0): - django.setup() test_state = {} -- cgit v1.2.3 From 52c984d3f1c6c440a832e53331d7f95f25c8b046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 14 Apr 2015 00:22:08 +0200 Subject: Fix minor typo (Closes #194). Thanks to @DasAllFolks for spotting it! --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 9fd2576..44f78b6 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -42,7 +42,7 @@ The :class:`Factory` class It will be automatically set to ``True`` if neither the :class:`Factory` subclass nor its parents define the :attr:`~FactoryOptions.model` attribute. - .. warning:: This flag is reset to ``False`` When a :class:`Factory` subclasses + .. warning:: This flag is reset to ``False`` when a :class:`Factory` subclasses another one if a :attr:`~FactoryOptions.model` is set. .. versionadded:: 2.4.0 -- cgit v1.2.3 From 23616f4a376f79aaf0f9088dc15dc87a668e1ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 19 Apr 2015 17:00:53 +0200 Subject: Update travis config: focus on Py2.7/Py3.4 --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3954bf5..a1d14f6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,15 @@ language: python python: - - "2.6" - "2.7" - - "3.2" - - "3.3" + - "3.4" - "pypy" script: - python setup.py test install: - - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install "Django<1.7" --use-mirrors; else pip install Django; fi + - pip install Django - pip install sqlalchemy --use-mirrors - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi -- cgit v1.2.3 From 3bb4a0eb6170a588434fbb6438ec1a063eb115e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:31:15 +0200 Subject: Add wheel support --- dev_requirements.txt | 1 + setup.cfg | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 setup.cfg diff --git a/dev_requirements.txt b/dev_requirements.txt index bdc23d0..a8dd896 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,3 +4,4 @@ Pillow sqlalchemy mongoengine mock +wheel diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 -- cgit v1.2.3 From 86d1b29ea5e508c37320ad66949d2d8d33b1db02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:31:43 +0200 Subject: Add badges to README. --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 8bfbc24..9ffa809 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,18 @@ factory_boy =========== +.. image:: https://pypip.in/version/factory_boy/badge.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: Latest Version + +.. image:: https://pypip.in/py_versions/factory_boy/badge.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: Supported Python versions + +.. image:: https://pypip.in/wheel/factory_boy/badge.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: Wheel status + .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ -- cgit v1.2.3 From 0e73c401d3a0aa0780bf428cc2f4cea142118ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:33:08 +0200 Subject: Declare Python3.4 support --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f637a48..9c8f8d2 100755 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ setup( "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules" -- cgit v1.2.3 From f73be3fc0d8c51aef3fcd890e61bb5ba74c55909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:33:50 +0200 Subject: Fix typo in setup.py (Closes #197). Thanks to @nikolas for spotting it! --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9c8f8d2..daee69b 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ PACKAGE = 'factory' setup( name='factory_boy', version=get_version(PACKAGE), - description="A verstile test fixtures replacement based on thoughtbot's factory_girl for Ruby.", + description="A versatile test fixtures replacement based on thoughtbot's factory_girl for Ruby.", author='Mark Sandstrom', author_email='mark@deliciouslynerdy.com', maintainer='Raphaël Barrois', -- cgit v1.2.3 From 24269d9265ee2e3f53ca9f4bdbb01c79470988df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:38:19 +0200 Subject: Release v2.5.2 --- docs/changelog.rst | 9 +++++++++ factory/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0d12cb3..8f63567 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.5.2: + +2.5.2 (2015-04-21) +------------------ + +*Bugfix:* + + - Add support for Django 1.7/1.8 + - Add support for mongoengine>=0.9.0 / pymongo>=2.1 .. _v2.5.1: diff --git a/factory/__init__.py b/factory/__init__.py index 378035f..ea8c459 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.5.1' +__version__ = '2.5.2' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 9b8ad9be6f3b033e1e3673e4329ac63ba9fa07d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:43:56 +0200 Subject: README: Remove duplicate "latest release" block. --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9ffa809..9492c67 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ factory_boy =========== .. image:: https://pypip.in/version/factory_boy/badge.svg - :target: https://pypi.python.org/pypi/factory_boy/ + :target: http://factoryboy.readthedocs.org/en/latest/changelog.html :alt: Latest Version .. image:: https://pypip.in/py_versions/factory_boy/badge.svg @@ -16,8 +16,6 @@ factory_boy .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ -Latest release: `2.5.0 `_ (includes breaking changes, see the `ChangeLog `_) - factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures -- cgit v1.2.3 From 0e3cdffac41250cddfe93388b1c9fc1547e77a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 25 Apr 2015 17:50:50 +0200 Subject: Clarify .build() issue with Django>1.8 (Ref #198). From 1.8 onwards, this crashes: >>> a = MyModel() # Don't save >>> b = MyOtherModel(fkey_to_mymodel=a) In turn, it breaks: class MyModelFactory(factory.django.DjangoModelFactory): class Meta: model = MyModel class MyOtherModelFactory(factory.django.DjangoModelFactory): class Meta: model = MyOtherModel fkey_to_mymodel = factory.SubFactory(MyModelFactory) MyOtherModelFactory.build() # Breaks The error message is: Cannot assign "MyModel()": "MyModel" instance isn't saved in the database. See https://code.djangoproject.com/ticket/10811 for details. --- docs/orms.rst | 8 ++++++++ tests/djapp/models.py | 9 +++++++++ tests/test_django.py | 26 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index 5105e66..bbe91e6 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -40,6 +40,14 @@ All factories for a Django :class:`~django.db.models.Model` should use the once all post-generation hooks have run. +.. note:: Starting with Django 1.8, it is no longer possible to call ``.build()`` + on a factory if this factory uses a :class:`~factory.SubFactory` pointing + to another model: Django refuses to set a :class:`~djang.db.models.ForeignKey` + to an unsaved :class:`~django.db.models.Model` instance. + + See https://code.djangoproject.com/ticket/10811 for details. + + .. class:: DjangoOptions(factory.base.FactoryOptions) The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters: diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 513c47c..68b9709 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -68,6 +68,15 @@ class StandardSon(StandardModel): pass +class PointedModel(models.Model): + foo = models.CharField(max_length=20) + + +class PointingModel(models.Model): + foo = models.CharField(max_length=20) + pointed = models.OneToOneField(PointedModel, related_name='pointer', null=True) + + WITHFILE_UPLOAD_TO = 'django' WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO) diff --git a/tests/test_django.py b/tests/test_django.py index 113caeb..33d159d 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -361,6 +361,32 @@ class DjangoAbstractBaseSequenceTestCase(django_test.TestCase): self.assertEqual(1, obj.pk) +@unittest.skipIf(django is None, "Django not installed.") +class DjangoRelatedFieldTestCase(django_test.TestCase): + + @classmethod + def setUpClass(cls): + super(DjangoRelatedFieldTestCase, cls).setUpClass() + class PointedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointedModel + foo = 'ahah' + + class PointerFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointingModel + pointed = factory.SubFactory(PointedFactory, foo='hihi') + foo = 'bar' + + cls.PointedFactory = PointedFactory + cls.PointerFactory = PointerFactory + + def test_direct_related_create(self): + ptr = self.PointerFactory() + self.assertEqual('hihi', ptr.pointed.foo) + self.assertEqual(ptr.pointed, models.PointedModel.objects.get()) + + @unittest.skipIf(django is None, "Django not installed.") class DjangoFileFieldTestCase(unittest.TestCase): -- cgit v1.2.3 From bb7939b061f468f977caba8e5fdaaff62096e7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 15:29:18 +0200 Subject: Simplify dependencies installation for multi-version. You may now use: ``make DJANGO_VERSION=1.7 test``. Valid options: * ``DJANGO_VERSION`` * ``MONGOENGINE_VERSION`` * ``ALCHEMY_VERSION`` --- .gitignore | 1 + .travis.yml | 4 +--- Makefile | 31 ++++++++++++++++++++++++++++--- dev_requirements.txt | 2 +- requirements.txt | 0 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index b4d25fc..5437c43 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Build-related files docs/_build/ +auto_dev_requirements*.txt .coverage .tox *.egg-info diff --git a/.travis.yml b/.travis.yml index a1d14f6..ed331d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,7 @@ script: - python setup.py test install: - - pip install Django - - pip install sqlalchemy --use-mirrors - - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi + - make install-deps notifications: email: false diff --git a/Makefile b/Makefile index bb0428b..9841b31 100644 --- a/Makefile +++ b/Makefile @@ -5,26 +5,51 @@ DOC_DIR=docs # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) +# Dependencies +DJANGO_VERSION ?= 1.8 +NEXT_DJANGO_VERSION = $(shell python -c "v='$(DJANGO_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") + +ALCHEMY_VERSION ?= 1.0 +NEXT_ALCHEMY_VERSION = $(shell python -c "v='$(ALCHEMY_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") + +MONGOENGINE_VERSION ?= 0.9 +NEXT_MONGOENGINE_VERSION = $(shell python -c "v='$(MONGOENGINE_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") + +REQ_FILE = auto_dev_requirements_django$(DJANGO_VERSION)_alchemy$(ALCHEMY_VERSION)_mongoengine$(MONGOENGINE_VERSION).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 + grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@ + echo "Django>=$(DJANGO_VERSION),<$(NEXT_DJANGO_VERSION)" >> $@ + echo "SQLAlchemy>=$(ALCHEMY_VERSION),<$(NEXT_ALCHEMY_VERSION)" >> $@ + echo "mongoengine>=$(MONGOENGINE_VERSION),<$(NEXT_MONGOENGINE_VERSION)" >> $@ + + 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: +test: install-deps python -W default setup.py test pylint: pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ -coverage: +coverage: install-deps $(COVERAGE) erase $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" @@ -34,4 +59,4 @@ doc: $(MAKE) -C $(DOC_DIR) html -.PHONY: all default clean coverage doc pylint test +.PHONY: all default clean coverage doc install-deps pylint test diff --git a/dev_requirements.txt b/dev_requirements.txt index a8dd896..7c29185 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,7 +1,7 @@ coverage Django Pillow -sqlalchemy +SQLAlchemy mongoengine mock wheel diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 -- cgit v1.2.3 From 526293fccdc2661d6b0d68e524dc32aa858a3435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 15:30:14 +0200 Subject: Fix test startup for Django==1.6 --- tests/__init__.py | 4 +++- tests/test_django.py | 40 +++++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index c73165f..dc1a119 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) 2011-2015 Raphaël Barrois +# factory.django needs a configured Django. +from .test_django import * + from .test_base import * from .test_containers import * from .test_declarations import * -from .test_django import * from .test_fuzzy import * from .test_helpers import * from .test_using import * diff --git a/tests/test_django.py b/tests/test_django.py index 33d159d..2cfb55c 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -22,31 +22,13 @@ import os -import factory -import factory.django - try: import django except ImportError: # pragma: no cover django = None -try: - from PIL import Image -except ImportError: # pragma: no cover - # Try PIL alternate name - try: - import Image - except ImportError: - # OK, not installed - Image = None - - -from .compat import is_python2, unittest, mock -from . import testdata -from . import tools - - +# Setup Django as soon as possible if django is not None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') @@ -67,6 +49,26 @@ else: django_test = unittest + +try: + from PIL import Image +except ImportError: # pragma: no cover + # Try PIL alternate name + try: + import Image + except ImportError: + # OK, not installed + Image = None + + +import factory +import factory.django + +from .compat import is_python2, unittest, mock +from . import testdata +from . import tools + + test_state = {} -- cgit v1.2.3 From d0de4c4bbc8d495f0dc6d4023f096e00118b3d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 15:43:19 +0200 Subject: Update testing instructions. --- README.rst | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 9492c67..ac1986c 100644 --- a/README.rst +++ b/README.rst @@ -311,20 +311,28 @@ All pull request should pass the test suite, which can be launched simply with: .. code-block:: sh - $ python setup.py test + $ make test -.. note:: - Running test requires the unittest2 (standard in Python 2.7+) and mock libraries. +In order to test coverage, please use: + +.. code-block:: sh + $ make coverage -In order to test coverage, please use: + +To test with a specific framework version, you may use: .. code-block:: sh - $ pip install coverage - $ coverage erase; coverage run --branch setup.py test; coverage report + $ make DJANGO_VERSION=1.7 test + +Valid options are: + +* ``DJANGO_VERSION`` for ``Django`` +* ``MONGOENGINE_VERSION`` for ``mongoengine`` +* ``ALCHEMY_VERSION`` for ``SQLAlchemy`` Contents, indices and tables -- cgit v1.2.3 From e95475d492ea4e08ebc9b99e1851861df1eb83c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 16:15:54 +0200 Subject: Simpler way to define version names. Avoid hitting bugs with max shebang line length in jenkins. --- Makefile | 20 ++++++++++---------- README.rst | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 9841b31..8883015 100644 --- a/Makefile +++ b/Makefile @@ -6,16 +6,16 @@ DOC_DIR=docs COVERAGE = python $(shell which coverage) # Dependencies -DJANGO_VERSION ?= 1.8 -NEXT_DJANGO_VERSION = $(shell python -c "v='$(DJANGO_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") +DJANGO ?= 1.8 +NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -ALCHEMY_VERSION ?= 1.0 -NEXT_ALCHEMY_VERSION = $(shell python -c "v='$(ALCHEMY_VERSION)'; 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_VERSION ?= 0.9 -NEXT_MONGOENGINE_VERSION = $(shell python -c "v='$(MONGOENGINE_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") +MONGOENGINE ?= 0.9 +NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -REQ_FILE = auto_dev_requirements_django$(DJANGO_VERSION)_alchemy$(ALCHEMY_VERSION)_mongoengine$(MONGOENGINE_VERSION).txt +REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt all: default @@ -30,9 +30,9 @@ install-deps: $(REQ_FILE) $(REQ_FILE): dev_requirements.txt requirements.txt grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@ - echo "Django>=$(DJANGO_VERSION),<$(NEXT_DJANGO_VERSION)" >> $@ - echo "SQLAlchemy>=$(ALCHEMY_VERSION),<$(NEXT_ALCHEMY_VERSION)" >> $@ - echo "mongoengine>=$(MONGOENGINE_VERSION),<$(NEXT_MONGOENGINE_VERSION)" >> $@ + echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@ + echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@ + echo "mongoengine>=$(MONGOENGINE),<$(NEXT_MONGOENGINE)" >> $@ clean: diff --git a/README.rst b/README.rst index ac1986c..59ea0cf 100644 --- a/README.rst +++ b/README.rst @@ -326,13 +326,13 @@ To test with a specific framework version, you may use: .. code-block:: sh - $ make DJANGO_VERSION=1.7 test + $ make DJANGO=1.7 test Valid options are: -* ``DJANGO_VERSION`` for ``Django`` -* ``MONGOENGINE_VERSION`` for ``mongoengine`` -* ``ALCHEMY_VERSION`` for ``SQLAlchemy`` +* ``DJANGO`` for ``Django`` +* ``MONGOENGINE`` for ``mongoengine`` +* ``ALCHEMY`` for ``SQLAlchemy`` Contents, indices and tables -- cgit v1.2.3 From 29de94f46b356bef181e8cf02d6cb3ae4ac52075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 16:39:10 +0200 Subject: Allow skipping Mongo tests. --- .travis.yml | 2 +- tests/test_mongoengine.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ed331d4..e1600bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - "pypy" script: - - python setup.py test + - SKIP_MONGOENGINE=1 python setup.py test install: - make install-deps diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 6fa4125..7badd43 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -31,6 +31,9 @@ try: except ImportError: mongoengine = None +if os.environ.get('SKIP_MONGOENGINE') == 1: + mongoengine = None + if mongoengine: from factory.mongoengine import MongoEngineFactory -- cgit v1.2.3 From 536ac1b0fe7c4a04ad144022d6394b994feccdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 16:48:30 +0200 Subject: Fix typo. --- tests/test_mongoengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 7badd43..148d274 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -31,7 +31,7 @@ try: except ImportError: mongoengine = None -if os.environ.get('SKIP_MONGOENGINE') == 1: +if os.environ.get('SKIP_MONGOENGINE') == '1': mongoengine = None if mongoengine: -- cgit v1.2.3 From fa6d60d17ddb7b70c6bc2337d901ef8cc924e67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 20 May 2015 23:24:45 +0200 Subject: Add Meta.rename to handle name conflicts (See #206). Define ``Meta.rename = {'attrs': 'attributes'}`` if your model expects a ``attributes`` kwarg but you can't define it since it's already reserved by the ``Factory`` class. --- docs/changelog.rst | 9 +++++++++ docs/reference.rst | 23 ++++++++++++++++++++++- factory/base.py | 8 ++++++++ tests/test_using.py | 15 +++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8f63567..cd5d281 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.6.0: + +2.6.0 (XXXX-XX-XX) +------------------ + +*New:* + + - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) + .. _v2.5.2: 2.5.2 (2015-04-21) diff --git a/docs/reference.rst b/docs/reference.rst index 44f78b6..0705ca2 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -106,6 +106,28 @@ The :class:`Factory` class .. versionadded:: 2.4.0 + .. attribute:: rename + + Sometimes, a model expect a field with a name already used by one + of :class:`Factory`'s methods. + + In this case, the :attr:`rename` attributes allows to define renaming + rules: the keys of the :attr:`rename` dict are those used in the + :class:`Factory` declarations, and their values the new name: + + .. code-block:: python + + class ImageFactory(factory.Factory): + # The model expects "attributes" + form_attributes = ['thumbnail', 'black-and-white'] + + class Meta: + model = Image + rename = {'form_attributes': 'attributes'} + + .. versionadded: 2.6.0 + + .. attribute:: strategy Use this attribute to change the strategy used by a :class:`Factory`. @@ -229,7 +251,6 @@ The :class:`Factory` class .. OHAI_VIM** - .. classmethod:: _setup_next_sequence(cls) This method will compute the first value to use for the sequence counter diff --git a/factory/base.py b/factory/base.py index d48edd5..0f2af59 100644 --- a/factory/base.py +++ b/factory/base.py @@ -176,6 +176,7 @@ class FactoryOptions(object): OptionDefault('strategy', CREATE_STRATEGY, inherit=True), OptionDefault('inline_args', (), inherit=True), OptionDefault('exclude', (), inherit=True), + OptionDefault('rename', {}, inherit=True), ] def _fill_from_meta(self, meta, base_meta): @@ -412,6 +413,12 @@ class BaseFactory(object): decls.update(extra_defs or {}) return decls + @classmethod + def _rename_fields(cls, **kwargs): + for old_name, new_name in cls._meta.rename.items(): + kwargs[new_name] = kwargs.pop(old_name) + return kwargs + @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" @@ -441,6 +448,7 @@ class BaseFactory(object): **kwargs: arguments to pass to the creation function """ model_class = cls._get_model_class() + kwargs = cls._rename_fields(**kwargs) kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. diff --git a/tests/test_using.py b/tests/test_using.py index b7fea81..6d75531 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1076,6 +1076,21 @@ class KwargAdjustTestCase(unittest.TestCase): self.assertEqual({'x': 1, 'y': 2, 'z': 3, 'foo': 3}, obj.kwargs) self.assertEqual((), obj.args) + def test_rename(self): + class TestObject(object): + def __init__(self, attributes=None): + self.attributes = attributes + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + rename = {'attributes_': 'attributes'} + + attributes_ = 42 + + obj = TestObjectFactory.build() + self.assertEqual(42, obj.attributes) + class SubFactoryTestCase(unittest.TestCase): def test_sub_factory(self): -- cgit v1.2.3 From 939796a915d66722b0c3a286a12c88757d4eb137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 20 May 2015 23:32:33 +0200 Subject: Fix typo in docs/fuzzy (Closes #207). Thanks to @nikolas for spotting it! --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 18978e4..af5c490 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -368,6 +368,6 @@ and provides a few helpers for this: Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD** -use :obj:`factory.fuzzy._random` foras a randomness source; this ensures that +use :obj:`factory.fuzzy._random` as a randomness source; this ensures that data they generate can be regenerated using the simple state from :meth:`get_random_state`. -- cgit v1.2.3 From da8d2e6323014be6065a9a490754173859fb95b4 Mon Sep 17 00:00:00 2001 From: Pauly Fenwar Date: Wed, 6 May 2015 13:54:31 +0100 Subject: Update README.rst - "attributes" is not a strategy (Closes #204). The wording of the readme suggested that "attributes" is a strategy just like "build" and "create", but this is not the case in the implementation (for example keyword arguments do not work, SubFactory fields don't behave as expected), so I have removed the mention of this and replaced the attributes example to mention the "stub" strategy. --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 59ea0cf..991180f 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ Its main features include: - Straightforward declarative syntax - Chaining factory calls while retaining the global context -- Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects) +- Support for multiple build strategies (saved/unsaved instances, stubbed objects) - Multiple factories per class support, including inheritance @@ -135,7 +135,7 @@ The class of the object must be defined in the ``model`` field of a ``class Meta Using factories """"""""""""""" -factory_boy supports several different build strategies: build, create, attributes and stub: +factory_boy supports several different build strategies: build, create, and stub: .. code-block:: python @@ -145,8 +145,8 @@ factory_boy supports several different build strategies: build, create, attribut # Returns a saved User instance user = UserFactory.create() - # Returns a dict of attributes that can be used to build a User instance - attributes = UserFactory.attributes() + # Returns a stub object (just a bunch of attributes) + obj = UserFactory.stub() You can use the Factory class as a shortcut for the default build strategy: -- cgit v1.2.3 From 6f37f9be2d2e1bc75340068911db18b2bbcbe722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 22 May 2015 20:24:13 +0200 Subject: Add factory.Faker() This relies on the ``fake-factory`` library, and provides realistic random values for most field types. --- README.rst | 22 +++++++++++ dev_requirements.txt | 2 + docs/changelog.rst | 2 + docs/reference.rst | 60 +++++++++++++++++++++++++++++ factory/__init__.py | 1 + factory/faker.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++ requirement.txt | 1 + setup.py | 3 ++ tests/__init__.py | 1 + tests/test_faker.py | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 293 insertions(+) create mode 100644 factory/faker.py create mode 100644 requirement.txt create mode 100644 tests/test_faker.py diff --git a/README.rst b/README.rst index 991180f..9b82406 100644 --- a/README.rst +++ b/README.rst @@ -177,6 +177,28 @@ It is also possible to create a bunch of objects in a single call: >>> [user.first_name for user in users] ["Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe"] + +Realistic, random values +"""""""""""""""""""""""" + +Tests look better with random yet realistic values. +For this, factory_boy relies on the excellent `fake-factory `_ library: + +.. code-block:: python + + class RandomUserFactory(factory.Factory): + class Meta: + model = models.User + + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + +.. code-block:: pycon + + >>> UserFactory() + + + Lazy Attributes """"""""""""""" diff --git a/dev_requirements.txt b/dev_requirements.txt index 7c29185..d55129a 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,3 +1,5 @@ +-r requirements.txt + coverage Django Pillow diff --git a/docs/changelog.rst b/docs/changelog.rst index cd5d281..886db0b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,8 @@ ChangeLog *New:* - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) + - Add support for random-yet-realistic values through `fake-factory `_, + through the :class:`factory.Faker` class. .. _v2.5.2: diff --git a/docs/reference.rst b/docs/reference.rst index 0705ca2..a168de5 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -474,6 +474,66 @@ factory_boy supports two main strategies for generating instances, plus stubs. Declarations ------------ + +Faker +""""" + +.. class:: Faker(provider, locale=None, **kwargs) + + .. OHAIVIM** + + In order to easily define realistic-looking factories, + use the :class:`Faker` attribute declaration. + + This is a wrapper around `fake-factory `_; + its argument is the name of a ``fake-factory`` provider: + + .. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + first_name = factory.Faker('name') + + .. code-block:: pycon + + >>> user = UserFactory() + >>> user.name + 'Lucy Cechtelar' + + + .. attribute:: locale + + If a custom locale is required for one specific field, + use the ``locale`` parameter: + + .. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + first_name = factory.Faker('name', locale='fr_FR') + + .. code-block:: pycon + + >>> user = UserFactory() + >>> user.name + 'Jean Valjean' + + + .. classmethod:: override_default_locale(cls, locale) + + If the locale needs to be overridden for a whole test, + use :meth:`~factory.Faker.override_default_locale`: + + .. code-block:: pycon + + >>> with factory.Faker.override_default_locale('de_DE'): + ... UserFactory() + + LazyAttribute """"""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index ea8c459..80eb6a8 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -43,6 +43,7 @@ from .base import ( # Backward compatibility; this should be removed soon. from .mogo import MogoFactory from .django import DjangoModelFactory +from .faker import Faker from .declarations import ( LazyAttribute, diff --git a/factory/faker.py b/factory/faker.py new file mode 100644 index 0000000..10a0cba --- /dev/null +++ b/factory/faker.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# 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. + + +"""Additional declarations for "faker" attributes. + +Usage: + + class MyFactory(factory.Factory): + class Meta: + model = MyProfile + + first_name = factory.Faker('name') +""" + + +from __future__ import absolute_import +from __future__ import unicode_literals + +import contextlib + +import faker +import faker.config + +from . import declarations + +class Faker(declarations.OrderedDeclaration): + """Wrapper for 'faker' values. + + Args: + provider (str): the name of the Faker field + locale (str): the locale to use for the faker + + All other kwargs will be passed to the underlying provider + (e.g ``factory.Faker('ean', length=10)`` + calls ``faker.Faker.ean(length=10)``) + + Usage: + >>> foo = factory.Faker('name') + """ + def __init__(self, provider, locale=None, **kwargs): + self.provider = provider + self.provider_kwargs = kwargs + self.locale = locale + + def generate(self, extra_kwargs): + kwargs = {} + kwargs.update(self.provider_kwargs) + kwargs.update(extra_kwargs) + faker = self._get_faker(self.locale) + return faker.format(self.provider, **kwargs) + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + return self.generate(extra or {}) + + _FAKER_REGISTRY = {} + _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE + + @classmethod + @contextlib.contextmanager + def override_default_locale(cls, locale): + old_locale = cls._DEFAULT_LOCALE + cls._DEFAULT_LOCALE = locale + try: + yield + finally: + cls._DEFAULT_LOCALE = old_locale + + @classmethod + def _get_faker(cls, locale=None): + if locale is None: + locale = cls._DEFAULT_LOCALE + + if locale not in cls._FAKER_REGISTRY: + cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale) + + return cls._FAKER_REGISTRY[locale] diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000..bd2a4a6 --- /dev/null +++ b/requirement.txt @@ -0,0 +1 @@ +fake-factory>=0.5.0 diff --git a/setup.py b/setup.py index daee69b..942fa2c 100755 --- a/setup.py +++ b/setup.py @@ -47,6 +47,9 @@ setup( setup_requires=[ 'setuptools>=0.8', ], + install_requires=[ + 'fake-factory>=0.5.0', + ], tests_require=[ #'mock', ], diff --git a/tests/__init__.py b/tests/__init__.py index dc1a119..b2c772d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,7 @@ from .test_django import * from .test_base import * from .test_containers import * from .test_declarations import * +from .test_faker import * from .test_fuzzy import * from .test_helpers import * from .test_using import * diff --git a/tests/test_faker.py b/tests/test_faker.py new file mode 100644 index 0000000..41f8e19 --- /dev/null +++ b/tests/test_faker.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# 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. + + +import factory +import unittest + + +class MockFaker(object): + def __init__(self, expected): + self.expected = expected + + def format(self, provider, **kwargs): + return self.expected[provider] + + +class FakerTests(unittest.TestCase): + def setUp(self): + self._real_fakers = factory.Faker._FAKER_REGISTRY + factory.Faker._FAKER_REGISTRY = {} + + def tearDown(self): + factory.Faker._FAKER_REGISTRY = self._real_fakers + + def _setup_mock_faker(self, locale=None, **definitions): + if locale is None: + locale = factory.Faker._DEFAULT_LOCALE + factory.Faker._FAKER_REGISTRY[locale] = MockFaker(definitions) + + def test_simple_biased(self): + self._setup_mock_faker(name="John Doe") + faker_field = factory.Faker('name') + self.assertEqual("John Doe", faker_field.generate({})) + + def test_full_factory(self): + class Profile(object): + def __init__(self, first_name, last_name, email): + self.first_name = first_name + self.last_name = last_name + self.email = email + + class ProfileFactory(factory.Factory): + class Meta: + model = Profile + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name', locale='fr_FR') + email = factory.Faker('email') + + self._setup_mock_faker(first_name="John", last_name="Doe", email="john.doe@example.org") + self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@exemple.fr", locale='fr_FR') + + profile = ProfileFactory() + self.assertEqual("John", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + self.assertEqual('john.doe@example.org', profile.email) + + def test_override_locale(self): + class Profile(object): + def __init__(self, first_name, last_name): + self.first_name = first_name + self.last_name = last_name + + class ProfileFactory(factory.Factory): + class Meta: + model = Profile + + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name', locale='fr_FR') + + self._setup_mock_faker(first_name="John", last_name="Doe") + self._setup_mock_faker(first_name="Jean", last_name="Valjean", locale='fr_FR') + self._setup_mock_faker(first_name="Johannes", last_name="Brahms", locale='de_DE') + + profile = ProfileFactory() + self.assertEqual("John", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + + with factory.Faker.override_default_locale('de_DE'): + profile = ProfileFactory() + self.assertEqual("Johannes", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + + profile = ProfileFactory() + self.assertEqual("John", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + -- cgit v1.2.3 From ebc89520d3f7589da35d4e7b78637fbe7d4d664a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 24 May 2015 18:21:04 +0200 Subject: Add lazy loading to factory.Iterator. factory.Iterator no longers begins iteration of its argument on declaration, since this behavior may trigger database query when that argument is, for instance, a Django queryset. The ``factory.Iterator``'s argument will only be called when the containing ``Factory`` is first evaluated; this means that factories using ``factory.Iterator(models.MyThingy.objects.all())`` will no longer call the database at import time. --- docs/changelog.rst | 3 +++ docs/recipes.rst | 23 +++++++++++++++++++++++ factory/declarations.py | 11 +++++++++-- setup.py | 8 ++++---- tests/test_django.py | 2 +- tests/test_using.py | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 886db0b..0cbd4af 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,9 @@ ChangeLog - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) - Add support for random-yet-realistic values through `fake-factory `_, through the :class:`factory.Faker` class. + - :class:`factory.Iterator` no longer begins iteration of its argument at import time, + thus allowing to pass in a lazy iterator such as a Django queryset + (i.e ``factory.Iterator(models.MyThingy.objects.all())``). .. _v2.5.2: diff --git a/docs/recipes.rst b/docs/recipes.rst index 70eca46..3cbe6d2 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -33,6 +33,29 @@ use the :class:`~factory.SubFactory` declaration: group = factory.SubFactory(GroupFactory) +Choosing from a populated table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the target of the :class:`~django.db.models.ForeignKey` should be +chosen from a pre-populated table +(e.g :class:`django.contrib.contenttypes.models.ContentType`), +simply use a :class:`factory.Iterator` on the chosen queryset: + +.. code-block:: python + + import factory, factory.django + from . import models + + class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.User + + language = factory.Iterator(models.Language.objects.all()) + +Here, ``models.Language.objects.all()`` won't be evaluated until the +first call to ``UserFactory``; thus avoiding DB queries at import time. + + Reverse dependencies (reverse ForeignKey) ----------------------------------------- diff --git a/factory/declarations.py b/factory/declarations.py index 8f2314a..f0dbfe5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -160,12 +160,19 @@ class Iterator(OrderedDeclaration): def __init__(self, iterator, cycle=True, getter=None): super(Iterator, self).__init__() self.getter = getter + self.iterator = None if cycle: - iterator = itertools.cycle(iterator) - self.iterator = utils.ResetableIterator(iterator) + self.iterator_builder = lambda: utils.ResetableIterator(itertools.cycle(iterator)) + else: + self.iterator_builder = lambda: utils.ResetableIterator(iterator) def evaluate(self, sequence, obj, create, extra=None, containers=()): + # Begin unrolling as late as possible. + # This helps with ResetableIterator(MyModel.objects.all()) + if self.iterator is None: + self.iterator = self.iterator_builder() + logger.debug("Iterator: Fetching next value from %r", self.iterator) value = next(iter(self.iterator)) if self.getter is None: diff --git a/setup.py b/setup.py index 942fa2c..8ca7e4b 100755 --- a/setup.py +++ b/setup.py @@ -44,12 +44,12 @@ setup( keywords=['factory_boy', 'factory', 'fixtures'], packages=['factory'], license='MIT', - setup_requires=[ - 'setuptools>=0.8', - ], install_requires=[ 'fake-factory>=0.5.0', ], + setup_requires=[ + 'setuptools>=0.8', + ], tests_require=[ #'mock', ], @@ -69,7 +69,7 @@ setup( "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Libraries :: Python Modules" + "Topic :: Software Development :: Libraries :: Python Modules", ], test_suite='tests', test_loader=test_loader, diff --git a/tests/test_django.py b/tests/test_django.py index 2cfb55c..bde8efe 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -21,6 +21,7 @@ """Tests for factory_boy/Django interactions.""" import os +from .compat import is_python2, unittest, mock try: @@ -64,7 +65,6 @@ except ImportError: # pragma: no cover import factory import factory.django -from .compat import is_python2, unittest, mock from . import testdata from . import tools diff --git a/tests/test_using.py b/tests/test_using.py index 6d75531..c7d2b85 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1493,6 +1493,38 @@ class IteratorTestCase(unittest.TestCase): for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) + def test_iterator_late_loading(self): + """Ensure that Iterator doesn't unroll on class creation. + + This allows, for Django objects, to call: + foo = factory.Iterator(models.MyThingy.objects.all()) + """ + class DBRequest(object): + def __init__(self): + self.ready = False + + def __iter__(self): + if not self.ready: + raise ValueError("Not ready!!") + return iter([1, 2, 3]) + + # calling __iter__() should crash + req1 = DBRequest() + with self.assertRaises(ValueError): + iter(req1) + + req2 = DBRequest() + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = factory.Iterator(req2) + + req2.ready = True + obj = TestObjectFactory() + self.assertEqual(1, obj.one) + class BetterFakeModelManager(object): def __init__(self, keys, instance): -- cgit v1.2.3 From e9851a7d51afffea2a5679934ad6284c0835cfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 31 May 2015 10:15:27 +0100 Subject: Docs: fix minor typo. As spotted by @proofit404 --- docs/reference.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index a168de5..4c7f8f7 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -494,7 +494,7 @@ Faker class Meta: model = User - first_name = factory.Faker('name') + name = factory.Faker('name') .. code-block:: pycon @@ -514,7 +514,7 @@ Faker class Meta: model = User - first_name = factory.Faker('name', locale='fr_FR') + name = factory.Faker('name', locale='fr_FR') .. code-block:: pycon -- cgit v1.2.3 From 0b5270eab393fad20faa7a6a9720af18c97b1773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 31 May 2015 10:47:28 +0100 Subject: Properly handle custom Django managers (Closes #201). The actual behavior of Django with custom managers and inherited abstract models is rather complex, so this had to be adapted to the actual Django source code. --- docs/changelog.rst | 4 ++++ factory/django.py | 9 ++++++++- tests/djapp/models.py | 11 +++++++++++ tests/test_django.py | 13 ++++++++++++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0cbd4af..c871ce8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,10 @@ ChangeLog thus allowing to pass in a lazy iterator such as a Django queryset (i.e ``factory.Iterator(models.MyThingy.objects.all())``). +*Bugfix:* + + - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. + .. _v2.5.2: 2.5.2 (2015-04-21) diff --git a/factory/django.py b/factory/django.py index 74e4fdb..e4a3ea7 100644 --- a/factory/django.py +++ b/factory/django.py @@ -115,7 +115,14 @@ class DjangoModelFactory(base.Factory): if model_class is None: raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) - manager = model_class.objects + + try: + manager = model_class.objects + except AttributeError: + # When inheriting from an abstract model with a custom + # manager, the class has no 'objects' field. + manager = model_class._default_manager + if cls._meta.database != DEFAULT_DB_ALIAS: manager = manager.using(cls._meta.database) return manager diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 68b9709..cadefbc 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -110,3 +110,14 @@ class WithCustomManager(models.Model): foo = models.CharField(max_length=20) objects = CustomManager() + + +class AbstractWithCustomManager(models.Model): + custom_objects = CustomManager() + + class Meta: + abstract = True + + +class FromAbstractWithCustomManager(AbstractWithCustomManager): + pass diff --git a/tests/test_django.py b/tests/test_django.py index bde8efe..b8e7ccb 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -755,10 +755,21 @@ class PreventSignalsTestCase(unittest.TestCase): self.assertSignalsReactivated() -class DjangoCustomManagerTestCase(django_test.TestCase): +@unittest.skipIf(django is None, "Django not installed.") +class DjangoCustomManagerTestCase(unittest.TestCase): def test_extra_args(self): + # Our CustomManager will remove the 'arg=' argument. model = WithCustomManagerFactory(arg='foo') + def test_with_manager_on_abstract(self): + class ObjFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.FromAbstractWithCustomManager + + # Our CustomManager will remove the 'arg=' argument, + # invalid for the actual model. + ObjFactory.create(arg='invalid') + if __name__ == '__main__': # pragma: no cover unittest.main() -- cgit v1.2.3 From 9246fa6d26ca655c02ae37bbfc389d9f34dfba16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 31 May 2015 10:57:53 +0100 Subject: Improve ORM layer import paths (Closes #186). You may now use the following code: import factory factory.alchemy.SQLAlchemyModelFactory factory.django.DjangoModelFactory factory.mongoengine.MongoEngineFactory --- docs/changelog.rst | 2 ++ docs/orms.rst | 31 ++++++++++++++++++++++++++++++- factory/__init__.py | 12 +++++++++--- factory/alchemy.py | 1 - 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c871ce8..eea38c5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,8 @@ ChangeLog - :class:`factory.Iterator` no longer begins iteration of its argument at import time, thus allowing to pass in a lazy iterator such as a Django queryset (i.e ``factory.Iterator(models.MyThingy.objects.all())``). + - Simplify imports for ORM layers, now available through a simple ``factory`` import, + at ``factory.alchemy.SQLAlchemyModelFactory`` / ``factory.django.DjangoModelFactory`` / ``factory.mongoengine.MongoEngineFactory``. *Bugfix:* diff --git a/docs/orms.rst b/docs/orms.rst index bbe91e6..26390b5 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -273,6 +273,34 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document. +A minimalist example: + +.. code-block:: python + + import mongoengine + + class Address(mongoengine.EmbeddedDocument): + street = mongoengine.StringField() + + class Person(mongoengine.Document): + name = mongoengine.StringField() + address = mongoengine.EmbeddedDocumentField(Address) + + import factory + + class AddressFactory(factory.mongoengine.MongoEngineFactory): + class Meta: + model = Address + + street = factory.Sequence(lambda n: 'street%d' % n) + + class PersonFactory(factory.mongoengine.MongoEngineFactory): + class Meta: + model = Person + + name = factory.Sequence(lambda n: 'name%d' % n) + address = factory.SubFactory(AddressFactory) + SQLAlchemy ---------- @@ -327,8 +355,9 @@ A (very) simple example: Base.metadata.create_all(engine) + import factory - class UserFactory(SQLAlchemyModelFactory): + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = User sqlalchemy_session = session # the SQLAlchemy session object diff --git a/factory/__init__.py b/factory/__init__.py index 80eb6a8..843cf99 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -40,9 +40,6 @@ from .base import ( use_strategy, ) -# Backward compatibility; this should be removed soon. -from .mogo import MogoFactory -from .django import DjangoModelFactory from .faker import Faker from .declarations import ( @@ -84,3 +81,12 @@ from .helpers import ( post_generation, ) +# Backward compatibility; this should be removed soon. +from . import alchemy +from . import django +from . import mogo +from . import mongoengine + +MogoFactory = mogo.MogoFactory +DjangoModelFactory = django.DjangoModelFactory + diff --git a/factory/alchemy.py b/factory/alchemy.py index 6408393..20da6cf 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -19,7 +19,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import unicode_literals -from sqlalchemy.sql.functions import max from . import base -- cgit v1.2.3 From 5114549520e95c658716b7cbe9a7ab333d8ca524 Mon Sep 17 00:00:00 2001 From: Peter Marsh Date: Tue, 30 Jun 2015 17:32:41 +0100 Subject: Remove requirement.txt, move content into requirements.txt requirement.txt was introduced in 6f37f9b, after requirements.txt had already put in place. dev_requirements.txt installs the contents of requirements.txt (which is empty) while a single dependency is specified in requirement.txt. It looks like requirement.txt was added accidently and it's content should always have been in requirements.txt. This removes requirement.txt and puts the dependency delcared in there in requirements.txt. --- requirement.txt | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 requirement.txt diff --git a/requirement.txt b/requirement.txt deleted file mode 100644 index bd2a4a6..0000000 --- a/requirement.txt +++ /dev/null @@ -1 +0,0 @@ -fake-factory>=0.5.0 diff --git a/requirements.txt b/requirements.txt index e69de29..bd2a4a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +fake-factory>=0.5.0 -- cgit v1.2.3 From d471c1b4b0d4b06d557b5b6a9349a7dc55515d69 Mon Sep 17 00:00:00 2001 From: Ilya Baryshev Date: Thu, 2 Jul 2015 23:44:37 +0300 Subject: Fix mute_signals behavior for signals with caching Connecting signals (with use_caching=True) inside mute_signals was breaking unmute on exit. Paused receivers were not running. This was caused by signal cache not being restored after unpatching. Workaround is to clear signal cache on exit. Fixes #212 --- docs/changelog.rst | 1 + factory/django.py | 2 ++ tests/test_django.py | 13 +++++++++++++ 3 files changed, 16 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index eea38c5..01d5775 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,7 @@ ChangeLog *Bugfix:* - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. + - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching .. _v2.5.2: diff --git a/factory/django.py b/factory/django.py index e4a3ea7..5cc2b31 100644 --- a/factory/django.py +++ b/factory/django.py @@ -277,6 +277,8 @@ class mute_signals(object): receivers) signal.receivers = receivers + with signal.lock: + signal.sender_receivers_cache.clear() self.paused = {} def copy(self): diff --git a/tests/test_django.py b/tests/test_django.py index b8e7ccb..103df91 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -678,6 +678,19 @@ class PreventSignalsTestCase(unittest.TestCase): self.assertSignalsReactivated() + def test_signal_cache(self): + with factory.django.mute_signals(signals.pre_save, signals.post_save): + signals.post_save.connect(self.handlers.mute_block_receiver) + WithSignalsFactory() + + self.assertTrue(self.handlers.mute_block_receiver.call_count, 1) + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.assertSignalsReactivated() + self.assertTrue(self.handlers.mute_block_receiver.call_count, 1) + def test_class_decorator(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): -- cgit v1.2.3 From 6d190fa33f8a0cd625d3ce13d6de29bd5b72e742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 5 Jul 2015 17:00:21 +0200 Subject: Improve @coagulant's fixes to django signals (Closes #212). Signal caching didn't exist until Django 1.6. --- factory/django.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/factory/django.py b/factory/django.py index 5cc2b31..b2af12c 100644 --- a/factory/django.py +++ b/factory/django.py @@ -268,6 +268,9 @@ class mute_signals(object): logger.debug('mute_signals: Disabling signal handlers %r', signal.receivers) + # Note that we're using implementation details of + # django.signals, since arguments to signal.connect() + # are lost in signal.receivers self.paused[signal] = signal.receivers signal.receivers = [] @@ -277,8 +280,12 @@ class mute_signals(object): receivers) signal.receivers = receivers - with signal.lock: - signal.sender_receivers_cache.clear() + if django.VERSION[:2] >= (1, 6): + with signal.lock: + # Django uses some caching for its signals. + # Since we're bypassing signal.connect and signal.disconnect, + # we have to keep messing with django's internals. + signal.sender_receivers_cache.clear() self.paused = {} def copy(self): -- cgit v1.2.3 From 63edb526bc4efd8cf7abe260f2787f55d2953e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 11 Jul 2015 19:52:45 +0200 Subject: Improve debug logging efficiency (Closes #155). As suggested by @adamchainz, use lazy computation of args/kwargs pprint to only perform complex computation when running with debug. --- factory/utils.py | 32 +++++++++++++++++++++++--------- tests/test_utils.py | 14 +++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/factory/utils.py b/factory/utils.py index 6ecf9a7..806b1ec 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -110,15 +110,29 @@ def _safe_repr(obj): return obj_repr.decode('utf-8') -def log_pprint(args=(), kwargs=None): - kwargs = kwargs or {} - return ', '.join( - [_safe_repr(arg) for arg in args] + - [ - '%s=%s' % (key, _safe_repr(value)) - for key, value in kwargs.items() - ] - ) +class log_pprint(object): + """Helper for properly printing args / kwargs passed to an object. + + Since it is only used with factory.debug(), the computation is + performed lazily. + """ + __slots__ = ['args', 'kwargs'] + + def __init__(self, args=(), kwargs=None): + self.args = args + self.kwargs = kwargs or {} + + def __repr__(self): + return repr(str(self)) + + def __str__(self): + return ', '.join( + [_safe_repr(arg) for arg in self.args] + + [ + '%s=%s' % (key, _safe_repr(value)) + for key, value in self.kwargs.items() + ] + ) class ResetableIterator(object): diff --git a/tests/test_utils.py b/tests/test_utils.py index eed7a57..77598e1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -238,33 +238,33 @@ class ImportObjectTestCase(unittest.TestCase): class LogPPrintTestCase(unittest.TestCase): def test_nothing(self): - txt = utils.log_pprint() + txt = str(utils.log_pprint()) self.assertEqual('', txt) def test_only_args(self): - txt = utils.log_pprint((1, 2, 3)) + txt = str(utils.log_pprint((1, 2, 3))) self.assertEqual('1, 2, 3', txt) def test_only_kwargs(self): - txt = utils.log_pprint(kwargs={'a': 1, 'b': 2}) + txt = str(utils.log_pprint(kwargs={'a': 1, 'b': 2})) self.assertIn(txt, ['a=1, b=2', 'b=2, a=1']) def test_bytes_args(self): - txt = utils.log_pprint((b'\xe1\xe2',)) + txt = str(utils.log_pprint((b'\xe1\xe2',))) expected = "b'\\xe1\\xe2'" if is_python2: expected = expected.lstrip('b') self.assertEqual(expected, txt) def test_text_args(self): - txt = utils.log_pprint(('ŧêßŧ',)) + txt = str(utils.log_pprint(('ŧêßŧ',))) expected = "'ŧêßŧ'" if is_python2: expected = "u'\\u0167\\xea\\xdf\\u0167'" self.assertEqual(expected, txt) def test_bytes_kwargs(self): - txt = utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'}) + txt = str(utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'})) expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'" expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'" if is_python2: @@ -273,7 +273,7 @@ class LogPPrintTestCase(unittest.TestCase): self.assertIn(txt, (expected1, expected2)) def test_text_kwargs(self): - txt = utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'}) + txt = str(utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'})) expected1 = "x='ŧêßŧ', y='ŧßêŧ'" expected2 = "y='ŧßêŧ', x='ŧêßŧ'" if is_python2: -- cgit v1.2.3 From b0fbd24c69a155c4f9d58f5e4dab8209afeb3660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 15 Jul 2015 23:15:13 +0200 Subject: Add examples folder. This should contain examples of "using factory_boy with third-party frameworks". --- Makefile | 6 +++- dev_requirements.txt | 1 + examples/Makefile | 9 +++++ examples/flask_alchemy/demoapp.py | 55 +++++++++++++++++++++++++++++ examples/flask_alchemy/demoapp_factories.py | 27 ++++++++++++++ examples/flask_alchemy/requirements.txt | 3 ++ examples/flask_alchemy/test_demoapp.py | 35 ++++++++++++++++++ examples/requirements.txt | 1 + 8 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 examples/Makefile create mode 100644 examples/flask_alchemy/demoapp.py create mode 100644 examples/flask_alchemy/demoapp_factories.py create mode 100644 examples/flask_alchemy/requirements.txt create mode 100644 examples/flask_alchemy/test_demoapp.py create mode 100644 examples/requirements.txt diff --git a/Makefile b/Makefile index 8883015..79b5e82 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ PACKAGE=factory TESTS_DIR=tests DOC_DIR=docs +EXAMPLES_DIR=examples # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) @@ -43,9 +44,12 @@ clean: @rm -rf tmp_test/ -test: install-deps +test: install-deps example-test python -W default setup.py test +example-test: + $(MAKE) -C $(EXAMPLES_DIR) test + pylint: pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ diff --git a/dev_requirements.txt b/dev_requirements.txt index d55129a..22261a1 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,5 @@ -r requirements.txt +-r examples/requirements.txt coverage Django diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 0000000..6064a9b --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,9 @@ +EXAMPLES = flask_alchemy + +TEST_TARGETS = $(addprefix runtest-,$(EXAMPLES)) + +test: $(TEST_TARGETS) + + +$(TEST_TARGETS): runtest-%: + cd $* && PYTHONPATH=../.. python -m unittest diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py new file mode 100644 index 0000000..4ab42b0 --- /dev/null +++ b/examples/flask_alchemy/demoapp.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 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. + +from flask import Flask +from flask.ext.sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' +db = SQLAlchemy(app) + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True) + email = db.Column(db.String(120), unique=True) + + def __init__(self, username, email): + self.username = username + self.email = email + + def __repr__(self): + return '' % self.username + + +class UserLog(db.Model): + id = db.Column(db.Integer, primary_key=True) + message = db.Column(db.String(1000)) + + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', backref=db.backref('logs', lazy='dynamic')) + + def __init__(self, message, user): + self.message = message + self.user = user + + def __repr__(self): + return '' % (self.user, self.message) diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py new file mode 100644 index 0000000..6b71d04 --- /dev/null +++ b/examples/flask_alchemy/demoapp_factories.py @@ -0,0 +1,27 @@ +import factory +import factory.alchemy +import factory.fuzzy + +import demoapp + + +class BaseFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + abstract = True + sqlalchemy_session = demoapp.db.session + + +class UserFactory(BaseFactory): + class Meta: + model = demoapp.User + + username = factory.fuzzy.FuzzyText() + email = factory.fuzzy.FuzzyText() + + +class UserLogFactory(BaseFactory): + class Meta: + model = demoapp.UserLog + + message = factory.fuzzy.FuzzyText() + user = factory.SubFactory(UserFactory) diff --git a/examples/flask_alchemy/requirements.txt b/examples/flask_alchemy/requirements.txt new file mode 100644 index 0000000..3ee3e5e --- /dev/null +++ b/examples/flask_alchemy/requirements.txt @@ -0,0 +1,3 @@ +-r ../../requirements.txt +Flask +Flask-SQLAlchemy diff --git a/examples/flask_alchemy/test_demoapp.py b/examples/flask_alchemy/test_demoapp.py new file mode 100644 index 0000000..b485a92 --- /dev/null +++ b/examples/flask_alchemy/test_demoapp.py @@ -0,0 +1,35 @@ +import os +import unittest +import tempfile + +import demoapp +import demoapp_factories + +class DemoAppTestCase(unittest.TestCase): + + def setUp(self): + demoapp.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + demoapp.app.config['TESTING'] = True + self.app = demoapp.app.test_client() + self.db = demoapp.db + self.db.create_all() + + def tearDown(self): + self.db.drop_all() + + def test_user_factory(self): + user = demoapp_factories.UserFactory() + self.db.session.commit() + self.assertIsNotNone(user.id) + self.assertEqual(1, len(demoapp.User.query.all())) + + def test_userlog_factory(self): + userlog = demoapp_factories.UserLogFactory() + self.db.session.commit() + self.assertIsNotNone(userlog.id) + self.assertIsNotNone(userlog.user.id) + self.assertEqual(1, len(demoapp.User.query.all())) + self.assertEqual(1, len(demoapp.UserLog.query.all())) + +if __name__ == '__main__': + unittest.main() diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 0000000..5e11ca5 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1 @@ +-r flask_alchemy/requirements.txt -- cgit v1.2.3 From 197555d3d0de4759ca9ad45d5986fdcc4aa4c15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 25 Jul 2015 14:19:12 +0200 Subject: Docs: 'import factory.fuzzy' as required (See #138). --- docs/fuzzy.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index af5c490..6b06608 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -8,6 +8,8 @@ Some tests may be interested in testing with fuzzy, random values. This is handled by the :mod:`factory.fuzzy` module, which provides a few random declarations. +.. note:: Use ``import factory.fuzzy`` to load this module. + FuzzyAttribute -------------- -- cgit v1.2.3 From b9347efae7e2f04687863f54e8db7d9e10f9dc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 22:51:35 +0200 Subject: examples: Fix make test (Closes #238) Properly install dependencies from examples folders. --- Makefile | 3 ++- examples/flask_alchemy/requirements.txt | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 79b5e82..35f635c 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ MONGOENGINE ?= 0.9 NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt +EXAMPLES_REQ_FILES = $(shell find $(EXAMPLES_DIR) -name requirements.txt) all: default @@ -29,7 +30,7 @@ install-deps: $(REQ_FILE) pip install --upgrade -r $< pip freeze -$(REQ_FILE): dev_requirements.txt requirements.txt +$(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)" >> $@ diff --git a/examples/flask_alchemy/requirements.txt b/examples/flask_alchemy/requirements.txt index 3ee3e5e..fb675a9 100644 --- a/examples/flask_alchemy/requirements.txt +++ b/examples/flask_alchemy/requirements.txt @@ -1,3 +1,2 @@ --r ../../requirements.txt Flask Flask-SQLAlchemy -- cgit v1.2.3 From 57be4ac78b1213928a83079d298bafcc93e69483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionu=C8=9B=20Ar=C8=9B=C4=83ri=C8=99i?= Date: Sat, 18 Jul 2015 14:30:50 +0100 Subject: add a way to add custom providers to Faker factory_boy wraps faker and it stores Faker generators in a 'private' _FAKER_REGISTRY class attribute dict. There needs to be a way to extend the Faker generators with additional custom providers (without having to access _FAKER_REGISTRY directly). This commit adds a (factory_boy) Faker.add_provider class method which calls Faker's own `add_provider` method on internally stored (via _FAKER_REGISTRY) Faker generators. --- factory/faker.py | 5 +++++ tests/test_faker.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/factory/faker.py b/factory/faker.py index 10a0cba..5411985 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -94,3 +94,8 @@ class Faker(declarations.OrderedDeclaration): cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale) return cls._FAKER_REGISTRY[locale] + + @classmethod + def add_provider(cls, provider, locale=None): + """Add a new Faker provider for the specified locale""" + cls._get_faker(locale).add_provider(provider) diff --git a/tests/test_faker.py b/tests/test_faker.py index 41f8e19..99e54af 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -21,9 +21,12 @@ # THE SOFTWARE. -import factory import unittest +import faker.providers + +import factory + class MockFaker(object): def __init__(self, expected): @@ -103,3 +106,30 @@ class FakerTests(unittest.TestCase): self.assertEqual("John", profile.first_name) self.assertEqual("Valjean", profile.last_name) + def test_add_provider(self): + class Face(object): + def __init__(self, smiley, french_smiley): + self.smiley = smiley + self.french_smiley = french_smiley + + class FaceFactory(factory.Factory): + class Meta: + model = Face + + smiley = factory.Faker('smiley') + french_smiley = factory.Faker('smiley', locale='fr_FR') + + class SmileyProvider(faker.providers.BaseProvider): + def smiley(self): + return ':)' + + class FrenchSmileyProvider(faker.providers.BaseProvider): + def smiley(self): + return '(:' + + factory.Faker.add_provider(SmileyProvider) + factory.Faker.add_provider(FrenchSmileyProvider, 'fr_FR') + + face = FaceFactory() + self.assertEqual(":)", face.smiley) + self.assertEqual("(:", face.french_smiley) -- cgit v1.2.3 From 15f328350311ee46f84c628310e58e4ed8b49e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:08:29 +0200 Subject: Docs: Document Faker.add_provider (Closes #218) --- docs/reference.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 4c7f8f7..3a57c66 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -534,6 +534,23 @@ Faker ... UserFactory() + .. classmethod:: add_provider(cls, locale=None) + + Some projects may need to fake fields beyond those provided by ``fake-factory``; + in such cases, use :meth:`factory.Faker.add_provider` to declare additional providers + for those fields: + + .. code-block:: python + + factory.Faker.add_provider(SmileyProvider) + + class FaceFactory(factory.Factory): + class Meta: + model = Face + + smiley = factory.Faker('smiley') + + LazyAttribute """"""""""""" -- cgit v1.2.3 From f30c7b243a112eb07af0bcddbd9a211596ed80d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:18:22 +0200 Subject: Lazy load django's get_model (Closes #228). Loading this function will, on pre-1.8 versions, load Django settings. We'll lazy-load it to avoid crashes when Django hasn't been configured yet (e.g in auto-discovery test setups). --- docs/changelog.rst | 1 + factory/django.py | 34 ++++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01d5775..24c01aa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,7 @@ ChangeLog - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching + - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required .. _v2.5.2: diff --git a/factory/django.py b/factory/django.py index b2af12c..b3c508c 100644 --- a/factory/django.py +++ b/factory/django.py @@ -56,16 +56,34 @@ def require_django(): raise import_failure -if django is None: - def get_model(app, model): - raise import_failure +_LAZY_LOADS = {} + +def get_model(app, model): + """Wrapper around django's get_model.""" + if 'get_model' not in _LAZY_LOADS: + _lazy_load_get_model() + + _get_model = _LAZY_LOADS['get_model'] + return _get_model(app, model) + + +def _lazy_load_get_model(): + """Lazy loading of get_model. + + get_model loads django.conf.settings, which may fail if + the settings haven't been configured yet. + """ + if django is None: + def get_model(app, model): + raise import_failure -elif django.VERSION[:2] < (1, 7): - from django.db.models.loading import get_model + elif django.VERSION[:2] < (1, 7): + from django.db.models.loading import get_model -else: - from django import apps as django_apps - get_model = django_apps.apps.get_model + else: + from django import apps as django_apps + get_model = django_apps.apps.get_model + _LAZY_LOADS['get_model'] = get_model class DjangoOptions(base.FactoryOptions): -- cgit v1.2.3 From 1e1adebe92397b405563dc141c853f62feca6c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:21:33 +0200 Subject: Docs: Fix typo in M2M recipes (Closes #226) As spotted by @stephane, thanks! --- docs/recipes.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 3cbe6d2..df86bac 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -148,8 +148,8 @@ factory_boy related factories. method. -Simple ManyToMany ------------------ +Simple Many-to-many relationship +-------------------------------- Building the adequate link between two models depends heavily on the use case; factory_boy doesn't provide a "all in one tools" as for :class:`~factory.SubFactory` @@ -167,7 +167,7 @@ hook: class User(models.Model): name = models.CharField() - groups = models.ManyToMany(Group) + groups = models.ManyToManyField(Group) # factories.py @@ -204,8 +204,8 @@ the ``groups`` declaration will add passed in groups to the set of groups for th user. -ManyToMany with a 'through' ---------------------------- +Many-to-many relation with a 'through' +-------------------------------------- If only one link is required, this can be simply performed with a :class:`RelatedFactory`. @@ -219,7 +219,7 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: class Group(models.Model): name = models.CharField() - members = models.ManyToMany(User, through='GroupLevel') + members = models.ManyToManyField(User, through='GroupLevel') class GroupLevel(models.Model): user = models.ForeignKey(User) -- cgit v1.2.3 From dc7d02095fff8124aaeccf8f08958fa6797b6ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:26:52 +0200 Subject: mogo: Stop using deprecated .new (Closes #219) This method has been deprecated in `mogo.model.Model` since 2012. Thanks to @federicobond for spotting this! --- docs/changelog.rst | 1 + factory/mogo.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 24c01aa..d38c06a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ ChangeLog - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required + - :issue:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago. .. _v2.5.2: diff --git a/factory/mogo.py b/factory/mogo.py index c6c3c19..b5841b1 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -37,7 +37,7 @@ class MogoFactory(base.Factory): @classmethod def _build(cls, model_class, *args, **kwargs): - return model_class.new(*args, **kwargs) + return model_class(*args, **kwargs) @classmethod def _create(cls, model_class, *args, **kwargs): -- cgit v1.2.3 From 41bbff4701ac857bf6c468a4dc53836ee85baa11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:30:18 +0200 Subject: Update note on django's unsaved instance checks This note was added to document a regression in Django 1.8.0; the regression has been fixed in 1.8.4. Closes #232 --- docs/orms.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 26390b5..9b209bc 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -40,12 +40,12 @@ All factories for a Django :class:`~django.db.models.Model` should use the once all post-generation hooks have run. -.. note:: Starting with Django 1.8, it is no longer possible to call ``.build()`` - on a factory if this factory uses a :class:`~factory.SubFactory` pointing - to another model: Django refuses to set a :class:`~djang.db.models.ForeignKey` +.. note:: With Django versions 1.8.0 to 1.8.3, it was no longer possible to call ``.build()`` + on a factory if this factory used a :class:`~factory.SubFactory` pointing + to another model: Django refused to set a :class:`~djang.db.models.ForeignKey` to an unsaved :class:`~django.db.models.Model` instance. - See https://code.djangoproject.com/ticket/10811 for details. + See https://code.djangoproject.com/ticket/10811 and https://code.djangoproject.com/ticket/25160 for details. .. class:: DjangoOptions(factory.base.FactoryOptions) -- cgit v1.2.3 From b827b1a06d5d06e97120f4fa582ebbe79cb59d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:34:42 +0200 Subject: Tox isn't used, remove its config file. --- tox.ini | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 2200f56..0000000 --- a/tox.ini +++ /dev/null @@ -1,17 +0,0 @@ -[tox] -envlist = py26,py27,pypy - -[testenv] -commands= - python -W default setup.py test - -[testenv:py26] - -deps= - mock - unittest2 - -[textenv:py27] - -deps= - mock -- cgit v1.2.3 From 72751aef7b4ba519575bbd8bd4b40864fdf5158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:41:02 +0200 Subject: Ideas: I want to be able to nest declarations Closes #140, as this won't be implemented in the next few weeks. --- docs/ideas.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ideas.rst b/docs/ideas.rst index f3c9e62..6e3962d 100644 --- a/docs/ideas.rst +++ b/docs/ideas.rst @@ -6,4 +6,4 @@ This is a list of future features that may be incorporated into factory_boy: * When a :class:`Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere * Define a proper set of rules for the support of third-party ORMs - +* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) -- cgit v1.2.3 From 3b3fed10ba95ef55cf057994922af55defd007ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:58:48 +0200 Subject: Release v2.6.0 --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/__init__.py b/factory/__init__.py index 843cf99..4a4a09f 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.5.2' +__version__ = '2.6.0' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 5ae2055fe474fbd2a5c5b5a92515bc0affcf9e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 21 Oct 2015 00:12:26 +0200 Subject: docs: Note 2.6.0 release date. --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d38c06a..32d8da6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. _v2.6.0: -2.6.0 (XXXX-XX-XX) +2.6.0 (2015-10-20) ------------------ *New:* -- cgit v1.2.3 From e1bf839f80398d5bd5465cf5fa9463915f887c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 21 Oct 2015 00:16:21 +0200 Subject: mogo: Stop using .new, continued. From dc7d02095fff, spotted by @federicobond too. See #219. --- factory/mogo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/mogo.py b/factory/mogo.py index b5841b1..aa9f28b 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -41,6 +41,6 @@ class MogoFactory(base.Factory): @classmethod def _create(cls, model_class, *args, **kwargs): - instance = model_class.new(*args, **kwargs) + instance = model_class(*args, **kwargs) instance.save() return instance -- cgit v1.2.3 From be85908f5205810083c524a25c7da565788f2c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 7 Nov 2015 10:08:47 +0100 Subject: Fix obsolete text in docs (Closes #245, #248, #249). Thanks a lot to Jeff Widman for spotting them! --- docs/changelog.rst | 2 -- docs/reference.rst | 2 +- examples/flask_alchemy/demoapp_factories.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 32d8da6..fa542f4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -131,8 +131,6 @@ This takes care of all ``FACTORY_FOR`` occurences; the files containing other at For :class:`factory.Factory`: - * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` - * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` * Rename :attr:`~factory.Factory.ABSTRACT_FACTORY` to :attr:`~factory.FactoryOptions.abstract` * Rename :attr:`~factory.Factory.FACTORY_STRATEGY` to :attr:`~factory.FactoryOptions.strategy` diff --git a/docs/reference.rst b/docs/reference.rst index 3a57c66..6398d9a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -634,7 +634,7 @@ This declaration takes a single argument, a function accepting a single paramete .. note:: An extra kwarg argument, ``type``, may be provided. - This feature is deprecated in 1.3.0 and will be removed in 2.0.0. + This feature was deprecated in 1.3.0 and will be removed in 2.0.0. .. code-block:: python diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py index 6b71d04..f32f8c3 100644 --- a/examples/flask_alchemy/demoapp_factories.py +++ b/examples/flask_alchemy/demoapp_factories.py @@ -1,5 +1,4 @@ import factory -import factory.alchemy import factory.fuzzy import demoapp -- cgit v1.2.3 From 05082c661655df319ce641dd7976c02d1799ab14 Mon Sep 17 00:00:00 2001 From: mluszczyk Date: Mon, 28 Dec 2015 13:06:13 +0100 Subject: Fixed spelling. --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index 9b209bc..0afda69 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -15,7 +15,7 @@ Django The first versions of factory_boy were designed specifically for Django, -but the library has now evolved to be framework-independant. +but the library has now evolved to be framework-independent. Most features should thus feel quite familiar to Django users. -- cgit v1.2.3 From 5000ddaaef582e7504babf4f8163de13b93e7459 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Wed, 6 Jan 2016 19:36:10 -0300 Subject: optional forced flush on SQLAlchemyModelFactory fixes rbarrois/factory_boy#81 --- docs/orms.rst | 4 ++++ factory/alchemy.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index 9b209bc..bd481bd 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -333,6 +333,10 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. + .. attribute:: force_flush + + Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + A (very) simple example: .. code-block:: python diff --git a/factory/alchemy.py b/factory/alchemy.py index 20da6cf..a9aab23 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -27,6 +27,7 @@ class SQLAlchemyOptions(base.FactoryOptions): def _build_default_options(self): return super(SQLAlchemyOptions, self)._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), + base.OptionDefault('force_flush', False, inherit=True), ] @@ -43,4 +44,6 @@ class SQLAlchemyModelFactory(base.Factory): session = cls._meta.sqlalchemy_session obj = model_class(*args, **kwargs) session.add(obj) + if cls._meta.force_flush: + session.flush() return obj -- cgit v1.2.3 From 28ce31db61a46fbd73126630c758d32a7245da42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 6 Jan 2016 23:10:26 +0100 Subject: Clarify the (dis)advantages of randomized tests. As noted in #259, fully random tests have some issues, notably possibly flaky builds: it is quite helpful to be able to choose the random seeds used by factory_boy and friends. --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9b82406..8914c62 100644 --- a/README.rst +++ b/README.rst @@ -181,7 +181,7 @@ It is also possible to create a bunch of objects in a single call: Realistic, random values """""""""""""""""""""""" -Tests look better with random yet realistic values. +Demos look better with random yet realistic values; and those realistic values can also help discover bugs. For this, factory_boy relies on the excellent `fake-factory `_ library: .. code-block:: python @@ -199,6 +199,10 @@ For this, factory_boy relies on the excellent `fake-factory +.. note:: Use of fully randomized data in tests is quickly a problem for reproducing broken builds. + To that purpose, factory_boy provides helpers to handle the random seeds it uses. + + Lazy Attributes """"""""""""""" -- cgit v1.2.3 From 4172dd686ce483191b33e3189d716f11b3da921e Mon Sep 17 00:00:00 2001 From: Alejandro Date: Wed, 6 Jan 2016 19:36:10 -0300 Subject: optional forced flush on SQLAlchemyModelFactory fixes rbarrois/factory_boy#81 --- docs/orms.rst | 4 ++++ factory/alchemy.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index 9b209bc..bd481bd 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -333,6 +333,10 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. + .. attribute:: force_flush + + Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + A (very) simple example: .. code-block:: python diff --git a/factory/alchemy.py b/factory/alchemy.py index 20da6cf..a9aab23 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -27,6 +27,7 @@ class SQLAlchemyOptions(base.FactoryOptions): def _build_default_options(self): return super(SQLAlchemyOptions, self)._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), + base.OptionDefault('force_flush', False, inherit=True), ] @@ -43,4 +44,6 @@ class SQLAlchemyModelFactory(base.Factory): session = cls._meta.sqlalchemy_session obj = model_class(*args, **kwargs) session.add(obj) + if cls._meta.force_flush: + session.flush() return obj -- cgit v1.2.3 From b8050b1d61cd3171c2640eeaa6b3f71a6cbef5f5 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Thu, 7 Jan 2016 12:52:57 -0300 Subject: added unittests for rbarrois/factory_boy#81 --- tests/test_alchemy.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 9d7288a..5d8f275 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -23,6 +23,7 @@ import factory from .compat import unittest +import mock try: @@ -55,6 +56,16 @@ class StandardFactory(SQLAlchemyModelFactory): foo = factory.Sequence(lambda n: 'foo%d' % n) +class ForceFlushingStandardFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_session = mock.MagicMock() + force_flush = True + + id = factory.Sequence(lambda n: n) + foo = factory.Sequence(lambda n: 'foo%d' % n) + + class NonIntegerPkFactory(SQLAlchemyModelFactory): class Meta: model = models.NonIntegerPk @@ -102,6 +113,27 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase): self.assertEqual(0, std2.id) +@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") +class SQLAlchemyForceFlushTestCase(unittest.TestCase): + def setUp(self): + super(SQLAlchemyForceFlushTestCase, self).setUp() + ForceFlushingStandardFactory.reset_sequence(1) + ForceFlushingStandardFactory._meta.sqlalchemy_session.rollback() + ForceFlushingStandardFactory._meta.sqlalchemy_session.reset_mock() + + def test_force_flush_called(self): + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory.create() + self.assertTrue(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + + def test_force_flush_not_called(self): + ForceFlushingStandardFactory._meta.force_flush = False + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory.create() + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory._meta.force_flush = True + + @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): -- cgit v1.2.3 From 229d43874723f36b380eb49e53538bf21511fa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 9 Feb 2016 23:57:31 +0100 Subject: Clarify the use of SelfAttribute in RelatedFactory (Closes #264) --- docs/reference.rst | 17 +++++++++++++++++ tests/test_using.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 6398d9a..b5ccd16 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1412,6 +1412,23 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables 1 +.. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated. + This means that calls to :class:`factory.SelfAttribute` cannot go higher than this :class:`RelatedFactory`: + + .. code-block:: python + + class CountryFactory(factory.Factory): + class Meta: + model = Country + + lang = 'fr' + capital_city = factory.RelatedFactory(CityFactory, 'capital_of', + # factory.SelfAttribute('..lang') will crash, since the context of + # ``CountryFactory`` has already been evaluated. + main_lang=factory.SelfAttribute('capital_of.lang'), + ) + + PostGeneration """""""""""""" diff --git a/tests/test_using.py b/tests/test_using.py index c7d2b85..0a893c1 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1924,6 +1924,36 @@ class PostGenerationTestCase(unittest.TestCase): self.assertEqual(3, related.one) self.assertEqual(4, related.two) + def test_related_factory_selfattribute(self): + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + obj.related = self + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactory(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj', + two=factory.SelfAttribute('obj.two'), + ) + + obj = TestObjectFactory.build(two=4) + self.assertEqual(3, obj.one) + self.assertEqual(4, obj.two) + self.assertEqual(1, obj.related.one) + self.assertEqual(4, obj.related.two) + + class RelatedFactoryExtractionTestCase(unittest.TestCase): def setUp(self): -- cgit v1.2.3 From 2eb8242a31f303d36c15b4644c54afb2cef8257e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 9 Feb 2016 23:58:01 +0100 Subject: doc: Use ReadTheDocs theme for local doc builds. --- dev_requirements.txt | 3 +++ docs/conf.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 22261a1..c78aa9d 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,3 +8,6 @@ SQLAlchemy mongoengine mock wheel + +Sphinx +sphinx_rtd_theme diff --git a/docs/conf.py b/docs/conf.py index c3512e0..d5b86f4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,7 +114,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the -- cgit v1.2.3 From 8269885f9a71850838ee003627bcfd6d6d53e2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:07:32 +0100 Subject: fuzzy: Fix decimal.FloatOperation warning (Closes #261) Under Python 2.7+, the previous versions was directly casting fuzzy Decimal values into a float, which led to warnings in code trying to avoid such conversions in its tested code. Since we're just building random values, that behavior led to false positives or required jumping through weird hoops whenever a FuzzyDecimal was used. We now go trough a ``str()`` call to avoid such warnings. --- factory/compat.py | 8 -------- factory/fuzzy.py | 2 +- tests/test_fuzzy.py | 12 ++++++++++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/factory/compat.py b/factory/compat.py index 785d174..737d91a 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -42,14 +42,6 @@ else: # pragma: no cover from io import BytesIO -if sys.version_info[:2] == (2, 6): # pragma: no cover - def float_to_decimal(fl): - return decimal.Decimal(str(fl)) -else: # pragma: no cover - def float_to_decimal(fl): - return decimal.Decimal(fl) - - try: # pragma: no cover # Python >= 3.2 UTC = datetime.timezone.utc diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 923d8b7..a7e834c 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -164,7 +164,7 @@ class FuzzyDecimal(BaseFuzzyAttribute): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - base = compat.float_to_decimal(_random.uniform(self.low, self.high)) + base = decimal.Decimal(str(_random.uniform(self.low, self.high))) return base.quantize(decimal.Decimal(10) ** -self.precision) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 3f9c434..d83f3dd 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -189,6 +189,18 @@ class FuzzyDecimalTestCase(unittest.TestCase): self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) + def test_no_approximation(self): + """We should not go through floats in our fuzzy calls unless actually needed.""" + fuzz = fuzzy.FuzzyDecimal(0, 10) + + decimal_context = decimal.getcontext() + old_traps = decimal_context.traps[decimal.FloatOperation] + try: + decimal_context.traps[decimal.FloatOperation] = True + fuzz.evaluate(2, None, None) + finally: + decimal_context.traps[decimal.FloatOperation] = old_traps + class FuzzyDateTestCase(unittest.TestCase): @classmethod -- cgit v1.2.3 From efd5c65b99a31992001a9581a41ec4627c4d94fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:15:52 +0100 Subject: Clarify precedence on factory.django.FileField (Closes #257). When both ``from_file`` and ``filename`` are provided, ``filename`` takes precedence. Thanks to @darkowic for spotting this :) --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index bd481bd..d1b30fc 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -126,7 +126,7 @@ Extra fields :param str from_path: Use data from the file located at ``from_path``, and keep its filename :param file from_file: Use the contents of the provided file object; use its filename - if available + if available, unless ``filename`` is also provided. :param bytes data: Use the provided bytes as file contents :param str filename: The filename for the FileField -- cgit v1.2.3 From fb613d3cd9513f283072e2792317a5874e148815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:32:36 +0100 Subject: Fix "no FloatOperation test", invalid until PY3 --- tests/test_fuzzy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index d83f3dd..4c3873a 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -189,6 +189,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) + @unittest.skipIf(compat.PY2, "decimal.FloatOperation was added in Py3") def test_no_approximation(self): """We should not go through floats in our fuzzy calls unless actually needed.""" fuzz = fuzzy.FuzzyDecimal(0, 10) -- cgit v1.2.3 From 97804f061ca8b0e090136c0d02e7549000c201ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:34:17 +0100 Subject: Update testing targets (Closes #265) Thanks to @jeffwidman for suggesting this! --- .travis.yml | 1 + Makefile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1600bd..ff805b0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python python: - "2.7" - "3.4" + - "3.5" - "pypy" script: diff --git a/Makefile b/Makefile index 35f635c..da8ac88 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,13 @@ EXAMPLES_DIR=examples COVERAGE = python $(shell which coverage) # Dependencies -DJANGO ?= 1.8 +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.9 +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 -- cgit v1.2.3 From 8050e9941408d29e339e47066c09f3d9ed19ffe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:51:39 +0100 Subject: Switch badges to shields.io --- README.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 8914c62..576ba39 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,24 @@ factory_boy =========== -.. image:: https://pypip.in/version/factory_boy/badge.svg +.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master + :target: http://travis-ci.org/rbarrois/factory_boy/ + +.. image:: https://img.shields.io/pypi/v/factory_boy.svg :target: http://factoryboy.readthedocs.org/en/latest/changelog.html :alt: Latest Version -.. image:: https://pypip.in/py_versions/factory_boy/badge.svg +.. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg :target: https://pypi.python.org/pypi/factory_boy/ :alt: Supported Python versions -.. image:: https://pypip.in/wheel/factory_boy/badge.svg +.. image:: https://img.shields.io/pypi/wheel/factory_boy.svg :target: https://pypi.python.org/pypi/factory_boy/ :alt: Wheel status -.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master - :target: http://travis-ci.org/rbarrois/factory_boy/ +.. image:: https://img.shields.io/pypi/l/factory_boy.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: License factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. -- cgit v1.2.3 From 023070446c6251e563f8eb087e0a1fe7fbeb248b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:52:36 +0100 Subject: Announce support for Python3.5 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8ca7e4b..003dd08 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ setup( "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", -- cgit v1.2.3 From 99337aaa01860c771704e1c558c225f8fead5b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:53:47 +0100 Subject: Update README: support 2.6-3.5 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 576ba39..ee737c9 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ Links * Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ -factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.6, 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. Download -- cgit v1.2.3 From 5ec286d3b666a8f570e90ea18ec492b6db996fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:54:00 +0100 Subject: Document mailing-list --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ee737c9..4d114e5 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,7 @@ Links * Documentation: http://factoryboy.readthedocs.org/ * Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ +* Mailing-list: `factoryboy@googlegroups.com `_ | 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. @@ -336,6 +337,7 @@ Contributing factory_boy is distributed under the MIT License. Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. +Questions and suggestions are welcome on the `mailing-list `_. All pull request should pass the test suite, which can be launched simply with: @@ -356,7 +358,7 @@ To test with a specific framework version, you may use: .. code-block:: sh - $ make DJANGO=1.7 test + $ make DJANGO=1.9 test Valid options are: @@ -365,6 +367,13 @@ Valid options are: * ``ALCHEMY`` for ``SQLAlchemy`` +To avoid running ``mongoengine`` tests (e.g no mongo server installed), run: + +.. code-block:: sh + + $ make SKIP_MONGOENGINE=1 test + + Contents, indices and tables ---------------------------- -- cgit v1.2.3 From 41560aa54e83fe539c0a5a1935bcaaf6363a522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 01:07:54 +0100 Subject: Release v2.6.1 --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/__init__.py b/factory/__init__.py index 4a4a09f..c8bc396 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.6.0' +__version__ = '2.6.1' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3