summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphaël Barrois <raphael.barrois@polytechnique.org>2013-04-21 19:35:43 +0200
committerRaphaël Barrois <raphael.barrois@polytechnique.org>2013-04-21 19:38:50 +0200
commita48f6482f21994a44da14dafdeff8e7e1237cec9 (patch)
treedaa9e1dc5ffdeeaf069cb2c759ce31bc7d4b4c0e
parentb6ecfdf7b8f0b05b3e78fe3aafaf26d9c00c3259 (diff)
downloadfactory-boy-a48f6482f21994a44da14dafdeff8e7e1237cec9.tar
factory-boy-a48f6482f21994a44da14dafdeff8e7e1237cec9.tar.gz
Add FuzzyDateTime/FuzzyNaiveDateTime.
-rw-r--r--docs/fuzzy.rst131
-rw-r--r--factory/compat.py28
-rw-r--r--factory/fuzzy.py102
-rw-r--r--tests/test_fuzzy.py238
-rw-r--r--tests/utils.py100
5 files changed, 598 insertions, 1 deletions
diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst
index 7d56995..d88cb5a 100644
--- a/docs/fuzzy.rst
+++ b/docs/fuzzy.rst
@@ -94,7 +94,136 @@ FuzzyDate
:class:`datetime.date`, the inclusive higher bound of generated dates
- int, the inclusive higher bound of generated dates
+
+FuzzyDateTime
+-------------
+
+.. class:: FuzzyDateTime(start_dt[, end_dt], tz=UTC, force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None)
+
+ The :class:`FuzzyDateTime` fuzzer generates random timezone-aware datetime within a given
+ inclusive range.
+
+ The :attr:`end_dt` bound may be omitted, in which case it defaults to ``datetime.datetime.now()``
+ localized into the UTC timezone.
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=UTC))
+ >>> fdt.start_dt, fdt.end_dt
+ datetime.datetime(2008, 1, 1, tzinfo=UTC), datetime.datetime(2013, 4, 21, 19, 13, 32, 458487, tzinfo=UTC)
+
+
+ The ``force_XXX`` keyword arguments force the related value of generated datetimes:
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyDateTime(datetime.datetime(2008, 1, 1, tzinfo=UTC), datetime.datetime(2009, 1, 1, tzinfo=UTC),
+ ... force_day=3, force_second=42)
+ >>> fdt.evaluate(2, None, False) # Actual code used by ``SomeFactory.build()``
+ datetime.datetime(2008, 5, 3, 12, 13, 42, 124848, tzinfo=UTC)
+
+
+ .. attribute:: start_dt
+
+ :class:`datetime.datetime`, the inclusive lower bound of generated datetimes
+
+ .. attribute:: end_dt
+
+ :class:`datetime.datetime`, the inclusive upper bound of generated datetimes
+
+
+ .. attribute:: force_year
+
+ int or None; if set, forces the :attr:`~datetime.datetime.year` of generated datetime.
+
+ .. attribute:: force_month
+
+ int or None; if set, forces the :attr:`~datetime.datetime.month` of generated datetime.
+
+ .. attribute:: force_day
+
+ int or None; if set, forces the :attr:`~datetime.datetime.day` of generated datetime.
+
+ .. attribute:: force_hour
+
+ int or None; if set, forces the :attr:`~datetime.datetime.hour` of generated datetime.
+
+ .. attribute:: force_minute
+
+ int or None; if set, forces the :attr:`~datetime.datetime.minute` of generated datetime.
+
+ .. attribute:: force_second
+
+ int or None; if set, forces the :attr:`~datetime.datetime.second` of generated datetime.
+
+ .. attribute:: force_microsecond
+
+ int or None; if set, forces the :attr:`~datetime.datetime.microsecond` of generated datetime.
+
+
+FuzzyNaiveDateTime
+------------------
+
+.. class:: FuzzyNaiveDateTime(start_dt[, end_dt], force_year=None, force_month=None, force_day=None, force_hour=None, force_minute=None, force_second=None, force_microsecond=None)
+
+ The :class:`FuzzyNaiveDateTime` fuzzer generates random naive datetime within a given
+ inclusive range.
+
+ The :attr:`end_dt` bound may be omitted, in which case it defaults to ``datetime.datetime.now()``:
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyNaiveDateTime(datetime.datetime(2008, 1, 1))
+ >>> fdt.start_dt, fdt.end_dt
+ datetime.datetime(2008, 1, 1), datetime.datetime(2013, 4, 21, 19, 13, 32, 458487)
+
+
+ The ``force_XXX`` keyword arguments force the related value of generated datetimes:
+
+ .. code-block:: pycon
+
+ >>> fdt = FuzzyNaiveDateTime(datetime.datetime(2008, 1, 1), datetime.datetime(2009, 1, 1),
+ ... force_day=3, force_second=42)
+ >>> fdt.evaluate(2, None, False) # Actual code used by ``SomeFactory.build()``
+ datetime.datetime(2008, 5, 3, 12, 13, 42, 124848)
+
+
+ .. attribute:: start_dt
+
+ :class:`datetime.datetime`, the inclusive lower bound of generated datetimes
+
+ .. attribute:: end_dt
+
+ :class:`datetime.datetime`, the inclusive upper bound of generated datetimes
+
+
+ .. attribute:: force_year
+
+ int or None; if set, forces the :attr:`~datetime.datetime.year` of generated datetime.
+
+ .. attribute:: force_month
+
+ int or None; if set, forces the :attr:`~datetime.datetime.month` of generated datetime.
+
+ .. attribute:: force_day
+
+ int or None; if set, forces the :attr:`~datetime.datetime.day` of generated datetime.
+
+ .. attribute:: force_hour
+
+ int or None; if set, forces the :attr:`~datetime.datetime.hour` of generated datetime.
+
+ .. attribute:: force_minute
+
+ int or None; if set, forces the :attr:`~datetime.datetime.minute` of generated datetime.
+
+ .. attribute:: force_second
+
+ int or None; if set, forces the :attr:`~datetime.datetime.second` of generated datetime.
+
+ .. attribute:: force_microsecond
+
+ int or None; if set, forces the :attr:`~datetime.datetime.microsecond` of generated datetime.
Custom fuzzy fields
diff --git a/factory/compat.py b/factory/compat.py
index 84f31b7..9594550 100644
--- a/factory/compat.py
+++ b/factory/compat.py
@@ -23,6 +23,7 @@
"""Compatibility tools"""
+import datetime
import sys
PY2 = (sys.version_info[0] == 2)
@@ -33,3 +34,30 @@ if PY2:
else:
def is_string(obj):
return isinstance(obj, str)
+
+try:
+ # Python >= 3.2
+ UTC = datetime.timezone.utc
+except AttributeError:
+ try:
+ # Fallback to pytz
+ from pytz import UTC
+ except ImportError:
+
+ # Ok, let's write our own.
+ class _UTC(datetime.tzinfo):
+ """The UTC tzinfo."""
+
+ def utcoffset(self, dt):
+ return datetime.timedelta(0)
+
+ def tzname(self, dt):
+ return "UTC"
+
+ def dst(self, dt):
+ return datetime.timedelta(0)
+
+ def localize(self, dt):
+ dt.astimezone(self)
+
+ UTC = _UTC()
diff --git a/factory/fuzzy.py b/factory/fuzzy.py
index fea7b05..e266b93 100644
--- a/factory/fuzzy.py
+++ b/factory/fuzzy.py
@@ -27,6 +27,7 @@
import random
import datetime
+from . import compat
from . import declarations
@@ -104,3 +105,104 @@ class FuzzyDate(BaseFuzzyAttribute):
def fuzz(self):
return datetime.date.fromordinal(random.randint(self.start_date, self.end_date))
+
+
+class BaseFuzzyDateTime(BaseFuzzyAttribute):
+ """Base class for fuzzy datetime-related attributes.
+
+ Provides fuzz() computation, forcing year/month/day/hour/...
+ """
+
+ def _check_bounds(self, start_dt, end_dt):
+ if start_dt > end_dt:
+ raise ValueError(
+ """%s boundaries should have start <= end, got %r > %r""" % (
+ self.__class__.__name__, start_dt, end_dt))
+
+ def __init__(self, start_dt, end_dt=None,
+ force_year=None, force_month=None, force_day=None,
+ force_hour=None, force_minute=None, force_second=None,
+ force_microsecond=None, **kwargs):
+ super(BaseFuzzyDateTime, self).__init__(**kwargs)
+
+ if end_dt is None:
+ end_dt = self._now()
+
+ self._check_bounds(start_dt, end_dt)
+
+ self.start_dt = start_dt
+ self.end_dt = end_dt
+ self.force_year = force_year
+ self.force_month = force_month
+ self.force_day = force_day
+ self.force_hour = force_hour
+ self.force_minute = force_minute
+ self.force_second = force_second
+ self.force_microsecond = force_microsecond
+
+ def fuzz(self):
+ delta = self.end_dt - self.start_dt
+ microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400))
+
+ offset = random.randint(0, microseconds)
+ result = self.start_dt + datetime.timedelta(microseconds=offset)
+
+ if self.force_year is not None:
+ result = result.replace(year=self.force_year)
+ if self.force_month is not None:
+ result = result.replace(month=self.force_month)
+ if self.force_day is not None:
+ result = result.replace(day=self.force_day)
+ if self.force_hour is not None:
+ result = result.replace(hour=self.force_hour)
+ if self.force_minute is not None:
+ result = result.replace(minute=self.force_minute)
+ if self.force_second is not None:
+ result = result.replace(second=self.force_second)
+ if self.force_microsecond is not None:
+ result = result.replace(microsecond=self.force_microsecond)
+
+ return result
+
+
+class FuzzyNaiveDateTime(BaseFuzzyDateTime):
+ """Random naive datetime within a given range.
+
+ If no upper bound is given, will default to datetime.datetime.utcnow().
+ """
+
+ def _now(self):
+ return datetime.datetime.now()
+
+ def _check_bounds(self, start_dt, end_dt):
+ if start_dt.tzinfo is not None:
+ raise ValueError(
+ "FuzzyNaiveDateTime only handles naive datetimes, got start=%r"
+ % start_dt)
+ if end_dt.tzinfo is not None:
+ raise ValueError(
+ "FuzzyNaiveDateTime only handles naive datetimes, got end=%r"
+ % end_dt)
+ super(FuzzyNaiveDateTime, self)._check_bounds(start_dt, end_dt)
+
+
+class FuzzyDateTime(BaseFuzzyDateTime):
+ """Random timezone-aware datetime within a given range.
+
+ If no upper bound is given, will default to datetime.datetime.now()
+ If no timezone is given, will default to utc.
+ """
+
+ def _now(self):
+ return datetime.datetime.now(tz=compat.UTC)
+
+ def _check_bounds(self, start_dt, end_dt):
+ if start_dt.tzinfo is None:
+ raise ValueError(
+ "FuzzyDateTime only handles aware datetimes, got start=%r"
+ % start_dt)
+ if end_dt.tzinfo is None:
+ raise ValueError(
+ "FuzzyDateTime only handles aware datetimes, got end=%r"
+ % end_dt)
+ super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt)
diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py
index e3b5772..97abece 100644
--- a/tests/test_fuzzy.py
+++ b/tests/test_fuzzy.py
@@ -23,9 +23,11 @@
import datetime
+from factory import compat
from factory import fuzzy
from .compat import mock, unittest
+from . import utils
class FuzzyAttributeTestCase(unittest.TestCase):
@@ -163,3 +165,239 @@ class FuzzyDateTestCase(unittest.TestCase):
res = fuzz.evaluate(2, None, False)
self.assertEqual(datetime.date(2013, 1, 2), res)
+
+
+class FuzzyNaiveDateTimeTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # Setup useful constants
+ cls.jan1 = datetime.datetime(2013, 1, 1)
+ cls.jan3 = datetime.datetime(2013, 1, 3)
+ cls.jan31 = datetime.datetime(2013, 1, 31)
+
+ def test_accurate_definition(self):
+ """Tests explicit definition of a FuzzyNaiveDateTime."""
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan31)
+
+ def test_partial_definition(self):
+ """Test defining a FuzzyNaiveDateTime without passing an end date."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan3)
+
+ def test_aware_start(self):
+ """Tests that a timezone-aware start datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan1.replace(tzinfo=compat.UTC), self.jan31)
+
+ def test_aware_end(self):
+ """Tests that a timezone-aware end datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan1, self.jan31.replace(tzinfo=compat.UTC))
+
+ def test_force_year(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_year=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.year)
+
+ def test_force_month(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_month=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.month)
+
+ def test_force_day(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_day=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.day)
+
+ def test_force_hour(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_hour=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.hour)
+
+ def test_force_minute(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_minute=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.minute)
+
+ def test_force_second(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_second=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.second)
+
+ def test_force_microsecond(self):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_microsecond=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.microsecond)
+
+ def test_invalid_definition(self):
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan31, self.jan1)
+
+ def test_invalid_partial_definition(self):
+ with utils.mocked_datetime_now(self.jan1, fuzzy):
+ self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime,
+ self.jan31)
+
+ def test_biased(self):
+ """Tests a FuzzyDate with a biased random.randint."""
+
+ fake_randint = lambda low, high: (low + high) // 2
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 16), res)
+
+ def test_biased_partial(self):
+ """Tests a FuzzyDate with a biased random and implicit upper bound."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1)
+
+ fake_randint = lambda low, high: (low + high) // 2
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 2), res)
+
+
+class FuzzyDateTimeTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # Setup useful constants
+ cls.jan1 = datetime.datetime(2013, 1, 1, tzinfo=compat.UTC)
+ cls.jan3 = datetime.datetime(2013, 1, 3, tzinfo=compat.UTC)
+ cls.jan31 = datetime.datetime(2013, 1, 31, tzinfo=compat.UTC)
+
+ def test_accurate_definition(self):
+ """Tests explicit definition of a FuzzyDateTime."""
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan31)
+
+ def test_partial_definition(self):
+ """Test defining a FuzzyDateTime without passing an end date."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertLessEqual(self.jan1, res)
+ self.assertLessEqual(res, self.jan3)
+
+ def test_invalid_definition(self):
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan31, self.jan1)
+
+ def test_invalid_partial_definition(self):
+ with utils.mocked_datetime_now(self.jan1, fuzzy):
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan31)
+
+ def test_naive_start(self):
+ """Tests that a timezone-naive start datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan1.replace(tzinfo=None), self.jan31)
+
+ def test_naive_end(self):
+ """Tests that a timezone-naive end datetime is rejected."""
+ self.assertRaises(ValueError, fuzzy.FuzzyDateTime,
+ self.jan1, self.jan31.replace(tzinfo=None))
+
+ def test_force_year(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_year=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.year)
+
+ def test_force_month(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_month=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.month)
+
+ def test_force_day(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_day=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.day)
+
+ def test_force_hour(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_hour=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.hour)
+
+ def test_force_minute(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_minute=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.minute)
+
+ def test_force_second(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_second=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.second)
+
+ def test_force_microsecond(self):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_microsecond=4)
+
+ for _i in range(20):
+ res = fuzz.evaluate(2, None, False)
+ self.assertEqual(4, res.microsecond)
+
+ def test_biased(self):
+ """Tests a FuzzyDate with a biased random.randint."""
+
+ fake_randint = lambda low, high: (low + high) // 2
+ fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31)
+
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res)
+
+ def test_biased_partial(self):
+ """Tests a FuzzyDate with a biased random and implicit upper bound."""
+ with utils.mocked_datetime_now(self.jan3, fuzzy):
+ fuzz = fuzzy.FuzzyDateTime(self.jan1)
+
+ fake_randint = lambda low, high: (low + high) // 2
+ with mock.patch('random.randint', fake_randint):
+ res = fuzz.evaluate(2, None, False)
+
+ self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res)
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 0000000..823e82b
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,100 @@
+# -*- 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.
+
+import datetime
+
+from .compat import mock
+
+
+class MultiModulePatcher(object):
+ """An abstract context processor for patching multiple modules."""
+
+ replaced_symbol = None
+ replaced_symbol_module = None
+ module_imported_as = ''
+
+ def __init__(self, *target_modules, **kwargs):
+ super(MultiModulePatcher, self).__init__(**kwargs)
+ if not self.module_imported_as:
+ self.module_imported_as = replaced_symbol.__module__.__name__
+ self.patchers = [self._build_patcher(mod) for mod in target_modules]
+
+ def _check_module(self, target_module):
+ if not self.replaced_symbol_module:
+ # No check to perform
+ return
+
+ replaced_import = getattr(target_module, self.module_imported_as)
+ assert replaced_import is self.replaced_symbol_module, (
+ "Cannot patch import %s.%s (%r != %r)" % (
+ target_module.__name__, self.module_imported_as,
+ replaced_import, self.replaced_symbol_module))
+
+ def _build_patcher(self, target_module):
+ """Build a mock patcher for the target module."""
+ self._check_module(target_module)
+
+ return mock.patch.object(
+ getattr(target_module, self.module_imported_as),
+ self.replaced_symbol.__name__,
+ mock.Mock(wraps=self.replaced_symbol),
+ )
+
+ def setup_mocked_symbol(self, mocked_symbol):
+ """Setup a mocked symbol for later use."""
+ pass
+
+ def __enter__(self):
+ for patcher in self.patchers:
+ mocked_symbol = patcher.start()
+ self.setup_mocked_symbol(mocked_symbol)
+
+ def __exit__(self, exc_type=None, exc_val=None, exc_tb=None):
+ for patcher in self.patchers:
+ patcher.stop()
+
+
+class mocked_date_today(MultiModulePatcher):
+ """A context processor changing the value of date.today()."""
+ replaced_symbol = datetime.date
+ replaced_symbol_module = datetime
+ module_imported_as = 'datetime'
+
+ def __init__(self, target_date, *target_modules, **kwargs):
+ self.target_date = target_date
+ super(mocked_date_today, self).__init__(*target_modules, **kwargs)
+
+ def setup_mocked_symbol(self, mocked_date):
+ mocked_date.today.return_value = self.target_date
+
+
+class mocked_datetime_now(MultiModulePatcher):
+ replaced_symbol = datetime.datetime
+ replaced_symbol_module = datetime
+ module_imported_as = 'datetime'
+
+ def __init__(self, target_dt, *target_modules, **kwargs):
+ self.target_dt = target_dt
+ super(mocked_datetime_now, self).__init__(*target_modules, **kwargs)
+
+ def setup_mocked_symbol(self, mocked_datetime):
+ mocked_datetime.now.return_value = self.target_dt