summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--dev_requirements.txt3
-rw-r--r--docs/changelog.rst11
-rw-r--r--docs/fuzzy.rst40
-rw-r--r--docs/orms.rst41
-rw-r--r--docs/recipes.rst22
-rw-r--r--factory/django.py68
-rw-r--r--factory/fuzzy.py22
-rw-r--r--factory/helpers.py1
-rw-r--r--tests/djapp/models.py4
-rw-r--r--tests/test_django.py92
-rw-r--r--tests/test_fuzzy.py22
11 files changed, 312 insertions, 14 deletions
diff --git a/dev_requirements.txt b/dev_requirements.txt
index e828644..bdc23d0 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -2,4 +2,5 @@ coverage
Django
Pillow
sqlalchemy
-mongoengine \ No newline at end of file
+mongoengine
+mock
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 4917578..47d1139 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -2,6 +2,17 @@ ChangeLog
=========
+.. _v2.4.0:
+
+2.4.0 (master)
+--------------
+
+*New:*
+
+ - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov <https://github.com/ilya-pirogov>`_ (:issue:`120`)
+ - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov <https://github.com>`_ (:issue:`122`)
+ - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`)
+
.. _v2.3.1:
2.3.1 (2014-01-22)
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
index b94dfa5..1480419 100644
--- a/docs/fuzzy.rst
+++ b/docs/fuzzy.rst
@@ -73,7 +73,7 @@ FuzzyChoice
FuzzyInteger
------------
-.. class:: FuzzyInteger(low[, high])
+.. class:: FuzzyInteger(low[, high[, step]])
The :class:`FuzzyInteger` fuzzer generates random integers within a given
inclusive range.
@@ -82,7 +82,7 @@ FuzzyInteger
.. code-block:: pycon
- >>> FuzzyInteger(0, 42)
+ >>> fi = FuzzyInteger(0, 42)
>>> fi.low, fi.high
0, 42
@@ -98,12 +98,18 @@ FuzzyInteger
int, the inclusive higher bound of generated integers
+ .. attribute:: step
+
+ int, the step between values in the range; for instance, a ``FuzzyInteger(0, 42, step=3)``
+ might only yield values from ``[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42]``.
+
+
FuzzyDecimal
------------
-.. class:: FuzzyDecimal(low[, high])
+.. class:: FuzzyDecimal(low[, high[, precision=2]])
- The :class:`FuzzyDecimal` fuzzer generates random integers within a given
+ The :class:`FuzzyDecimal` fuzzer generates random :class:`decimals <decimal.Decimal>` within a given
inclusive range.
The :attr:`low` bound may be omitted, in which case it defaults to 0:
@@ -134,6 +140,32 @@ FuzzyDecimal
int, the number of digits to generate after the dot. The default is 2 digits.
+FuzzyFloat
+----------
+
+.. class:: FuzzyFloat(low[, high])
+
+ The :class:`FuzzyFloat` fuzzer provides random :class:`float` objects within a given inclusive range.
+
+ .. code-block:: pycon
+
+ >>> FuzzyFloat(0.5, 42.7)
+ >>> fi.low, fi.high
+ 0.5, 42.7
+
+ >>> fi = FuzzyFloat(42.7)
+ >>> fi.low, fi.high
+ 0.0, 42.7
+
+
+ .. attribute:: low
+
+ decimal, the inclusive lower bound of generated floats
+
+ .. attribute:: high
+
+ decimal, the inclusive higher bound of generated floats
+
FuzzyDate
---------
diff --git a/docs/orms.rst b/docs/orms.rst
index e50e706..c893cac 100644
--- a/docs/orms.rst
+++ b/docs/orms.rst
@@ -89,6 +89,10 @@ All factories for a Django :class:`~django.db.models.Model` should use the
Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model.
+Extra fields
+""""""""""""
+
+
.. class:: FileField
Custom declarations for :class:`django.db.models.FileField`
@@ -157,6 +161,43 @@ All factories for a Django :class:`~django.db.models.Model` should use the
None
+Disabling signals
+"""""""""""""""""
+
+Signals are often used to plug some custom code into external components code;
+for instance to create ``Profile`` objects on-the-fly when a new ``User`` object is saved.
+
+This may interfere with finely tuned :class:`factories <DjangoModelFactory>`, which would
+create both using :class:`~factory.RelatedFactory`.
+
+To work around this problem, use the :meth:`mute_signals()` decorator/context manager:
+
+.. method:: mute_signals(signal1, ...)
+
+ Disable the list of selected signals when calling the factory, and reactivate them upon leaving.
+
+.. code-block:: python
+
+ # foo/factories.py
+
+ import factory
+ import factory.django
+
+ from . import models
+ from . import signals
+
+ @factory.django.mute_signals(signals.pre_save, signals.post_save)
+ class FooFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.Foo
+
+ # ...
+
+ def make_chain():
+ with factory.django.mute_signals(signals.pre_save, signals.post_save):
+ # pre_save/post_save won't be called here.
+ return SomeFactory(), SomeOtherFactory()
+
+
Mogo
----
diff --git a/docs/recipes.rst b/docs/recipes.rst
index c1f3700..9e07413 100644
--- a/docs/recipes.rst
+++ b/docs/recipes.rst
@@ -291,3 +291,25 @@ Here, we want:
name = "ACME, Inc."
country = factory.SubFactory(CountryFactory)
owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country'))
+
+
+Custom manager methods
+----------------------
+
+Sometimes you need a factory to call a specific manager method other then the
+default :meth:`Model.objects.create() <django.db.models.query.QuerySet.create>` method:
+
+.. code-block:: python
+
+ class UserFactory(factory.DjangoModelFactory):
+ FACTORY_FOR = UserenaSignup
+ username = "l7d8s"
+ email = "my_name@example.com"
+ password = "my_password"
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """Override the default ``_create`` with our custom call."""
+ manager = cls._get_manager(target_class)
+ # The default would use ``manager.create(*args, **kwargs)``
+ return manager.create_user(*args, **kwargs)
diff --git a/factory/django.py b/factory/django.py
index fee8e52..a3dfdfc 100644
--- a/factory/django.py
+++ b/factory/django.py
@@ -25,6 +25,9 @@ from __future__ import absolute_import
from __future__ import unicode_literals
import os
+import types
+import logging
+import functools
"""factory_boy extensions for use with the Django framework."""
@@ -39,6 +42,9 @@ from . import base
from . import declarations
from .compat import BytesIO, is_string
+logger = logging.getLogger('factory.generate')
+
+
def require_django():
"""Simple helper to ensure Django is available."""
@@ -214,3 +220,65 @@ class ImageField(FileField):
thumb.save(thumb_io, format=image_format)
return thumb_io.getvalue()
+
+class mute_signals(object):
+ """Temporarily disables and then restores any django signals.
+
+ Args:
+ *signals (django.dispatch.dispatcher.Signal): any django signals
+
+ Examples:
+ with mute_signals(pre_init):
+ user = UserFactory.build()
+ ...
+
+ @mute_signals(pre_save, post_save)
+ class UserFactory(factory.Factory):
+ ...
+
+ @mute_signals(post_save)
+ def generate_users():
+ UserFactory.create_batch(10)
+ """
+
+ def __init__(self, *signals):
+ self.signals = signals
+ self.paused = {}
+
+ def __enter__(self):
+ for signal in self.signals:
+ logger.debug('mute_signals: Disabling signal handlers %r',
+ signal.receivers)
+
+ self.paused[signal] = signal.receivers
+ signal.receivers = []
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ for signal, receivers in self.paused.items():
+ logger.debug('mute_signals: Restoring signal handlers %r',
+ receivers)
+
+ signal.receivers = receivers
+ self.paused = {}
+
+ def __call__(self, callable_obj):
+ if isinstance(callable_obj, base.FactoryMetaClass):
+ # Retrieve __func__, the *actual* callable object.
+ generate_method = callable_obj._generate.__func__
+
+ @classmethod
+ @functools.wraps(generate_method)
+ def wrapped_generate(*args, **kwargs):
+ with self:
+ return generate_method(*args, **kwargs)
+
+ callable_obj._generate = wrapped_generate
+ return callable_obj
+
+ else:
+ @functools.wraps(callable_obj)
+ def wrapper(*args, **kwargs):
+ with self:
+ return callable_obj(*args, **kwargs)
+ return wrapper
+
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index 34949c5..94599b7 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -107,18 +107,19 @@ class FuzzyChoice(BaseFuzzyAttribute):
class FuzzyInteger(BaseFuzzyAttribute):
"""Random integer within a given range."""
- def __init__(self, low, high=None, **kwargs):
+ def __init__(self, low, high=None, step=1, **kwargs):
if high is None:
high = low
low = 0
self.low = low
self.high = high
+ self.step = step
super(FuzzyInteger, self).__init__(**kwargs)
def fuzz(self):
- return random.randint(self.low, self.high)
+ return random.randrange(self.low, self.high + 1, self.step)
class FuzzyDecimal(BaseFuzzyAttribute):
@@ -140,6 +141,23 @@ class FuzzyDecimal(BaseFuzzyAttribute):
return base.quantize(decimal.Decimal(10) ** -self.precision)
+class FuzzyFloat(BaseFuzzyAttribute):
+ """Random float within a given range."""
+
+ def __init__(self, low, high=None, **kwargs):
+ if high is None:
+ high = low
+ low = 0
+
+ self.low = low
+ self.high = high
+
+ super(FuzzyFloat, self).__init__(**kwargs)
+
+ def fuzz(self):
+ return random.uniform(self.low, self.high)
+
+
class FuzzyDate(BaseFuzzyAttribute):
"""Random date within a given date range."""
diff --git a/factory/helpers.py b/factory/helpers.py
index 37b41bf..4a2a254 100644
--- a/factory/helpers.py
+++ b/factory/helpers.py
@@ -28,6 +28,7 @@ import logging
from . import base
from . import declarations
+from . import django
@contextlib.contextmanager
diff --git a/tests/djapp/models.py b/tests/djapp/models.py
index e98279d..a65b50a 100644
--- a/tests/djapp/models.py
+++ b/tests/djapp/models.py
@@ -74,3 +74,7 @@ if Image is not None: # PIL is available
else:
class WithImage(models.Model):
pass
+
+
+class WithSignals(models.Model):
+ foo = models.CharField(max_length=20) \ No newline at end of file
diff --git a/tests/test_django.py b/tests/test_django.py
index e4bbc2b..50a67a3 100644
--- a/tests/test_django.py
+++ b/tests/test_django.py
@@ -42,7 +42,7 @@ except ImportError: # pragma: no cover
Image = None
-from .compat import is_python2, unittest
+from .compat import is_python2, unittest, mock
from . import testdata
from . import tools
@@ -55,6 +55,7 @@ if django is not None:
from django.db import models as django_models
from django.test import simple as django_test_simple
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
@@ -70,6 +71,7 @@ else: # pragma: no cover
models.NonIntegerPk = Fake
models.WithFile = Fake
models.WithImage = Fake
+ models.WithSignals = Fake
test_state = {}
@@ -142,6 +144,10 @@ class WithImageFactory(factory.django.DjangoModelFactory):
animage = factory.django.ImageField()
+class WithSignalsFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.WithSignals
+
+
@unittest.skipIf(django is None, "Django not installed.")
class DjangoPkSequenceTestCase(django_test.TestCase):
def setUp(self):
@@ -511,5 +517,89 @@ class DjangoImageFieldTestCase(unittest.TestCase):
self.assertFalse(o.animage)
+@unittest.skipIf(django is None, "Django not installed.")
+class PreventSignalsTestCase(unittest.TestCase):
+ def setUp(self):
+ self.handlers = mock.MagicMock()
+
+ signals.pre_init.connect(self.handlers.pre_init)
+ signals.pre_save.connect(self.handlers.pre_save)
+ signals.post_save.connect(self.handlers.post_save)
+
+ def tearDown(self):
+ signals.pre_init.disconnect(self.handlers.pre_init)
+ signals.pre_save.disconnect(self.handlers.pre_save)
+ signals.post_save.disconnect(self.handlers.post_save)
+
+ def assertSignalsReactivated(self):
+ WithSignalsFactory()
+
+ self.assertEqual(self.handlers.pre_save.call_count, 1)
+ self.assertEqual(self.handlers.post_save.call_count, 1)
+
+ def test_context_manager(self):
+ with factory.django.mute_signals(signals.pre_save, signals.post_save):
+ WithSignalsFactory()
+
+ 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()
+
+ def test_class_decorator(self):
+ @factory.django.mute_signals(signals.pre_save, signals.post_save)
+ class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.WithSignals
+
+ WithSignalsDecoratedFactory()
+
+ 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()
+
+ def test_class_decorator_build(self):
+ @factory.django.mute_signals(signals.pre_save, signals.post_save)
+ class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory):
+ FACTORY_FOR = models.WithSignals
+
+ WithSignalsDecoratedFactory.build()
+
+ 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()
+
+ def test_function_decorator(self):
+ @factory.django.mute_signals(signals.pre_save, signals.post_save)
+ def foo():
+ WithSignalsFactory()
+
+ foo()
+
+ 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()
+
+ def test_classmethod_decorator(self):
+ class Foo(object):
+ @classmethod
+ @factory.django.mute_signals(signals.pre_save, signals.post_save)
+ def generate(cls):
+ WithSignalsFactory()
+
+ Foo.generate()
+
+ 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()
+
if __name__ == '__main__': # pragma: no cover
unittest.main()
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index d6f33bb..1caeb0a 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -89,24 +89,34 @@ class FuzzyIntegerTestCase(unittest.TestCase):
self.assertIn(res, [0, 1, 2, 3, 4])
def test_biased(self):
- fake_randint = lambda low, high: low + high
+ fake_randrange = lambda low, high, step: (low + high) * step
fuzz = fuzzy.FuzzyInteger(2, 8)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
- self.assertEqual(10, res)
+ self.assertEqual((2 + 8 + 1) * 1, res)
def test_biased_high_only(self):
- fake_randint = lambda low, high: low + high
+ fake_randrange = lambda low, high, step: (low + high) * step
fuzz = fuzzy.FuzzyInteger(8)
- with mock.patch('random.randint', fake_randint):
+ with mock.patch('random.randrange', fake_randrange):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual((0 + 8 + 1) * 1, res)
+
+ def test_biased_with_step(self):
+ fake_randrange = lambda low, high, step: (low + high) * step
+
+ fuzz = fuzzy.FuzzyInteger(5, 8, 3)
+
+ with mock.patch('random.randrange', fake_randrange):
res = fuzz.evaluate(2, None, False)
- self.assertEqual(8, res)
+ self.assertEqual((5 + 8 + 1) * 3, res)
class FuzzyDecimalTestCase(unittest.TestCase):