diff options
-rw-r--r-- | dev_requirements.txt | 3 | ||||
-rw-r--r-- | docs/changelog.rst | 11 | ||||
-rw-r--r-- | docs/fuzzy.rst | 40 | ||||
-rw-r--r-- | docs/orms.rst | 41 | ||||
-rw-r--r-- | docs/recipes.rst | 22 | ||||
-rw-r--r-- | factory/django.py | 68 | ||||
-rw-r--r-- | factory/fuzzy.py | 22 | ||||
-rw-r--r-- | factory/helpers.py | 1 | ||||
-rw-r--r-- | tests/djapp/models.py | 4 | ||||
-rw-r--r-- | tests/test_django.py | 92 | ||||
-rw-r--r-- | tests/test_fuzzy.py | 22 |
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): |