From b6ecfdf7b8f0b05b3e78fe3aafaf26d9c00c3259 Mon Sep 17 00:00:00 2001 From: Saul Shanabrook Date: Tue, 16 Apr 2013 10:49:31 -0300 Subject: Add FuzzyDate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- docs/fuzzy.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) (limited to 'docs') diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index f1f4085..7d56995 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -70,6 +70,33 @@ FuzzyInteger int, the inclusive higher bound of generated integers +FuzzyDate +--------- + +.. class:: FuzzyDate(start_date[, end_date]) + + The :class:`FuzzyDate` fuzzer generates random dates within a given + inclusive range. + + The :attr:`end_date` bound may be omitted, in which case it defaults to the current date: + + .. code-block:: pycon + + >>> fd = FuzzyDate(datetime.date(2008, 1, 1)) + >>> fd.start_date, fd.end_date + datetime.date(2008, 1, 1), datetime.date(2013, 4, 16) + + .. attribute:: start_date + + :class:`datetime.date`, the inclusive lower bound of generated dates + + .. attribute:: end_date + + :class:`datetime.date`, the inclusive higher bound of generated dates + + int, the inclusive higher bound of generated dates + + Custom fuzzy fields ------------------- -- cgit v1.2.3 From a48f6482f21994a44da14dafdeff8e7e1237cec9 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Sun, 21 Apr 2013 19:35:43 +0200 Subject: Add FuzzyDateTime/FuzzyNaiveDateTime. --- docs/fuzzy.rst | 131 ++++++++++++++++++++++++++++- factory/compat.py | 28 +++++++ factory/fuzzy.py | 102 ++++++++++++++++++++++ tests/test_fuzzy.py | 238 ++++++++++++++++++++++++++++++++++++++++++++++++++++ tests/utils.py | 100 ++++++++++++++++++++++ 5 files changed, 598 insertions(+), 1 deletion(-) create mode 100644 tests/utils.py (limited to 'docs') 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 -- cgit v1.2.3 From 31f6f666a02bc3b19836ab996295587de2542f11 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Sun, 21 Apr 2013 19:40:40 +0200 Subject: Update changelog. --- docs/changelog.rst | 8 ++++++++ factory/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 173c40f..f072b1c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,14 @@ ChangeLog ========= +2.1.0 (current) +--------------- + +*New:* + + - Add :class:`~factory.fuzzy.FuzzyDate` thanks to `saulshanabrook `_ + - Add :class:`~factory.fuzzy.FuzzyDateTime` and :class:`~factory.fuzzy.FuzzyNaiveDateTime`. + 2.0.2 (2013-04-16) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index e1138fa..2c81705 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.0.2' +__version__ = '2.1.0-dev' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From b5f0579a9050b38392f45eb8e29b51e46928584a Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Sun, 21 Apr 2013 19:53:35 +0200 Subject: docs: add note to SubFactory/RelatedFactory. --- docs/reference.rst | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/reference.rst b/docs/reference.rst index 81aa645..ad91a8d 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -586,7 +586,7 @@ handles more complex cases: SubFactory """""""""" -.. class:: SubFactory(sub_factory, **kwargs) +.. class:: SubFactory(factory, **kwargs) .. OHAI_VIM** @@ -601,6 +601,18 @@ The :class:`SubFactory` attribute should be called with: factory +.. note:: When passing an actual :class:`~factory.Factory` for the + :attr:`~factory.SubFactory.factory` argument, make sure to pass + the class and not instance (i.e no ``()`` after the class): + + .. code-block:: python + + class FooFactory(factory.Factory): + FACTORY_FOR = Foo + + bar = factory.SubFactory(BarFactory) # Not BarFactory() + + Definition ~~~~~~~~~~ @@ -1040,6 +1052,18 @@ RelatedFactory keyword: +.. note:: When passing an actual :class:`~factory.Factory` for the + :attr:`~factory.RelatedFactory.factory` argument, make sure to pass + the class and not instance (i.e no ``()`` after the class): + + .. code-block:: python + + class FooFactory(factory.Factory): + FACTORY_FOR = Foo + + bar = factory.RelatedFactory(BarFactory) # Not BarFactory() + + .. code-block:: python class CityFactory(factory.Factory): -- cgit v1.2.3 From 38dfde05f5be3cdd69e6fee66e7968b776b0ed9f Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Mon, 29 Apr 2013 23:00:32 +0200 Subject: declarations: Rename RelatedFactory.name (See #58). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use less conflict-prone factory_related_name. Signed-off-by: Raphaël Barrois --- docs/changelog.rst | 4 ++++ docs/reference.rst | 4 ++-- factory/declarations.py | 14 ++++++++++++-- tests/test_declarations.py | 11 +++++++++++ 4 files changed, 29 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index f072b1c..da22b13 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,10 @@ ChangeLog - Add :class:`~factory.fuzzy.FuzzyDate` thanks to `saulshanabrook `_ - Add :class:`~factory.fuzzy.FuzzyDateTime` and :class:`~factory.fuzzy.FuzzyNaiveDateTime`. +*Deprecation:* + + - Rename :class:`~factory.RelatedFactory`'s ``name`` argument to ``factory_related_name`` (See #58) + 2.0.2 (2013-04-16) ------------------ diff --git a/docs/reference.rst b/docs/reference.rst index ad91a8d..09d9ceb 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1025,7 +1025,7 @@ as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory.FACTORY_ RelatedFactory """""""""""""" -.. class:: RelatedFactory(factory, name='', **kwargs) +.. class:: RelatedFactory(factory, factory_related_name='', **kwargs) .. OHAI_VIM** @@ -1045,7 +1045,7 @@ RelatedFactory .. attribute:: name The generated object (where the :class:`RelatedFactory` attribute will - set) may be passed to the related factory if the :attr:`name` parameter + set) may be passed to the related factory if the :attr:`factory_related_name` parameter is set. It will be passed as a keyword argument, using the :attr:`name` value as diff --git a/factory/declarations.py b/factory/declarations.py index 974b4ac..17f4434 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -22,6 +22,7 @@ import itertools +import warnings from . import compat from . import utils @@ -428,9 +429,18 @@ class RelatedFactory(PostGenerationDeclaration): calling the related factory """ - def __init__(self, factory, name='', **defaults): + def __init__(self, factory, factory_related_name='', **defaults): super(RelatedFactory, self).__init__() - self.name = name + 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 if isinstance(factory, type): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 4c08dfa..90e54c2 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -210,6 +210,17 @@ class RelatedFactoryTestCase(unittest.TestCase): # IMPORTANT: restore attribute. datetime.date = orig_date + 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): -- cgit v1.2.3 From 56a5db608f457259f7e69a5e01225adaf7016a31 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Fri, 3 May 2013 01:03:09 +0200 Subject: docs: Add recipe for disconnecting signals (Closes #59). --- docs/recipes.rst | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) (limited to 'docs') diff --git a/docs/recipes.rst b/docs/recipes.rst index e226732..ecdff96 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -62,6 +62,50 @@ When a :class:`UserFactory` is instantiated, factory_boy will call ``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``. +Example: Django's Profile +""""""""""""""""""""""""" + +Django (<1.5) provided a mechanism to attach a ``Profile`` to a ``User`` instance, +using a :class:`~django.db.models.ForeignKey` from the ``Profile`` to the ``User``. + +A typical way to create those profiles was to hook a post-save signal to the ``User`` model. + +factory_boy allows to define attributes of such profiles dynamically when creating a ``User``: + +.. code-block:: python + + class ProfileFactory(factory.DjangoModelFactory): + FACTORY_FOR = my_models.Profile + + title = 'Dr' + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = auth_models.User + + username = factory.Sequence(lambda n: "user_%d" % n) + profile = factory.RelatedFactory(ProfileFactory) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + """Override the default _create() to disable the post-save signal.""" + post_save.disconnect(handler_create_user_profile, auth_models.User) + user = super(UserFactory, cls)._create(target_class, *args, **kwargs) + post_save.connect(handler_create_user_profile, auth_models.User) + return user + +.. OHAI_VIM:* + + +.. code-block:: pycon + + >>> u = UserFactory(profile__title=u"Lord") + >>> u.get_profile().title + u"Lord" + +Such behaviour can be extended to other situations where a signal interferes with +factory_boy related factories. + + Simple ManyToMany ----------------- -- cgit v1.2.3 From 2c8d2cd5c9bf3074bd70559a850d77bd3a28ae8d Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Fri, 3 May 2013 01:15:21 +0200 Subject: Add LazyStub.factory_parent (See #55). --- docs/changelog.rst | 3 +++ docs/reference.rst | 18 ++++++++++++++++++ factory/containers.py | 1 + tests/test_containers.py | 10 ++++++++++ 4 files changed, 32 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index da22b13..0baa3f7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,9 @@ ChangeLog - Add :class:`~factory.fuzzy.FuzzyDate` thanks to `saulshanabrook `_ - Add :class:`~factory.fuzzy.FuzzyDateTime` and :class:`~factory.fuzzy.FuzzyNaiveDateTime`. + - Add a :attr:`~factory.containers.LazyStub.factory_parent` attribute to the + :class:`~factory.containers.LazyStub` passed to :class:`~factory.LazyAttribute`, in order to access + fields defined in wrapping factories. *Deprecation:* diff --git a/docs/reference.rst b/docs/reference.rst index 09d9ceb..ad921fa 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -353,6 +353,11 @@ accept the object being built as sole argument, and return a value. 'leo@example.com' +The object passed to :class:`LazyAttribute` is not an instance of the target class, +but instead a :class:`~containers.LazyStub`: a temporary container that computes +the value of all declared fields. + + Decorator ~~~~~~~~~ @@ -798,6 +803,19 @@ Obviously, this "follow parents" hability also handles overriding some attribute 'cn' +This feature is also available to :class:`LazyAttribute` and :class:`LazyAttributeSequence`, +through the :attr:`~containers.LazyStub.factory_parent` attribute of the passed-in object: + +.. code-block:: python + + class CompanyFactory(factory.Factory): + FACTORY_FOR = Company + country = factory.SubFactory(CountryFactory) + owner = factory.SubFactory(UserFactory, + language=factory.LazyAttribute(lambda user: user.factory_parent.country.language), + ) + + Iterator """""""" diff --git a/factory/containers.py b/factory/containers.py index ee2ad82..b3b003e 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -55,6 +55,7 @@ class LazyStub(object): self.__pending = [] self.__containers = containers self.__target_class = target_class + self.factory_parent = containers[0] if containers else None self.__initialized = True def __repr__(self): diff --git a/tests/test_containers.py b/tests/test_containers.py index 70ed885..75e3237 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -68,6 +68,16 @@ class LazyStubTestCase(unittest.TestCase): containers=()) self.assertEqual(2, stub.one) + def test_access_parent(self): + """Test simple access to a stub' parent.""" + o1 = containers.LazyStub({'rank': 1}) + o2 = containers.LazyStub({'rank': 2}, (o1,)) + stub = containers.LazyStub({'rank': 3}, (o2, o1)) + + self.assertEqual(3, stub.rank) + self.assertEqual(2, stub.factory_parent.rank) + self.assertEqual(1, stub.factory_parent.factory_parent.rank) + def test_cyclic_definition(self): class LazyAttr(containers.LazyValue): def __init__(self, attrname): -- cgit v1.2.3 From 639e5cd1c6baf1cb19d9134545e29fbb5ba16d99 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Sun, 9 Jun 2013 18:05:20 +0200 Subject: Move DjangoModelFactory / MogoFactory to their own modules. --- README | 4 +- docs/changelog.rst | 18 +++++---- docs/introduction.rst | 2 +- docs/orms.rst | 32 ++++++++++++++-- docs/recipes.rst | 24 ++++++------ docs/reference.rst | 14 ++++--- factory/__init__.py | 5 ++- factory/base.py | 77 -------------------------------------- factory/django.py | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++ factory/mogo.py | 45 +++++++++++++++++++++++ tests/test_using.py | 30 +++++++-------- 11 files changed, 225 insertions(+), 126 deletions(-) create mode 100644 factory/django.py create mode 100644 factory/mogo.py (limited to 'docs') diff --git a/README b/README index aaf0b34..06f3393 100644 --- a/README +++ b/README @@ -189,8 +189,8 @@ ORM Support factory_boy has specific support for a few ORMs, through specific :class:`~factory.Factory` subclasses: -* Django, with :class:`~factory.DjangoModelFactory` -* Mogo, with :class:`~factory.MogoFactory` +* Django, with :class:`~factory.django.DjangoModelFactory` +* Mogo, with :class:`~factory.mogo.MogoFactory` Contributing ------------ diff --git a/docs/changelog.rst b/docs/changelog.rst index 0baa3f7..e820d8c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,8 @@ ChangeLog - Add a :attr:`~factory.containers.LazyStub.factory_parent` attribute to the :class:`~factory.containers.LazyStub` passed to :class:`~factory.LazyAttribute`, in order to access fields defined in wrapping factories. + - Move :class:`~factory.django.DjangoModelFactory` and :class:`~factory.mogo.MogoFactory` + to their own modules (:mod:`factory.django` and :mod:`factory.mogo`) *Deprecation:* @@ -21,7 +23,7 @@ ChangeLog *New:* - - When :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is + - When :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is empty, use ``Model.objects.create()`` instead of ``Model.objects.get_or_create``. @@ -31,7 +33,7 @@ ChangeLog *New:* - Don't push ``defaults`` to ``get_or_create`` when - :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set. + :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set. 2.0.0 (2013-04-15) @@ -44,8 +46,8 @@ ChangeLog - The default :attr:`~factory.Sequence.type` for :class:`~factory.Sequence` is now :obj:`int` - Fields listed in :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` won't be passed to the associated class' constructor - - Add support for ``get_or_create`` in :class:`~factory.DjangoModelFactory`, - through :attr:`~factory.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. + - Add support for ``get_or_create`` in :class:`~factory.django.DjangoModelFactory`, + through :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. - Add support for :mod:`~factory.fuzzy` attribute definitions. - The :class:`Sequence` counter can be overridden when calling a generating function - Add :class:`~factory.Dict` and :class:`~factory.List` declarations (Closes #18). @@ -75,7 +77,7 @@ New - **Global:** - Rewrite the whole documentation - - Provide a dedicated :class:`~factory.MogoFactory` subclass of :class:`~factory.Factory` + - Provide a dedicated :class:`~factory.mogo.MogoFactory` subclass of :class:`~factory.Factory` - **The Factory class:** - Better creation/building customization hooks at :meth:`factory.Factory._build` and :meth:`factory.Factory.create` @@ -92,7 +94,7 @@ New its :attr:`~factory.Iterator.cycle` argument to ``False`` - Allow overriding default arguments in a :class:`~factory.PostGenerationMethodCall` when generating an instance of the factory - - An object created by a :class:`~factory.DjangoModelFactory` will be saved + - An object created by a :class:`~factory.django.DjangoModelFactory` will be saved again after :class:`~factory.PostGeneration` hooks execution @@ -127,7 +129,7 @@ In order to upgrade client code, apply the following rules: :class:`~factory.Factory`, instead of relying on automagic associated class discovery - When using factory_boy for Django models, have each factory inherit from - :class:`~factory.DjangoModelFactory` + :class:`~factory.django.DjangoModelFactory` - Replace ``factory.CircularSubFactory('some.module', 'Symbol')`` with ``factory.SubFactory('some.module.Symbol')`` - Replace ``factory.InfiniteIterator(iterable)`` with ``factory.Iterator(iterable)`` @@ -216,7 +218,7 @@ In order to upgrade client code, apply the following rules: - Introduce :data:`~factory.MOGO_BUILD` build function - Add support for inheriting from multiple :class:`~factory.Factory` - Base :class:`~factory.Factory` classes can now be declared :attr:`abstract `. - - Provide :class:`~factory.DjangoModelFactory`, whose :class:`~factory.Sequence` counter starts at the next free database id + - Provide :class:`~factory.django.DjangoModelFactory`, whose :class:`~factory.Sequence` counter starts at the next free database id - Introduce :class:`~factory.SelfAttribute`, a shortcut for ``factory.LazyAttribute(lambda o: o.foo.bar.baz``. *Bugfix:* diff --git a/docs/introduction.rst b/docs/introduction.rst index 8bbb10c..356e11f 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -227,7 +227,7 @@ All factories support two built-in strategies: as for a Django model. Starting from 2.0, :meth:`factory.Factory.create` simply calls ``AssociatedClass(**kwargs)``. - You should use :class:`~factory.DjangoModelFactory` for Django models. + You should use :class:`~factory.django.DjangoModelFactory` for Django models. When a :class:`~factory.Factory` includes related fields (:class:`~factory.SubFactory`, :class:`~factory.RelatedFactory`), diff --git a/docs/orms.rst b/docs/orms.rst index 8e5b6f6..9cf7f8e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -11,6 +11,8 @@ adding dedicated features. Django ------ +.. currentmodule:: factory.django + The first versions of factory_boy were designed specifically for Django, but the library has now evolved to be framework-independant. @@ -24,14 +26,14 @@ All factories for a Django :class:`~django.db.models.Model` should use the :class:`DjangoModelFactory` base class. -.. class:: DjangoModelFactory(Factory) +.. class:: DjangoModelFactory(factory.Factory) Dedicated class for Django :class:`~django.db.models.Model` factories. This class provides the following features: - * :func:`~Factory.create()` uses :meth:`Model.objects.create() ` - * :func:`~Factory._setup_next_sequence()` selects the next unused primary key value + * :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. @@ -44,7 +46,7 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. code-block:: python - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User FACTORY_DJANGO_GET_OR_CREATE = ('username',) @@ -68,3 +70,25 @@ All factories for a Django :class:`~django.db.models.Model` should use the >>> User.objects.all() [, ] + + +Mogo +---- + +.. currentmodule:: factory.mogo + +factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` class. + +`Mogo`_ is a wrapper around the ``pymongo`` library for MongoDB. + +.. _Mogo: https://github.com/joshmarshall/mogo + +.. class:: MogoFactory(factory.Factory) + + Dedicated class for `Mogo`_ models. + + This class provides the following features: + + * :func:`~factory.Factory.build()` calls a model's ``new()`` method + * :func:`~factory.Factory.create()` builds an instance through ``new()`` then + saves it. diff --git a/docs/recipes.rst b/docs/recipes.rst index ecdff96..82f0658 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -25,7 +25,7 @@ use the :class:`~factory.SubFactory` declaration: import factory from . import models - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User first_name = factory.Sequence(lambda n: "Agent %03d" % n) @@ -52,7 +52,7 @@ use a :class:`~factory.RelatedFactory` declaration: # factories.py - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) @@ -74,12 +74,12 @@ factory_boy allows to define attributes of such profiles dynamically when creati .. code-block:: python - class ProfileFactory(factory.DjangoModelFactory): + class ProfileFactory(factory.django.DjangoModelFactory): FACTORY_FOR = my_models.Profile title = 'Dr' - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = auth_models.User username = factory.Sequence(lambda n: "user_%d" % n) @@ -129,12 +129,12 @@ hook: # factories.py - class GroupFactory(factory.DjangoModelFactory): + class GroupFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.Group name = factory.Sequence(lambda n: "Group #%s" % n) - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User name = "John Doe" @@ -184,17 +184,17 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: # factories.py - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User name = "John Doe" - class GroupFactory(factory.DjangoModelFactory): + class GroupFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.Group name = "Admins" - class GroupLevelFactory(factory.DjangoModelFactory): + class GroupLevelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.GroupLevel user = factory.SubFactory(UserFactory) @@ -257,20 +257,20 @@ Here, we want: .. code-block:: python # factories.py - class CountryFactory(factory.DjangoModelFactory): + class CountryFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.Country name = factory.Iterator(["France", "Italy", "Spain"]) lang = factory.Iterator(['fr', 'it', 'es']) - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User name = "John" lang = factory.SelfAttribute('country.lang') country = factory.SubFactory(CountryFactory) - class CompanyFactory(factory.DjangoModelFactory): + class CompanyFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.Company name = "ACME, Inc." diff --git a/docs/reference.rst b/docs/reference.rst index ad921fa..377feb1 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -606,7 +606,9 @@ The :class:`SubFactory` attribute should be called with: factory -.. note:: When passing an actual :class:`~factory.Factory` for the +.. note:: + + When passing an actual :class:`~factory.Factory` for the :attr:`~factory.SubFactory.factory` argument, make sure to pass the class and not instance (i.e no ``()`` after the class): @@ -1070,7 +1072,9 @@ RelatedFactory keyword: -.. note:: When passing an actual :class:`~factory.Factory` for the +.. note:: + + When passing an actual :class:`~factory.Factory` for the :attr:`~factory.RelatedFactory.factory` argument, make sure to pass the class and not instance (i.e no ``()`` after the class): @@ -1266,7 +1270,7 @@ factory during instantiation. .. code-block:: python - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = User username = 'user' @@ -1357,12 +1361,12 @@ Lightweight factory declaration UserFactory = make_factory(models.User, login='john', email=factory.LazyAttribute(lambda u: '%s@example.com' % u.login), - FACTORY_CLASS=factory.DjangoModelFactory, + FACTORY_CLASS=factory.django.DjangoModelFactory, ) # This is equivalent to: - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.User login = 'john' diff --git a/factory/__init__.py b/factory/__init__.py index 2c81705..9eb3b7b 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -29,9 +29,7 @@ from .base import ( DictFactory, BaseListFactory, ListFactory, - MogoFactory, StubFactory, - DjangoModelFactory, BUILD_STRATEGY, CREATE_STRATEGY, @@ -39,6 +37,9 @@ from .base import ( use_strategy, ) +from .mogo import MogoFactory +from .django import DjangoModelFactory + from .declarations import ( LazyAttribute, Iterator, diff --git a/factory/base.py b/factory/base.py index 2ff2944..23fdac7 100644 --- a/factory/base.py +++ b/factory/base.py @@ -554,83 +554,6 @@ class StubFactory(Factory): raise UnsupportedStrategy() -class DjangoModelFactory(Factory): - """Factory for Django models. - - This makes sure that the 'sequence' field of created objects is a new id. - - Possible improvement: define a new 'attribute' type, AutoField, which would - handle those for non-numerical primary keys. - """ - - ABSTRACT_FACTORY = True - FACTORY_DJANGO_GET_OR_CREATE = () - - @classmethod - def _get_manager(cls, target_class): - try: - return target_class._default_manager # pylint: disable=W0212 - except AttributeError: - return target_class.objects - - @classmethod - def _setup_next_sequence(cls): - """Compute the next available PK, based on the 'pk' database field.""" - - model = cls._associated_class # pylint: disable=E1101 - manager = cls._get_manager(model) - - try: - return 1 + manager.values_list('pk', flat=True - ).order_by('-pk')[0] - except IndexError: - return 1 - - @classmethod - def _get_or_create(cls, target_class, *args, **kwargs): - """Create an instance of the model through objects.get_or_create.""" - manager = cls._get_manager(target_class) - - assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, ( - "'defaults' is a reserved keyword for get_or_create " - "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" - % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) - - key_fields = {} - for field in cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields[field] = kwargs.pop(field) - key_fields['defaults'] = kwargs - - obj, _created = manager.get_or_create(*args, **key_fields) - return obj - - @classmethod - def _create(cls, target_class, *args, **kwargs): - """Create an instance of the model, and save it to the database.""" - manager = cls._get_manager(target_class) - - if cls.FACTORY_DJANGO_GET_OR_CREATE: - return cls._get_or_create(target_class, *args, **kwargs) - - return manager.create(*args, **kwargs) - - @classmethod - def _after_postgeneration(cls, obj, create, results=None): - """Save again the instance if creating and at least one hook ran.""" - if create and results: - # Some post-generation hooks ran, and may have modified us. - obj.save() - - -class MogoFactory(Factory): - """Factory for mogo objects.""" - ABSTRACT_FACTORY = True - - @classmethod - def _build(cls, target_class, *args, **kwargs): - return target_class.new(*args, **kwargs) - - class BaseDictFactory(Factory): """Factory for dictionary-like classes.""" ABSTRACT_FACTORY = True diff --git a/factory/django.py b/factory/django.py new file mode 100644 index 0000000..351cb76 --- /dev/null +++ b/factory/django.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. + + +from __future__ import unicode_literals + + +"""factory_boy extensions for use with the Django framework.""" + + +from . import base + + +class DjangoModelFactory(base.Factory): + """Factory for Django models. + + This makes sure that the 'sequence' field of created objects is a new id. + + Possible improvement: define a new 'attribute' type, AutoField, which would + handle those for non-numerical primary keys. + """ + + ABSTRACT_FACTORY = True + FACTORY_DJANGO_GET_OR_CREATE = () + + @classmethod + def _get_manager(cls, target_class): + try: + return target_class._default_manager # pylint: disable=W0212 + except AttributeError: + return target_class.objects + + @classmethod + def _setup_next_sequence(cls): + """Compute the next available PK, based on the 'pk' database field.""" + + model = cls._associated_class # pylint: disable=E1101 + manager = cls._get_manager(model) + + try: + return 1 + manager.values_list('pk', flat=True + ).order_by('-pk')[0] + except IndexError: + return 1 + + @classmethod + def _get_or_create(cls, target_class, *args, **kwargs): + """Create an instance of the model through objects.get_or_create.""" + manager = cls._get_manager(target_class) + + assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, ( + "'defaults' is a reserved keyword for get_or_create " + "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" + % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) + + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs + + obj, _created = manager.get_or_create(*args, **key_fields) + return obj + + @classmethod + def _create(cls, target_class, *args, **kwargs): + """Create an instance of the model, and save it to the database.""" + manager = cls._get_manager(target_class) + + if cls.FACTORY_DJANGO_GET_OR_CREATE: + return cls._get_or_create(target_class, *args, **kwargs) + + return manager.create(*args, **kwargs) + + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Save again the instance if creating and at least one hook ran.""" + if create and results: + # Some post-generation hooks ran, and may have modified us. + obj.save() + + diff --git a/factory/mogo.py b/factory/mogo.py new file mode 100644 index 0000000..48d9677 --- /dev/null +++ b/factory/mogo.py @@ -0,0 +1,45 @@ +# -*- 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. + + +from __future__ import unicode_literals + + +"""factory_boy extensions for use with the mogo library (pymongo wrapper).""" + + +from . import base + + +class MogoFactory(base.Factory): + """Factory for mogo objects.""" + ABSTRACT_FACTORY = True + + @classmethod + def _build(cls, target_class, *args, **kwargs): + return target_class.new(*args, **kwargs) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + instance = target_class.new(*args, **kwargs) + instance.save() + return instance diff --git a/tests/test_using.py b/tests/test_using.py index 821fad3..a3f5322 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -125,7 +125,7 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.foo, 'bar') def test_create_custom_base(self): - obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory) + obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -141,7 +141,7 @@ class SimpleBuildTestCase(unittest.TestCase): def test_create_batch_custom_base(self): objs = factory.create_batch(FakeModel, 4, foo='bar', - FACTORY_CLASS=factory.DjangoModelFactory) + FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(4, len(objs)) self.assertEqual(4, len(set(objs))) @@ -177,7 +177,7 @@ class SimpleBuildTestCase(unittest.TestCase): def test_generate_create_custom_base(self): obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar', - FACTORY_CLASS=factory.DjangoModelFactory) + FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -208,7 +208,7 @@ class SimpleBuildTestCase(unittest.TestCase): def test_generate_batch_create_custom_base(self): objs = factory.generate_batch(FakeModel, factory.CREATE_STRATEGY, 20, foo='bar', - FACTORY_CLASS=factory.DjangoModelFactory) + FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) @@ -238,7 +238,7 @@ class SimpleBuildTestCase(unittest.TestCase): self.assertEqual(obj.foo, 'bar') def test_simple_generate_create_custom_base(self): - obj = factory.simple_generate(FakeModel, True, foo='bar', FACTORY_CLASS=factory.DjangoModelFactory) + obj = factory.simple_generate(FakeModel, True, foo='bar', FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -264,7 +264,7 @@ class SimpleBuildTestCase(unittest.TestCase): def test_simple_generate_batch_create_custom_base(self): objs = factory.simple_generate_batch(FakeModel, True, 20, foo='bar', - FACTORY_CLASS=factory.DjangoModelFactory) + FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) @@ -1298,7 +1298,7 @@ class BetterFakeModel(object): class DjangoModelFactoryTestCase(unittest.TestCase): def test_simple(self): - class FakeModelFactory(factory.DjangoModelFactory): + class FakeModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = FakeModel obj = FakeModelFactory(one=1) @@ -1312,7 +1312,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1}, prev) - class MyFakeModelFactory(factory.DjangoModelFactory): + class MyFakeModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = MyFakeModel FACTORY_DJANGO_GET_OR_CREATE = ('x',) x = 1 @@ -1333,7 +1333,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev) - class MyFakeModelFactory(factory.DjangoModelFactory): + class MyFakeModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = MyFakeModel FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z') x = 1 @@ -1354,7 +1354,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1}, prev) - class MyFakeModelFactory(factory.DjangoModelFactory): + class MyFakeModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = MyFakeModel FACTORY_DJANGO_GET_OR_CREATE = ('x',) x = 1 @@ -1375,7 +1375,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev) - class MyFakeModelFactory(factory.DjangoModelFactory): + class MyFakeModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = MyFakeModel FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z') x = 1 @@ -1769,7 +1769,7 @@ class ListTestCase(unittest.TestCase): class DjangoModelFactoryTestCase(unittest.TestCase): def test_sequence(self): - class TestModelFactory(factory.DjangoModelFactory): + class TestModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = TestModel a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1787,7 +1787,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): self.assertEqual('foo_5', o4.a) def test_no_get_or_create(self): - class TestModelFactory(factory.DjangoModelFactory): + class TestModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = TestModel a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1798,7 +1798,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): self.assertEqual(2, o.id) def test_get_or_create(self): - class TestModelFactory(factory.DjangoModelFactory): + class TestModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = TestModel FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b') @@ -1817,7 +1817,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): def test_full_get_or_create(self): """Test a DjangoModelFactory with all fields in get_or_create.""" - class TestModelFactory(factory.DjangoModelFactory): + class TestModelFactory(factory.django.DjangoModelFactory): FACTORY_FOR = TestModel FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b', 'c', 'd') -- cgit v1.2.3 From 85dde20cf2e337a4e0b7de47d067edfaf2e633ab Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Mon, 10 Jun 2013 01:18:54 +0200 Subject: Add Factory.reset_sequence. --- docs/changelog.rst | 2 ++ docs/reference.rst | 42 ++++++++++++++++++++++++++++ factory/base.py | 15 ++++++++++ tests/test_base.py | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index e820d8c..da3c4dc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,8 @@ ChangeLog fields defined in wrapping factories. - Move :class:`~factory.django.DjangoModelFactory` and :class:`~factory.mogo.MogoFactory` to their own modules (:mod:`factory.django` and :mod:`factory.mogo`) + - Add the :meth:`~factory.Factory.reset_sequence` classmethod to :class:`~factory.Factory` + to ease resetting the sequence counter for a given factory. *Deprecation:* diff --git a/docs/reference.rst b/docs/reference.rst index 377feb1..a2d6c9a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -234,6 +234,48 @@ The :class:`Factory` class values, for instance. + **Advanced functions:** + + + .. classmethod:: reset_sequence(cls, value=None, force=False) + + :arg int value: The value to reset the sequence to + :arg bool force: Whether to force-reset the sequence + + Allows to reset the sequence counter for a :class:`~factory.Factory`. + The new value can be passed in as the ``value`` argument: + + .. code-block:: pycon + + >>> SomeFactory.reset_sequence(4) + >>> SomeFactory._next_sequence + 4 + + Since subclasses of a non-:attr:`abstract ` + :class:`~factory.Factory` share the same sequence counter, special care needs + to be taken when resetting the counter of such a subclass. + + By default, :meth:`reset_sequence` will raise a :exc:`ValueError` when + called on a subclassed :class:`~factory.Factory` subclass. This can be + avoided by passing in the ``force=True`` flag: + + .. code-block:: pycon + + >>> InheritedFactory.reset_sequence() + Traceback (most recent call last): + File "factory_boy/tests/test_base.py", line 179, in test_reset_sequence_subclass_parent + SubTestObjectFactory.reset_sequence() + File "factory_boy/factory/base.py", line 250, in reset_sequence + "Cannot reset the sequence of a factory subclass. " + ValueError: Cannot reset the sequence of a factory subclass. Please call reset_sequence() on the root factory, or call reset_sequence(forward=True). + + >>> InheritedFactory.reset_sequence(force=True) + >>> + + This is equivalent to calling :meth:`reset_sequence` on the base + factory in the chain. + + .. _strategies: Strategies diff --git a/factory/base.py b/factory/base.py index 23fdac7..ff5404f 100644 --- a/factory/base.py +++ b/factory/base.py @@ -239,6 +239,21 @@ class BaseFactory(object): # List of attributes that should not be passed to the underlying class FACTORY_HIDDEN_ARGS = () + @classmethod + def reset_sequence(cls, value=None, force=False): + """Reset the sequence counter.""" + if cls._base_factory: + if force: + cls._base_factory.reset_sequence(value=value) + else: + raise ValueError( + "Cannot reset the sequence of a factory subclass. " + "Please call reset_sequence() on the root factory, " + "or call reset_sequence(forward=True)." + ) + else: + cls._next_sequence = value + @classmethod def _setup_next_sequence(cls): """Set up an initial sequence value for Sequence attributes. diff --git a/tests/test_base.py b/tests/test_base.py index 73e59fa..4978d10 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -34,6 +34,7 @@ class TestObject(object): self.three = three self.four = four + class FakeDjangoModel(object): @classmethod def create(cls, **kwargs): @@ -46,6 +47,7 @@ class FakeDjangoModel(object): setattr(self, name, value) self.id = None + class FakeModelFactory(base.Factory): ABSTRACT_FACTORY = True @@ -116,6 +118,84 @@ class FactoryTestCase(unittest.TestCase): self.assertEqual(4, len(ones)) +class FactorySequenceTestCase(unittest.TestCase): + def setUp(self): + super(FactorySequenceTestCase, self).setUp() + + class TestObjectFactory(base.Factory): + FACTORY_FOR = TestObject + one = declarations.Sequence(lambda n: n) + + self.TestObjectFactory = TestObjectFactory + + def test_reset_sequence(self): + o1 = self.TestObjectFactory() + self.assertEqual(0, o1.one) + + o2 = self.TestObjectFactory() + self.assertEqual(1, o2.one) + + self.TestObjectFactory.reset_sequence() + o3 = self.TestObjectFactory() + self.assertEqual(0, o3.one) + + def test_reset_sequence_with_value(self): + o1 = self.TestObjectFactory() + self.assertEqual(0, o1.one) + + o2 = self.TestObjectFactory() + self.assertEqual(1, o2.one) + + self.TestObjectFactory.reset_sequence(42) + o3 = self.TestObjectFactory() + self.assertEqual(42, o3.one) + + def test_reset_sequence_subclass_fails(self): + """Tests that the sequence of a 'slave' factory cannot be reseted.""" + class SubTestObjectFactory(self.TestObjectFactory): + pass + + self.assertRaises(ValueError, SubTestObjectFactory.reset_sequence) + + def test_reset_sequence_subclass_force(self): + """Tests that reset_sequence(force=True) works.""" + class SubTestObjectFactory(self.TestObjectFactory): + pass + + o1 = SubTestObjectFactory() + self.assertEqual(0, o1.one) + + o2 = SubTestObjectFactory() + self.assertEqual(1, o2.one) + + SubTestObjectFactory.reset_sequence(force=True) + o3 = SubTestObjectFactory() + self.assertEqual(0, o3.one) + + # The master sequence counter has been reset + o4 = self.TestObjectFactory() + self.assertEqual(1, o4.one) + + def test_reset_sequence_subclass_parent(self): + """Tests that the sequence of a 'slave' factory cannot be reseted.""" + class SubTestObjectFactory(self.TestObjectFactory): + pass + + o1 = SubTestObjectFactory() + self.assertEqual(0, o1.one) + + o2 = SubTestObjectFactory() + self.assertEqual(1, o2.one) + + self.TestObjectFactory.reset_sequence() + o3 = SubTestObjectFactory() + self.assertEqual(0, o3.one) + + o4 = self.TestObjectFactory() + self.assertEqual(1, o4.one) + + + class FactoryDefaultStrategyTestCase(unittest.TestCase): def setUp(self): self.default_strategy = base.Factory.FACTORY_STRATEGY -- cgit v1.2.3 From 1f90c656089326228ef4aaf3d634cc843fad14b2 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Mon, 10 Jun 2013 01:30:04 +0200 Subject: Update ChangeLog (Closed #57). --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index da3c4dc..5d6c4bf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,10 @@ ChangeLog - Add the :meth:`~factory.Factory.reset_sequence` classmethod to :class:`~factory.Factory` to ease resetting the sequence counter for a given factory. +*Bugfix* + + - Properly handle non-integer pks in :class:`~factory.django.DjangoModelFactory` (#57). + *Deprecation:* - Rename :class:`~factory.RelatedFactory`'s ``name`` argument to ``factory_related_name`` (See #58) -- cgit v1.2.3 From 2cb136cfb8ef1d4b3a2cb68c4cbc23547bfc395f Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Fri, 12 Apr 2013 00:14:33 +0200 Subject: Add logging calls (Closes #45). --- README | 28 ++++++++++++++++++++++++++++ docs/changelog.rst | 1 + factory/base.py | 18 +++++++++++++++++- factory/containers.py | 21 ++++++++++++++++----- factory/declarations.py | 35 +++++++++++++++++++++++++++++++++++ factory/utils.py | 7 +++++++ setup.py | 11 ++++++++--- 7 files changed, 112 insertions(+), 9 deletions(-) (limited to 'docs') diff --git a/README b/README index 06f3393..8586e1d 100644 --- a/README +++ b/README @@ -184,6 +184,34 @@ The associated object's strategy will be used: True +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: + +.. code-block:: python + + import logging + logger = logging.getLogger('factory') + logger.addHandler(logging.StreamHandler()) + logger.setLevel(logging.DEBUG) + +This will yield messages similar to those (artificial indentation): + +.. code-block:: ini + + BaseFactory: Preparing tests.test_using.TestModel2Factory(extra={}) + LazyStub: Computing values for tests.test_using.TestModel2Factory(two=>) + SubFactory: Instantiating tests.test_using.TestModelFactory(__containers=(,), one=4), create=True + BaseFactory: Preparing tests.test_using.TestModelFactory(extra={'__containers': (,), 'one': 4}) + LazyStub: Computing values for tests.test_using.TestModelFactory(one=4) + LazyStub: Computed values, got tests.test_using.TestModelFactory(one=4) + BaseFactory: Generating tests.test_using.TestModelFactory(one=4) + LazyStub: Computed values, got tests.test_using.TestModel2Factory(two=) + BaseFactory: Generating tests.test_using.TestModel2Factory(two=) + + ORM Support """"""""""" diff --git a/docs/changelog.rst b/docs/changelog.rst index 5d6c4bf..a08330f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ ChangeLog to their own modules (:mod:`factory.django` and :mod:`factory.mogo`) - Add the :meth:`~factory.Factory.reset_sequence` classmethod to :class:`~factory.Factory` to ease resetting the sequence counter for a given factory. + - Add debug messages to ``factory`` logger. *Bugfix* diff --git a/factory/base.py b/factory/base.py index ff5404f..60aa218 100644 --- a/factory/base.py +++ b/factory/base.py @@ -20,7 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import logging + from . import containers +from . import utils + +logger = logging.getLogger('factory.generate') # Strategies BUILD_STRATEGY = 'build' @@ -300,7 +305,13 @@ class BaseFactory(object): force_sequence = None if extra: force_sequence = extra.pop('__sequence', None) - return containers.AttributeBuilder(cls, extra).build( + log_ctx = '%s.%s' % (cls.__module__, cls.__name__) + logger.debug('BaseFactory: Preparing %s.%s(extra=%r)', + cls.__module__, + cls.__name__, + extra, + ) + return containers.AttributeBuilder(cls, extra, log_ctx=log_ctx).build( create=create, force_sequence=force_sequence, ) @@ -338,6 +349,11 @@ class BaseFactory(object): # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) + logger.debug('BaseFactory: Generating %s.%s(%s)', + cls.__module__, + cls.__name__, + utils.log_pprint(args, kwargs), + ) if create: return cls._create(target_class, *args, **kwargs) else: diff --git a/factory/containers.py b/factory/containers.py index b3b003e..4975036 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -20,6 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import logging + +logger = logging.getLogger(__name__) from . import declarations from . import utils @@ -49,17 +52,18 @@ class LazyStub(object): __initialized = False - def __init__(self, attrs, containers=(), target_class=object): + def __init__(self, attrs, containers=(), target_class=object, log_ctx=None): self.__attrs = attrs self.__values = {} self.__pending = [] self.__containers = containers self.__target_class = target_class + self.__log_ctx = log_ctx or '%s.%s' % (target_class.__module__, target_class.__name__) self.factory_parent = containers[0] if containers else None self.__initialized = True def __repr__(self): - return '' % self.__target_class.__name__ + return '' % (self.__target_class.__module__, self.__target_class.__name__) def __str__(self): return '' % ( @@ -72,8 +76,14 @@ class LazyStub(object): dict: map of attribute name => computed value """ res = {} + logger.debug("LazyStub: Computing values for %s(%s)", + self.__log_ctx, utils.log_pprint(kwargs=self.__attrs), + ) for attr in self.__attrs: res[attr] = getattr(self, attr) + logger.debug("LazyStub: Computed values, got %s(%s)", + self.__log_ctx, utils.log_pprint(kwargs=res), + ) return res def __getattr__(self, name): @@ -219,8 +229,8 @@ class AttributeBuilder(object): overridden default values for the related SubFactory. """ - def __init__(self, factory, extra=None, *args, **kwargs): - super(AttributeBuilder, self).__init__(*args, **kwargs) + def __init__(self, factory, extra=None, log_ctx=None, **kwargs): + super(AttributeBuilder, self).__init__(**kwargs) if not extra: extra = {} @@ -228,6 +238,7 @@ class AttributeBuilder(object): self.factory = factory self._containers = extra.pop('__containers', ()) self._attrs = factory.declarations(extra) + self._log_ctx = log_ctx attrs_with_subfields = [ k for k, v in self._attrs.items() @@ -266,7 +277,7 @@ class AttributeBuilder(object): wrapped_attrs[k] = v stub = LazyStub(wrapped_attrs, containers=self._containers, - target_class=self.factory) + target_class=self.factory, log_ctx=self._log_ctx) return stub.__fill__() diff --git a/factory/declarations.py b/factory/declarations.py index abd384e..dbade97 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -23,11 +23,15 @@ import itertools import warnings +import logging from . import compat from . import utils +logger = logging.getLogger('factory.generate') + + class OrderedDeclaration(object): """A factory declaration. @@ -67,6 +71,7 @@ class LazyAttribute(OrderedDeclaration): self.function = function def evaluate(self, sequence, obj, create, extra=None, containers=()): + logger.debug("LazyAttribute: Evaluating %r on %r", self.function, obj) return self.function(obj) @@ -131,6 +136,8 @@ class SelfAttribute(OrderedDeclaration): target = containers[self.depth - 2] else: target = obj + + logger.debug("SelfAttribute: Picking attribute %r on %r", self.attribute_name, target) return deepgetattr(target, self.attribute_name, self.default) def __repr__(self): @@ -161,6 +168,7 @@ class Iterator(OrderedDeclaration): self.iterator = iter(iterator) def evaluate(self, sequence, obj, create, extra=None, containers=()): + logger.debug("Iterator: Fetching next value from %r", self.iterator) value = next(self.iterator) if self.getter is None: return value @@ -184,6 +192,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) return self.function(self.type(sequence)) @@ -197,6 +206,8 @@ 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", + self.function, sequence, obj) return self.function(obj, self.type(sequence)) @@ -364,6 +375,11 @@ class SubFactory(ParameteredAttribute): override the wrapped factory's defaults """ subfactory = self.get_factory() + logger.debug("SubFactory: Instantiating %s.%s(%s), create=%r", + subfactory.__module__, subfactory.__name__, + utils.log_pprint(kwargs=params), + create, + ) return subfactory.simple_generate(create, **params) @@ -375,6 +391,7 @@ class Dict(SubFactory): def generate(self, sequence, obj, create, params): dict_factory = self.get_factory() + logger.debug("Dict: Building dict(%s)", utils.log_pprint(kwargs=params)) return dict_factory.simple_generate(create, __sequence=sequence, **params) @@ -389,6 +406,9 @@ class List(SubFactory): def generate(self, sequence, obj, create, params): list_factory = self.get_factory() + logger.debug('List: Building list(%s)', + utils.log_pprint(args=[v for _i, v in sorted(params.items())]), + ) return list_factory.simple_generate(create, __sequence=sequence, **params) @@ -435,6 +455,11 @@ class PostGeneration(PostGenerationDeclaration): self.function = function def call(self, obj, create, extracted=None, **kwargs): + logger.debug('PostGeneration: Calling %s.%s(%s)', + self.function.__module__, + self.function.__name__, + utils.log_pprint((obj, create, extracted), kwargs), + ) return self.function(obj, create, extracted, **kwargs) @@ -474,6 +499,11 @@ class RelatedFactory(PostGenerationDeclaration): passed_kwargs[self.name] = obj factory = self.get_factory() + logger.debug('RelatedFactory: Generating %s.%s(%s)', + factory.__module__, + factory.__name__, + utils.log_pprint((create,), passed_kwargs), + ) factory.simple_generate(create, **passed_kwargs) @@ -509,4 +539,9 @@ class PostGenerationMethodCall(PostGenerationDeclaration): passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) + logger.debug('PostGenerationMethodCall: Calling %r.%s(%s)', + obj, + self.method_name, + utils.log_pprint(passed_args, passed_kwargs), + ) method(*passed_args, **passed_kwargs) diff --git a/factory/utils.py b/factory/utils.py index fb8cfef..e1b265f 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -94,3 +94,10 @@ def import_object(module_name, attribute_name): module = __import__(module_name, {}, {}, [attribute_name], 0) return getattr(module, attribute_name) + +def log_pprint(args=(), kwargs=None): + kwargs = kwargs or {} + return ', '.join( + [str(arg) for arg in args] + + ['%s=%r' % item for item in kwargs.items()] + ) diff --git a/setup.py b/setup.py index 050e43b..42519ce 100755 --- a/setup.py +++ b/setup.py @@ -46,10 +46,15 @@ class test(cmd.Command): except ImportError: import unittest - if self.verbose: - verbosity=1 + import logging + logger = logging.getLogger('factory') + logger.addHandler(logging.StreamHandler()) + + verbosity = self.verbose + if verbosity >= 2: + logger.setLevel(logging.DEBUG) else: - verbosity=0 + logger.setLevel(logging.INFO) loader = unittest.TestLoader() suite = unittest.TestSuite() -- cgit v1.2.3 From 251ae29b4beedd7e9af721ceabb82a03f2d55bab Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Fri, 14 Jun 2013 23:34:47 +0200 Subject: doc: Add :issue: markup --- docs/changelog.rst | 8 +++++--- docs/conf.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index a08330f..6489176 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,11 +19,13 @@ ChangeLog *Bugfix* - - Properly handle non-integer pks in :class:`~factory.django.DjangoModelFactory` (#57). + - Properly handle non-integer pks in :class:`~factory.django.DjangoModelFactory` (:issue:`57`). + - Disable :class:`~factory.RelatedFactory` generation when a specific value was + passed (:issue:`62`, thanks to `Gabe Koscky `_) *Deprecation:* - - Rename :class:`~factory.RelatedFactory`'s ``name`` argument to ``factory_related_name`` (See #58) + - Rename :class:`~factory.RelatedFactory`'s ``name`` argument to ``factory_related_name`` (See :issue:`58`) 2.0.2 (2013-04-16) ------------------ @@ -57,7 +59,7 @@ ChangeLog through :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. - Add support for :mod:`~factory.fuzzy` attribute definitions. - The :class:`Sequence` counter can be overridden when calling a generating function - - Add :class:`~factory.Dict` and :class:`~factory.List` declarations (Closes #18). + - Add :class:`~factory.Dict` and :class:`~factory.List` declarations (Closes :issue:`18`). *Removed:* diff --git a/docs/conf.py b/docs/conf.py index 0ccaf29..ee6a739 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,16 @@ sys.path.insert(0, os.path.dirname(os.path.abspath('.'))) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.extlinks', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +extlinks = { + 'issue': ('https://github.com/rbarrois/factory_boy/issues/%s', 'issue #'), +} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -- cgit v1.2.3 From 1ba20b0ed7b920fa2d161df94a0dda3d93b1e14b Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Fri, 14 Jun 2013 23:36:25 +0200 Subject: Properly handle passed-in None in RelatedFactory (Closes #62). Thanks to @Dhekke for the help! --- docs/reference.rst | 16 ++++++++++++++++ factory/base.py | 4 ++-- factory/declarations.py | 39 +++++++++++++++++++++++++++++---------- tests/test_declarations.py | 19 ++++++++++++------- 4 files changed, 59 insertions(+), 19 deletions(-) (limited to 'docs') diff --git a/docs/reference.rst b/docs/reference.rst index a2d6c9a..74f2dbd 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1157,6 +1157,22 @@ Extra kwargs may be passed to the related factory, through the usual ``ATTR__SUB >>> City.objects.get(capital_of=england) +If a value if passed for the :class:`RelatedFactory` attribute, this disables +:class:`RelatedFactory` generation: + +.. code-block:: pycon + + >>> france = CountryFactory() + >>> paris = City.objects.get() + >>> paris + + >>> reunion = CountryFactory(capital_city=paris) + >>> City.objects.count() # No new capital_city generated + 1 + >>> guyane = CountryFactory(capital_city=paris, capital_city__name='Kourou') + >>> City.objects.count() # No new capital_city generated, ``name`` ignored. + 1 + PostGeneration """""""""""""" diff --git a/factory/base.py b/factory/base.py index 60aa218..76d3d4a 100644 --- a/factory/base.py +++ b/factory/base.py @@ -380,9 +380,9 @@ class BaseFactory(object): # Handle post-generation attributes results = {} for name, decl in sorted(postgen_declarations.items()): - extracted, extracted_kwargs = postgen_attributes[name] + did_extract, extracted, extracted_kwargs = postgen_attributes[name] results[name] = decl.call(obj, create, extracted, - **extracted_kwargs) + factory_extracted=did_extract, **extracted_kwargs) cls._after_postgeneration(obj, create, results) diff --git a/factory/declarations.py b/factory/declarations.py index dbade97..f068c0d 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -430,11 +430,18 @@ class PostGenerationDeclaration(object): (object, dict): a tuple containing the attribute at 'name' (if provided) and a dict of extracted attributes """ - extracted = attrs.pop(name, None) + try: + extracted = attrs.pop(name) + did_extract = True + except KeyError: + extracted = None + did_extract = False + kwargs = utils.extract_dict(name, attrs) - return extracted, kwargs + return did_extract, extracted, kwargs - def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover + def call(self, obj, create, + extracted=None, factory_extracted=False, **kwargs): # pragma: no cover """Call this hook; no return value is expected. Args: @@ -454,7 +461,8 @@ class PostGeneration(PostGenerationDeclaration): super(PostGeneration, self).__init__() self.function = function - def call(self, obj, create, extracted=None, **kwargs): + def call(self, obj, create, + extracted=None, factory_extracted=False, **kwargs): logger.debug('PostGeneration: Calling %s.%s(%s)', self.function.__module__, self.function.__name__, @@ -492,19 +500,29 @@ class RelatedFactory(PostGenerationDeclaration): """Retrieve the wrapped factory.Factory subclass.""" return self.factory_wrapper.get() - def call(self, obj, create, extracted=None, **kwargs): + def call(self, obj, create, + extracted=None, factory_extracted=False, **kwargs): + factory = self.get_factory() + + if factory_extracted: + # The user passed in a custom value + logger.debug('RelatedFactory: Using provided %r instead of ' + 'generating %s.%s.', + extracted, factory.__module__, factory.__name__, + ) + return extracted + passed_kwargs = dict(self.defaults) passed_kwargs.update(kwargs) if self.name: passed_kwargs[self.name] = obj - factory = self.get_factory() logger.debug('RelatedFactory: Generating %s.%s(%s)', factory.__module__, factory.__name__, utils.log_pprint((create,), passed_kwargs), ) - factory.simple_generate(create, **passed_kwargs) + return factory.simple_generate(create, **passed_kwargs) class PostGenerationMethodCall(PostGenerationDeclaration): @@ -526,8 +544,9 @@ class PostGenerationMethodCall(PostGenerationDeclaration): self.method_args = args self.method_kwargs = kwargs - def call(self, obj, create, extracted=None, **kwargs): - if extracted is None: + def call(self, obj, create, + extracted=None, factory_extracted=False, **kwargs): + if not factory_extracted: passed_args = self.method_args elif len(self.method_args) <= 1: @@ -544,4 +563,4 @@ class PostGenerationMethodCall(PostGenerationDeclaration): self.method_name, utils.log_pprint(passed_args, passed_kwargs), ) - method(*passed_args, **passed_kwargs) + return method(*passed_args, **passed_kwargs) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index e0b2513..cd38dd2 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -119,7 +119,9 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): def test_extract_no_prefix(self): decl = declarations.PostGenerationDeclaration() - extracted, kwargs = decl.extract('foo', {'foo': 13, 'foo__bar': 42}) + did_extract, extracted, kwargs = decl.extract('foo', + {'foo': 13, 'foo__bar': 42}) + self.assertTrue(did_extract) self.assertEqual(extracted, 13) self.assertEqual(kwargs, {'bar': 42}) @@ -130,8 +132,9 @@ class PostGenerationDeclarationTestCase(unittest.TestCase): call_params.append(args) call_params.append(kwargs) - extracted, kwargs = foo.extract('foo', + did_extract, extracted, kwargs = foo.extract('foo', {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) + self.assertTrue(did_extract) self.assertEqual(13, extracted) self.assertEqual({'bar': 42}, kwargs) @@ -215,17 +218,17 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): def test_call_with_passed_extracted_string(self): decl = declarations.PostGenerationMethodCall( 'method') - decl.call(self.obj, False, 'data') + decl.call(self.obj, False, 'data', factory_extracted=True) self.obj.method.assert_called_once_with('data') def test_call_with_passed_extracted_int(self): decl = declarations.PostGenerationMethodCall('method') - decl.call(self.obj, False, 1) + decl.call(self.obj, False, 1, factory_extracted=True) self.obj.method.assert_called_once_with(1) def test_call_with_passed_extracted_iterable(self): decl = declarations.PostGenerationMethodCall('method') - decl.call(self.obj, False, (1, 2, 3)) + decl.call(self.obj, False, (1, 2, 3), factory_extracted=True) self.obj.method.assert_called_once_with((1, 2, 3)) def test_call_with_method_kwargs(self): @@ -248,13 +251,15 @@ class PostGenerationMethodCallTestCase(unittest.TestCase): def test_multi_call_with_passed_multiple_args(self): decl = declarations.PostGenerationMethodCall( 'method', 'arg1', 'arg2') - decl.call(self.obj, False, ('param1', 'param2', 'param3')) + decl.call(self.obj, False, ('param1', 'param2', 'param3'), + factory_extracted=True) self.obj.method.assert_called_once_with('param1', 'param2', 'param3') def test_multi_call_with_passed_tuple(self): decl = declarations.PostGenerationMethodCall( 'method', 'arg1', 'arg2') - decl.call(self.obj, False, (('param1', 'param2'),)) + decl.call(self.obj, False, (('param1', 'param2'),), + factory_extracted=True) self.obj.method.assert_called_once_with(('param1', 'param2')) def test_multi_call_with_kwargs(self): -- cgit v1.2.3 From fda40cb64041aacdb776e0b1f4f4a635bdc9d70b Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Sat, 15 Jun 2013 00:17:41 +0200 Subject: Add Iterator.reset() (Closes #63). --- docs/changelog.rst | 1 + docs/reference.rst | 26 +++++++++++ factory/declarations.py | 11 +++-- factory/utils.py | 23 ++++++++++ tests/test_declarations.py | 21 +++++++++ tests/test_utils.py | 106 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 6489176..98b177d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,7 @@ ChangeLog - Add the :meth:`~factory.Factory.reset_sequence` classmethod to :class:`~factory.Factory` to ease resetting the sequence counter for a given factory. - Add debug messages to ``factory`` logger. + - Add a :meth:`~factory.Iterator.reset` method to :class:`~factory.Iterator` (:issue:`63`) *Bugfix* diff --git a/docs/reference.rst b/docs/reference.rst index 74f2dbd..e98665f 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -884,6 +884,14 @@ Iterator .. versionadded:: 1.3.0 + .. method:: reset() + + Reset the internal iterator used by the attribute, so that the next value + will be the first value generated by the iterator. + + May be called several times. + + Each call to the factory will receive the next value from the iterable: .. code-block:: python @@ -953,6 +961,24 @@ use the :func:`iterator` decorator: yield line +Resetting +~~~~~~~~~ + +In order to start back at the first value in an :class:`Iterator`, +simply call the :meth:`~Iterator.reset` method of that attribute +(accessing it from the bare :class:`~Factory` subclass): + +.. code-block:: pycon + + >>> UserFactory().lang + 'en' + >>> UserFactory().lang + 'fr' + >>> UserFactory.lang.reset() + >>> UserFactory().lang + 'en' + + Dict and List """"""""""""" diff --git a/factory/declarations.py b/factory/declarations.py index f068c0d..552ddf2 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -163,17 +163,20 @@ class Iterator(OrderedDeclaration): self.getter = getter if cycle: - self.iterator = itertools.cycle(iterator) - else: - self.iterator = iter(iterator) + iterator = itertools.cycle(iterator) + self.iterator = utils.ResetableIterator(iterator) def evaluate(self, sequence, obj, create, extra=None, containers=()): logger.debug("Iterator: Fetching next value from %r", self.iterator) - value = next(self.iterator) + value = next(iter(self.iterator)) if self.getter is None: return value return self.getter(value) + def reset(self): + """Reset the internal iterator.""" + self.iterator.reset() + class Sequence(OrderedDeclaration): """Specific OrderedDeclaration to use for 'sequenced' fields. diff --git a/factory/utils.py b/factory/utils.py index e1b265f..48c6eed 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import collections #: String for splitting an attribute name into a #: (subfactory_name, subfactory_field) tuple. @@ -101,3 +102,25 @@ def log_pprint(args=(), kwargs=None): [str(arg) for arg in args] + ['%s=%r' % item for item in kwargs.items()] ) + + +class ResetableIterator(object): + """An iterator wrapper that can be 'reset()' to its start.""" + def __init__(self, iterator, **kwargs): + super(ResetableIterator, self).__init__(**kwargs) + self.iterator = iter(iterator) + self.past_elements = collections.deque() + self.next_elements = collections.deque() + + def __iter__(self): + while True: + if self.next_elements: + yield self.next_elements.popleft() + else: + value = next(self.iterator) + self.past_elements.append(value) + yield value + + def reset(self): + self.next_elements.clear() + self.next_elements.extend(self.past_elements) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index cd38dd2..9d54c59 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -107,6 +107,27 @@ class IteratorTestCase(unittest.TestCase): self.assertEqual(2, it.evaluate(1, None, False)) self.assertRaises(StopIteration, it.evaluate, 2, None, False) + def test_reset_cycle(self): + it = declarations.Iterator([1, 2]) + self.assertEqual(1, it.evaluate(0, None, False)) + self.assertEqual(2, it.evaluate(1, None, False)) + self.assertEqual(1, it.evaluate(2, None, False)) + self.assertEqual(2, it.evaluate(3, None, False)) + self.assertEqual(1, it.evaluate(4, None, False)) + it.reset() + self.assertEqual(1, it.evaluate(5, None, False)) + self.assertEqual(2, it.evaluate(6, None, False)) + + def test_reset_no_cycling(self): + it = declarations.Iterator([1, 2], cycle=False) + self.assertEqual(1, it.evaluate(0, None, False)) + self.assertEqual(2, it.evaluate(1, None, False)) + self.assertRaises(StopIteration, it.evaluate, 2, None, False) + it.reset() + self.assertEqual(1, it.evaluate(0, None, False)) + self.assertEqual(2, it.evaluate(1, None, False)) + self.assertRaises(StopIteration, it.evaluate, 2, None, False) + def test_getter(self): it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1]) self.assertEqual(2, it.evaluate(0, None, False)) diff --git a/tests/test_utils.py b/tests/test_utils.py index b353c9d..8c73935 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import itertools from factory import utils @@ -230,3 +231,108 @@ class ImportObjectTestCase(unittest.TestCase): def test_invalid_module(self): self.assertRaises(ImportError, utils.import_object, 'this-is-an-invalid-module', '__name__') + + +class ResetableIteratorTestCase(unittest.TestCase): + def test_no_reset(self): + i = utils.ResetableIterator([1, 2, 3]) + self.assertEqual([1, 2, 3], list(i)) + + def test_no_reset_new_iterator(self): + i = utils.ResetableIterator([1, 2, 3]) + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + + iterator2 = iter(i) + self.assertEqual(3, next(iterator2)) + + def test_infinite(self): + i = utils.ResetableIterator(itertools.cycle([1, 2, 3])) + iterator = iter(i) + values = [next(iterator) for _i in range(10)] + self.assertEqual([1, 2, 3, 1, 2, 3, 1, 2, 3, 1], values) + + def test_reset_simple(self): + i = utils.ResetableIterator([1, 2, 3]) + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + + def test_reset_at_begin(self): + i = utils.ResetableIterator([1, 2, 3]) + iterator = iter(i) + i.reset() + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + + def test_reset_at_end(self): + i = utils.ResetableIterator([1, 2, 3]) + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + + def test_reset_after_end(self): + i = utils.ResetableIterator([1, 2, 3]) + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + self.assertRaises(StopIteration, next, iterator) + + i.reset() + # Previous iter() has stopped + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + + def test_reset_twice(self): + i = utils.ResetableIterator([1, 2, 3, 4, 5]) + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + self.assertEqual(4, next(iterator)) + + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + self.assertEqual(4, next(iterator)) + + def test_reset_shorter(self): + i = utils.ResetableIterator([1, 2, 3, 4, 5]) + iterator = iter(i) + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + self.assertEqual(4, next(iterator)) + + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + + i.reset() + self.assertEqual(1, next(iterator)) + self.assertEqual(2, next(iterator)) + self.assertEqual(3, next(iterator)) + self.assertEqual(4, next(iterator)) + -- cgit v1.2.3 From 4f786ac280d7454205ee7922385420a0623974b2 Mon Sep 17 00:00:00 2001 From: rcommande Date: Wed, 19 Jun 2013 00:03:02 +0200 Subject: Updated documentation --- README | 3 ++- docs/conf.py | 4 ++++ docs/orms.rst | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/README b/README index 8586e1d..dee01b4 100644 --- a/README +++ b/README @@ -12,7 +12,7 @@ Its features include: - Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects) - Powerful helpers for common cases (sequences, sub-factories, reverse dependencies, circular factories, ...) - Multiple factories per class support, including inheritance -- Support for various ORMs (currently Django, Mogo) +- Support for various ORMs (currently Django, Mogo, SQLAlchemy) Links @@ -219,6 +219,7 @@ factory_boy has specific support for a few ORMs, through specific :class:`~facto * Django, with :class:`~factory.django.DjangoModelFactory` * Mogo, with :class:`~factory.mogo.MogoFactory` +* SQLAlchemy, with :class:`~factory.alchemy.SQLAlchemyModelFactory` Contributing ------------ diff --git a/docs/conf.py b/docs/conf.py index ee6a739..4f76d45 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -246,4 +246,8 @@ intersphinx_mapping = { 'http://docs.djangoproject.com/en/dev/', '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', + ), } diff --git a/docs/orms.rst b/docs/orms.rst index 9cf7f8e..05166de 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -92,3 +92,68 @@ factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` clas * :func:`~factory.Factory.build()` calls a model's ``new()`` method * :func:`~factory.Factory.create()` builds an instance through ``new()`` then saves it. + +SQLAlchemy +---------- + +.. currentmodule:: factory.alchemy + + +Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class. + +To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_SESSION" class attribute. + +.. _SQLAlchemy: http://www.sqlalchemy.org/ + +.. class:: SQLAlchemyModelFactory(factory.Factory) + + Dedicated class for `SQLAlchemy`_ models. + + 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 + + Fields whose SQLAlchemy session object are passed will be used to communicate with the database + +A (very) simple exemple: + +.. code-block:: python + + from sqlalchemy import Column, Integer, Unicode, create_engine + 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) + Base = declarative_base() + + + class User(Base): + """ A SQLAlchemy simple model class who represents a user """ + __tablename__ = 'UserTable' + + id = Column(Integer(), primary_key=True) + name = Column(Unicode(20)) + + Base.metadata.create_all(engine) + + + class UserFactory(SQLAlchemyModelFactory): + FACTORY_FOR = User + FACTORY_SESSION = session # the SQLAlchemy session object + + id = factory.Sequence(lambda n: n) + name = factory.Sequence(lambda n: u'User %d' % n) + +.. code-block:: pycon + + >>> session.query(User).all() + [] + >>> UserFactory() + + >>> session.query(User).all() + [] -- cgit v1.2.3 From cdc3ae9a91cbe7db1728bb0e83b1c3beee940310 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Thu, 20 Jun 2013 01:43:56 +0200 Subject: Update ChangeLog. --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 98b177d..4437528 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,8 @@ ChangeLog to ease resetting the sequence counter for a given factory. - Add debug messages to ``factory`` logger. - Add a :meth:`~factory.Iterator.reset` method to :class:`~factory.Iterator` (:issue:`63`) + - Add support for the SQLAlchemy ORM through :class:`~factory.alchemy.SQLAlchemyModelFactory` + (:issue:`64`, thanks to `Romain Commandé `_) *Bugfix* -- cgit v1.2.3 From 168ef54e5acfac59f7c625e75a0c7c6d2484cdf0 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Thu, 27 Jun 2013 01:19:24 +0200 Subject: Add factory.django.ImageField (Closes #52). --- docs/orms.rst | 68 ++++++++++++++++++++++++++ factory/compat.py | 5 ++ factory/django.py | 43 +++++++++++++++-- tests/djapp/models.py | 5 ++ tests/test_django.py | 114 ++++++++++++++++++++++++++++++++++++++++++++ tests/testdata/__init__.py | 1 + tests/testdata/example.jpeg | Bin 0 -> 301 bytes 7 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 tests/testdata/example.jpeg (limited to 'docs') diff --git a/docs/orms.rst b/docs/orms.rst index 05166de..611a9ae 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -72,6 +72,74 @@ All factories for a Django :class:`~django.db.models.Model` should use the [, ] +.. class:: FileField + + Custom declarations for :class:`django.db.models.FileField` + + .. method:: __init__(self, from_path='', from_file='', data=b'', filename='example.dat') + + :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 + :param bytes data: Use the provided bytes as file contents + :param str filename: The filename for the FileField + +.. note:: If the value ``None`` was passed for the :class:`FileField` field, this will + disable field generation: + +.. code-block:: python + + class MyFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.MyModel + + the_file = factory.django.FileField(filename='the_file.dat') + +.. code-block:: pycon + + >>> MyFactory(the_file__data=b'uhuh').the_file.read() + b'uhuh' + >>> MyFactory(the_file=None).the_file + None + + +.. class:: ImageField + + Custom declarations for :class:`django.db.models.ImageField` + + .. method:: __init__(self, from_path='', from_file='', filename='example.jpg', width=100, height=100, color='green', format='JPEG') + + :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 + :param str filename: The filename for the ImageField + :param int width: The width of the generated image (default: ``100``) + :param int height: The height of the generated image (default: ``100``) + :param str color: The color of the generated image (default: ``'green'``) + :param str format: The image format (as supported by PIL) (default: ``'JPEG'``) + +.. note:: If the value ``None`` was passed for the :class:`FileField` field, this will + disable field generation: + +.. note:: Just as Django's :class:`django.db.models.ImageField` requires the + Python Imaging Library, this :class:`ImageField` requires it too. + +.. code-block:: python + + class MyFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.MyModel + + the_image = factory.django.ImageField(color='blue') + +.. code-block:: pycon + + >>> MyFactory(the_image__width=42).the_image.width + 42 + >>> MyFactory(the_image=None).the_image + None + + Mogo ---- diff --git a/factory/compat.py b/factory/compat.py index 7c9b845..f77458f 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -31,10 +31,15 @@ PY2 = (sys.version_info[0] == 2) if PY2: # pragma: no cover def is_string(obj): return isinstance(obj, (str, unicode)) + + from StringIO import StringIO as BytesIO + else: # pragma: no cover def is_string(obj): return isinstance(obj, str) + from io import BytesIO + try: # pragma: no cover # Python >= 3.2 UTC = datetime.timezone.utc diff --git a/factory/django.py b/factory/django.py index 7182be6..04bdbbf 100644 --- a/factory/django.py +++ b/factory/django.py @@ -34,8 +34,10 @@ except ImportError as e: # pragma: no cover django_files = None import_failure = e + from . import base from . import declarations +from .compat import BytesIO class DjangoModelFactory(base.Factory): @@ -111,14 +113,22 @@ class DjangoModelFactory(base.Factory): class FileField(declarations.PostGenerationDeclaration): """Helper to fill in django.db.models.FileField from a Factory.""" - def __init__(self, *args, **kwargs): + DEFAULT_FILENAME = 'example.dat' + + def __init__(self, **defaults): if django_files is None: # pragma: no cover raise import_failure - super(FileField, self).__init__(*args, **kwargs) + self.defaults = defaults + super(FileField, self).__init__() + + def _make_data(self, params): + """Create data for the field.""" + return params.get('data', b'') def _make_content(self, extraction_context): path = '' - params = extraction_context.extra + params = dict(self.defaults) + params.update(extraction_context.extra) if params.get('from_path') and params.get('from_file'): raise ValueError( @@ -142,13 +152,13 @@ class FileField(declarations.PostGenerationDeclaration): path = content.name else: - data = params.get('data', b'') + data = self._make_data(params) content = django_files.base.ContentFile(data) if path: default_filename = os.path.basename(path) else: - default_filename = 'example.dat' + default_filename = self.DEFAULT_FILENAME filename = params.get('filename', default_filename) return filename, content @@ -166,3 +176,26 @@ class FileField(declarations.PostGenerationDeclaration): finally: content.file.close() return field_file + + +class ImageField(FileField): + DEFAULT_FILENAME = 'example.jpg' + + def _make_data(self, params): + # ImageField (both django's and factory_boy's) require PIL. + # Try to import it along one of its known installation paths. + try: + from PIL import Image + except ImportError: + import Image + + width = params.get('width', 100) + height = params.get('height', width) + color = params.get('blue') + image_format = params.get('format', 'JPEG') + + thumb = Image.new('RGB', (width, height), color) + thumb_io = BytesIO() + thumb.save(thumb_io, format=image_format) + return thumb_io.getvalue() + diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 52acebe..7bb5ace 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -42,3 +42,8 @@ WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO) class WithFile(models.Model): afile = models.FileField(upload_to=WITHFILE_UPLOAD_TO) + + +class WithImage(models.Model): + animage = models.ImageField(upload_to=WITHFILE_UPLOAD_TO) + diff --git a/tests/test_django.py b/tests/test_django.py index a74a7ae..4ae2a58 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -31,6 +31,16 @@ try: 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 from . import testdata @@ -99,6 +109,12 @@ class WithFileFactory(factory.django.DjangoModelFactory): afile = factory.django.FileField() +class WithImageFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.WithImage + + animage = factory.django.ImageField() + + @unittest.skipIf(django is None, "Django not installed.") class DjangoPkSequenceTestCase(django_test.TestCase): def setUp(self): @@ -261,5 +277,103 @@ class DjangoFileFieldTestCase(unittest.TestCase): self.assertFalse(o.afile) +@unittest.skipIf(django is None, "Django not installed.") +@unittest.skipIf(Image is None, "PIL not installed.") +class DjangoImageFieldTestCase(unittest.TestCase): + + def tearDown(self): + super(DjangoImageFieldTestCase, self).tearDown() + for path in os.listdir(models.WITHFILE_UPLOAD_DIR): + # Remove temporary files written during tests. + os.unlink(os.path.join(models.WITHFILE_UPLOAD_DIR, path)) + + def test_default_build(self): + o = WithImageFactory.build() + self.assertIsNone(o.pk) + self.assertEqual(100, o.animage.width) + self.assertEqual(100, o.animage.height) + self.assertEqual('django/example.jpg', o.animage.name) + + def test_default_create(self): + o = WithImageFactory.create() + self.assertIsNotNone(o.pk) + self.assertEqual(100, o.animage.width) + self.assertEqual(100, o.animage.height) + self.assertEqual('django/example.jpg', o.animage.name) + + def test_with_content(self): + o = WithImageFactory.build(animage__width=13, animage__color='blue') + self.assertIsNone(o.pk) + self.assertEqual(13, o.animage.width) + self.assertEqual(13, o.animage.height) + self.assertEqual('django/example.jpg', o.animage.name) + + def test_with_file(self): + with open(testdata.TESTIMAGE_PATH, 'rb') as f: + o = WithImageFactory.build(animage__from_file=f) + self.assertIsNone(o.pk) + # 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) + + def test_with_path(self): + o = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH) + self.assertIsNone(o.pk) + # 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) + + def test_with_file_empty_path(self): + with open(testdata.TESTIMAGE_PATH, 'rb') as f: + o = WithImageFactory.build( + animage__from_file=f, + animage__from_path='' + ) + self.assertIsNone(o.pk) + # 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) + + def test_with_path_empty_file(self): + o = WithImageFactory.build( + animage__from_path=testdata.TESTIMAGE_PATH, + animage__from_file=None, + ) + self.assertIsNone(o.pk) + # 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) + + def test_error_both_file_and_path(self): + self.assertRaises(ValueError, WithImageFactory.build, + animage__from_file='fakefile', + animage__from_path=testdata.TESTIMAGE_PATH, + ) + + def test_override_filename_with_path(self): + o = WithImageFactory.build( + animage__from_path=testdata.TESTIMAGE_PATH, + animage__filename='example.foo', + ) + self.assertIsNone(o.pk) + # 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) + + o2 = WithImageFactory.build(animage=o1.animage) + 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) + + def test_no_file(self): + o = WithImageFactory.build(animage=None) + self.assertIsNone(o.pk) + self.assertFalse(o.animage) + + if __name__ == '__main__': # pragma: no cover unittest.main() diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py index 3d1d441..9956610 100644 --- a/tests/testdata/__init__.py +++ b/tests/testdata/__init__.py @@ -24,3 +24,4 @@ import os.path TESTDATA_ROOT = os.path.abspath(os.path.dirname(__file__)) TESTFILE_PATH = os.path.join(TESTDATA_ROOT, 'example.data') +TESTIMAGE_PATH = os.path.join(TESTDATA_ROOT, 'example.jpeg') diff --git a/tests/testdata/example.jpeg b/tests/testdata/example.jpeg new file mode 100644 index 0000000..28beea9 Binary files /dev/null and b/tests/testdata/example.jpeg differ -- cgit v1.2.3 From 1a24cf43576a18eaf8b7b7a836ca4ebb7d445148 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Thu, 27 Jun 2013 01:24:41 +0200 Subject: Update ChangeLog --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 4437528..7ae082a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,6 +19,8 @@ ChangeLog - Add a :meth:`~factory.Iterator.reset` method to :class:`~factory.Iterator` (:issue:`63`) - Add support for the SQLAlchemy ORM through :class:`~factory.alchemy.SQLAlchemyModelFactory` (:issue:`64`, thanks to `Romain Commandé `_) + - Add :class:`factory.django.FileField` and :class:`factory.django.ImageField` hooks for + related Django model fields (:issue:`52`) *Bugfix* -- cgit v1.2.3 From cf686b2b4f2002d27eb139dc6f0f684721c873b1 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Thu, 27 Jun 2013 01:34:47 +0200 Subject: Release v2.1.0 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 7ae082a..ff99795 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,8 @@ ChangeLog ========= -2.1.0 (current) ---------------- +2.1.0 (2013-06-26) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index f90f40a..923d097 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.1.0-dev' +__version__ = '2.1.0' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 79566ad0c485d6fd7b881ba783b894f2640f98c9 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Thu, 27 Jun 2013 01:43:22 +0200 Subject: docs: Add reference to version sections. --- docs/changelog.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index ff99795..d22d63e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,8 @@ ChangeLog ========= +.. _v2.1.0: + 2.1.0 (2013-06-26) ------------------ @@ -32,6 +34,9 @@ ChangeLog - Rename :class:`~factory.RelatedFactory`'s ``name`` argument to ``factory_related_name`` (See :issue:`58`) + +.. _v2.0.2: + 2.0.2 (2013-04-16) ------------------ @@ -41,6 +46,8 @@ ChangeLog empty, use ``Model.objects.create()`` instead of ``Model.objects.get_or_create``. +.. _v2.0.1: + 2.0.1 (2013-04-16) ------------------ @@ -50,6 +57,8 @@ ChangeLog :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set. +.. _v2.0.0: + 2.0.0 (2013-04-15) ------------------ @@ -77,6 +86,8 @@ ChangeLog - Remove :meth:`~factory.Factory.set_building_function` / :meth:`~factory.Factory.set_creation_function` +.. _v1.3.0: + 1.3.0 (2013-03-11) ------------------ @@ -155,6 +166,8 @@ In order to upgrade client code, apply the following rules: +.. _v1.2.0: + 1.2.0 (2012-09-08) ------------------ @@ -162,6 +175,9 @@ In order to upgrade client code, apply the following rules: - Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories + +.. _v1.1.5: + 1.1.5 (2012-07-09) ------------------ @@ -169,6 +185,9 @@ In order to upgrade client code, apply the following rules: - Fix :class:`~factory.PostGenerationDeclaration` and derived classes. + +.. _v1.1.4: + 1.1.4 (2012-06-19) ------------------ @@ -180,6 +199,9 @@ In order to upgrade client code, apply the following rules: - Introduce :class:`~factory.PostGeneration` and :class:`~factory.RelatedFactory` + +.. _v1.1.3: + 1.1.3 (2012-03-09) ------------------ @@ -187,6 +209,9 @@ In order to upgrade client code, apply the following rules: - Fix packaging rules + +.. _v1.1.2: + 1.1.2 (2012-02-25) ------------------ @@ -196,6 +221,9 @@ In order to upgrade client code, apply the following rules: - Provide :func:`~factory.Factory.generate` and :func:`~factory.Factory.simple_generate`, that allow specifying the instantiation strategy directly. Also provides :func:`~factory.Factory.generate_batch` and :func:`~factory.Factory.simple_generate_batch`. + +.. _v1.1.1: + 1.1.1 (2012-02-24) ------------------ @@ -203,6 +231,9 @@ In order to upgrade client code, apply the following rules: - Add :func:`~factory.Factory.build_batch`, :func:`~factory.Factory.create_batch` and :func:`~factory.Factory.stub_batch`, to instantiate factories in batch + +.. _v1.1.0: + 1.1.0 (2012-02-24) ------------------ @@ -221,6 +252,9 @@ In order to upgrade client code, apply the following rules: - Auto-discovery of :attr:`~factory.Factory.FACTORY_FOR` based on class name is now deprecated + +.. _v1.0.4: + 1.0.4 (2011-12-21) ------------------ @@ -241,6 +275,9 @@ In order to upgrade client code, apply the following rules: - Share sequence counter between parent and subclasses - Fix :class:`~factory.SubFactory` / :class:`~factory.Sequence` interferences + +.. _v1.0.2: + 1.0.2 (2011-05-16) ------------------ @@ -248,6 +285,9 @@ In order to upgrade client code, apply the following rules: - Introduce :class:`~factory.SubFactory` + +.. _v1.0.1: + 1.0.1 (2011-05-13) ------------------ @@ -260,6 +300,9 @@ In order to upgrade client code, apply the following rules: - Fix concurrency between :class:`~factory.LazyAttribute` and :class:`~factory.Sequence` + +.. _v1.0.0: + 1.0.0 (2010-08-22) ------------------ -- cgit v1.2.3 From 740eeb0c0330e0292b215a3128788f553dd80f17 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 2 Jul 2013 11:31:31 +0200 Subject: Release v2.1.1 --- docs/changelog.rst | 10 ++++++++++ factory/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index d22d63e..f9302ad 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ ChangeLog ========= + +.. _v2.1.1: + +2.1.1 (2013-07-02) +------------------ + +*Bugfix:* + + - Properly retrieve the ``color`` keyword argument passed to :class:`~factory.django.ImageField` + .. _v2.1.0: 2.1.0 (2013-06-26) diff --git a/factory/__init__.py b/factory/__init__.py index 923d097..a90030e 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.1.0' +__version__ = '2.1.1' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 1c473f9b3e3ba216c37df7ce7393d8c3742fa054 Mon Sep 17 00:00:00 2001 From: Branko Majic Date: Fri, 2 Aug 2013 22:43:22 +0200 Subject: Fix factory.sequence documentation (Closes #77). The documentation erroneously included an extra 'self' parameter. --- docs/introduction.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/introduction.rst b/docs/introduction.rst index 356e11f..4dc1168 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -99,7 +99,7 @@ This is achieved with the :class:`~factory.Sequence` declaration: >>> UserFactory() -.. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator: +.. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator (note that ``self`` is not added as first parameter): .. code-block:: python @@ -107,7 +107,7 @@ This is achieved with the :class:`~factory.Sequence` declaration: FACTORY_FOR = models.User @factory.sequence - def username(self, n): + def username(n): return 'user%d' % n -- cgit v1.2.3 From 5730d4ac18f8684c37168033ef32d1ee31f5e4a1 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Mon, 5 Aug 2013 19:02:25 +0200 Subject: Clarify intro doc about @lazy_attribute. --- docs/introduction.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/introduction.rst b/docs/introduction.rst index 4dc1168..41c6f7b 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -140,7 +140,19 @@ taking the object being built and returning the value for the field: -.. note:: As for :class:`~factory.Sequence`, a :meth:`~factory.@lazy_attribute` decorator is available. +.. note:: As for :class:`~factory.Sequence`, a :meth:`~factory.@lazy_attribute` decorator is available: + + +.. code-block:: python + + class UserFactory(factory.Factory): + FACTORY_FOR = models.User + + username = factory.Sequence(lambda n: 'user%d' % n) + + @factory.lazy_attribute + def email(self): + return '%s@example.com' % self.username Inheritance -- cgit v1.2.3 From 2796de70d5bed4cfff5749085ce4e6f16eba1b1e Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 13 Aug 2013 12:06:52 +0200 Subject: Make ABSTRACT_FACTORY optional (Closes #74) It will be automatically set to True if neither the Factory subclass nor its parents define a FACTORY_FOR argument. It can also be set on a Factory subclass to prevent it from being called. --- docs/changelog.rst | 11 +++++++++++ docs/reference.rst | 11 +++++++---- factory/base.py | 57 +++++++++++++++++++++++++++++------------------------- factory/django.py | 2 +- tests/test_base.py | 24 +++++++++++++++++------ 5 files changed, 68 insertions(+), 37 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index f9302ad..ec47b14 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ ChangeLog ========= +.. _v2.1.2: + +2.1.2 (current) +--------------- + +*New:* + + - The :class:`~factory.Factory.ABSTRACT_FACTORY` keyword is now optional, and automatically set + to ``True`` if neither the :class:`~factory.Factory` subclass nor its parent declare the + :class:`~factory.Factory.FACTORY_FOR` attribute (:issue:`74`) + .. _v2.1.1: diff --git a/docs/reference.rst b/docs/reference.rst index e98665f..3d3097d 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -19,15 +19,18 @@ The :class:`Factory` class .. attribute:: FACTORY_FOR - This required attribute describes the class of objects to generate. - It may only be absent if the factory has been marked abstract through - :attr:`ABSTRACT_FACTORY`. + This optional attribute describes the class of objects to generate. + + If unset, it will be inherited from parent :class:`Factory` subclasses. .. attribute:: ABSTRACT_FACTORY This attribute indicates that the :class:`Factory` subclass should not be used to generate objects, but instead provides some extra defaults. + It will be automatically set to ``True`` if neither the :class:`Factory` + subclass nor its parents define the :attr:`~Factory.FACTORY_FOR` attribute. + .. attribute:: FACTORY_ARG_PARAMETERS Some factories require non-keyword arguments to their :meth:`~object.__init__`. @@ -211,7 +214,7 @@ The :class:`Factory` class .. code-block:: python class BaseBackendFactory(factory.Factory): - ABSTRACT_FACTORY = True + ABSTRACT_FACTORY = True # Optional def _create(cls, target_class, *args, **kwargs): obj = target_class(*args, **kwargs) diff --git a/factory/base.py b/factory/base.py index 029185b..0db8249 100644 --- a/factory/base.py +++ b/factory/base.py @@ -40,6 +40,7 @@ FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' CLASS_ATTRIBUTE_DECLARATIONS = '_declarations' CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations' CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' +CLASS_ATTRIBUTE_IS_ABSTRACT = '_abstract_factory' class FactoryError(Exception): @@ -101,10 +102,6 @@ class FactoryMetaClass(type): Returns: class: the class to associate with this factory - - Raises: - AssociatedClassError: If we were unable to associate this factory - to a class. """ if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] @@ -114,10 +111,8 @@ class FactoryMetaClass(type): if inherited is not None: return inherited - raise AssociatedClassError( - "Could not determine the class associated with %s. " - "Use the FACTORY_FOR attribute to specify an associated class." % - class_name) + # Nothing found, return None. + return None @classmethod def _extract_declarations(mcs, bases, attributes): @@ -155,7 +150,7 @@ class FactoryMetaClass(type): return attributes - def __new__(mcs, class_name, bases, attrs, extra_attrs=None): + def __new__(mcs, class_name, bases, attrs): """Record attributes as a pattern for later instance construction. This is called when a new Factory subclass is defined; it will collect @@ -166,9 +161,6 @@ class FactoryMetaClass(type): bases (list of class): the parents of the class being created attrs (str => obj dict): the attributes as defined in the class definition - extra_attrs (str => obj dict): extra attributes that should not be - included in the factory defaults, even if public. This - argument is only provided by extensions of this metaclass. Returns: A new class @@ -178,18 +170,20 @@ class FactoryMetaClass(type): return super(FactoryMetaClass, mcs).__new__( mcs, class_name, bases, attrs) - is_abstract = attrs.pop('ABSTRACT_FACTORY', False) extra_attrs = {} - if not is_abstract: + is_abstract = attrs.pop('ABSTRACT_FACTORY', False) - base = parent_factories[0] + base = parent_factories[0] + inherited_associated_class = getattr(base, + CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + associated_class = mcs._discover_associated_class(class_name, attrs, + inherited_associated_class) - inherited_associated_class = getattr(base, - CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) - associated_class = mcs._discover_associated_class(class_name, attrs, - inherited_associated_class) + if associated_class is None: + is_abstract = True + else: # If inheriting the factory from a parent, keep a link to it. # This allows to use the sequence counters from the parents. if associated_class == inherited_associated_class: @@ -197,21 +191,23 @@ class FactoryMetaClass(type): # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into # account when parsing the declared attributes of the new class. - extra_attrs = {CLASS_ATTRIBUTE_ASSOCIATED_CLASS: associated_class} + extra_attrs[CLASS_ATTRIBUTE_ASSOCIATED_CLASS] = associated_class + + extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract # Extract pre- and post-generation declarations attributes = mcs._extract_declarations(parent_factories, attrs) - - # Add extra args if provided. - if extra_attrs: - attributes.update(extra_attrs) + attributes.update(extra_attrs) return super(FactoryMetaClass, mcs).__new__( mcs, class_name, bases, attributes) def __str__(cls): - return '<%s for %s>' % (cls.__name__, - getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + if cls._abstract_factory: + return '<%s (abstract)>' + else: + return '<%s for %s>' % (cls.__name__, + getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) # Factory base classes @@ -238,6 +234,9 @@ class BaseFactory(object): # Holds the target class, once resolved. _associated_class = None + # Whether this factory is considered "abstract", thus uncallable. + _abstract_factory = False + # List of arguments that should be passed as *args instead of **kwargs FACTORY_ARG_PARAMETERS = () @@ -367,6 +366,12 @@ class BaseFactory(object): create (bool): whether to 'build' or 'create' the object attrs (dict): attributes to use for generating the object """ + if cls._abstract_factory: + raise FactoryError( + "Cannot generate instances of abstract factory %(f)s; " + "Ensure %(f)s.FACTORY_FOR is set and %(f)s.ABSTRACT_FACTORY " + "is either not set or False." % dict(f=cls)) + # Extract declarations used for post-generation postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) diff --git a/factory/django.py b/factory/django.py index 1672fcc..e3e8829 100644 --- a/factory/django.py +++ b/factory/django.py @@ -49,7 +49,7 @@ class DjangoModelFactory(base.Factory): handle those for non-numerical primary keys. """ - ABSTRACT_FACTORY = True + ABSTRACT_FACTORY = True # Optional, but explicit. FACTORY_DJANGO_GET_OR_CREATE = () @classmethod diff --git a/tests/test_base.py b/tests/test_base.py index 8ac2f44..8cea6fc 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -73,6 +73,20 @@ class AbstractFactoryTestCase(unittest.TestCase): # Passed + def test_factory_for_and_abstract_factory_optional(self): + """Ensure that ABSTRACT_FACTORY is optional.""" + class TestObjectFactory(base.Factory): + pass + + # passed + + def test_abstract_factory_cannot_be_called(self): + class TestObjectFactory(base.Factory): + pass + + self.assertRaises(base.FactoryError, TestObjectFactory.build) + self.assertRaises(base.FactoryError, TestObjectFactory.create) + class FactoryTestCase(unittest.TestCase): def test_factory_for(self): @@ -318,12 +332,10 @@ class FactoryCreationTestCase(unittest.TestCase): # Errors def test_no_associated_class(self): - try: - class Test(base.Factory): - pass - self.fail() # pragma: no cover - except base.Factory.AssociatedClassError as e: - self.assertTrue('autodiscovery' not in str(e)) + class Test(base.Factory): + pass + + self.assertTrue(Test._abstract_factory) class PostGenerationParsingTestCase(unittest.TestCase): -- cgit v1.2.3 From 9afe451c1deafda085d4a8c9d1c93ea95376ac95 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 13 Aug 2013 13:20:21 +0200 Subject: doc: Fix 'ProfileFactory' doc. --- docs/recipes.rst | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/recipes.rst b/docs/recipes.rst index 82f0658..529d837 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -78,18 +78,26 @@ factory_boy allows to define attributes of such profiles dynamically when creati FACTORY_FOR = my_models.Profile title = 'Dr' + # We pass in profile=None to prevent UserFactory from creating another profile + # (this disables the RelatedFactory) + user = factory.SubFactory(UserFactory, profile=None) class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = auth_models.User username = factory.Sequence(lambda n: "user_%d" % n) - profile = factory.RelatedFactory(ProfileFactory) + + # We pass in 'user' to link the generated Profile to our just-generated User + # This will call ProfileFactory(user=our_new_user), thus skipping the SubFactory. + profile = factory.RelatedFactory(ProfileFactory, 'user') @classmethod - def _create(cls, target_class, *args, **kwargs): - """Override the default _create() to disable the post-save signal.""" + def _generate(cls, create, attrs): + """Override the default _generate() to disable the post-save signal.""" + + # Note: If the signal was defined with a dispatch_uid, include that in both calls. post_save.disconnect(handler_create_user_profile, auth_models.User) - user = super(UserFactory, cls)._create(target_class, *args, **kwargs) + user = super(UserFactory, cls)._generate(create, attrs) post_save.connect(handler_create_user_profile, auth_models.User) return user @@ -105,6 +113,13 @@ factory_boy allows to define attributes of such profiles dynamically when creati Such behaviour can be extended to other situations where a signal interferes with factory_boy related factories. +.. note:: When any :class:`~factory.RelatedFactory` or :class:`~factory.post_generation` + attribute is defined on the :class:`~factory.django.DjangoModelFactory` subclass, + a second ``save()`` is performed *after* the call to ``_create()``. + + Code working with signals should thus override the :meth:`~factory.Factory._generate` + method. + Simple ManyToMany ----------------- -- cgit v1.2.3 From 50a4ec92ad097d91ad971ae7f8b788d8b538dcfc Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 14 Aug 2013 17:40:38 +0200 Subject: Release v2.1.2 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index ec47b14..76dd360 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,8 +3,8 @@ ChangeLog .. _v2.1.2: -2.1.2 (current) ---------------- +2.1.2 (2013-08-14) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index a90030e..ea45a9a 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.1.1' +__version__ = '2.1.2' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 297a111cc918c6451f1b66e3fe3572a9f3fc6b8f Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 28 Aug 2013 01:30:15 +0200 Subject: Allow FACTORY_FOR = 'app.Model' for Django (Closes #66). --- docs/orms.rst | 4 +++- factory/base.py | 11 ++++++++++- factory/django.py | 28 ++++++++++++++++++++++++---- tests/test_django.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 6 deletions(-) (limited to 'docs') diff --git a/docs/orms.rst b/docs/orms.rst index 611a9ae..8215fe6 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -32,6 +32,8 @@ All factories for a Django :class:`~django.db.models.Model` should use the This class provides the following features: + * The :attr:`~factory.Factory.FACTORY_FOR` 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` @@ -47,7 +49,7 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. code-block:: python class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + FACTORY_FOR = 'myapp.User' # Equivalent to ``FACTORY_FOR = myapp.models.User`` FACTORY_DJANGO_GET_OR_CREATE = ('username',) username = 'john' diff --git a/factory/base.py b/factory/base.py index ac906de..1b9fa0d 100644 --- a/factory/base.py +++ b/factory/base.py @@ -330,6 +330,15 @@ class BaseFactory(object): """Extension point for custom kwargs adjustment.""" return kwargs + @classmethod + def _load_target_class(cls): + """Extension point for loading target classes. + + This can be overridden in framework-specific subclasses to hook into + existing model repositories, for instance. + """ + return getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + @classmethod def _prepare(cls, create, **kwargs): """Prepare an object for this factory. @@ -338,7 +347,7 @@ class BaseFactory(object): create: bool, whether to create or to build the object **kwargs: arguments to pass to the creation function """ - target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + target_class = cls._load_target_class() kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. diff --git a/factory/django.py b/factory/django.py index e3e8829..016586d 100644 --- a/factory/django.py +++ b/factory/django.py @@ -37,7 +37,13 @@ except ImportError as e: # pragma: no cover from . import base from . import declarations -from .compat import BytesIO +from .compat import BytesIO, is_string + + +def require_django(): + """Simple helper to ensure Django is available.""" + if django_files is None: # pragma: no cover + raise import_failure class DjangoModelFactory(base.Factory): @@ -52,6 +58,21 @@ class DjangoModelFactory(base.Factory): ABSTRACT_FACTORY = True # Optional, but explicit. FACTORY_DJANGO_GET_OR_CREATE = () + _associated_model = None + + @classmethod + def _load_target_class(cls): + associated_class = super(DjangoModelFactory, cls)._load_target_class() + + if is_string(associated_class) and '.' in associated_class: + app, model = associated_class.split('.', 1) + if cls._associated_model is None: + from django.db.models import loading as django_loading + cls._associated_model = django_loading.get_model(app, model) + return cls._associated_model + + return associated_class + @classmethod def _get_manager(cls, target_class): try: @@ -63,7 +84,7 @@ class DjangoModelFactory(base.Factory): def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" - model = cls._associated_class # pylint: disable=E1101 + model = cls._load_target_class() # pylint: disable=E1101 manager = cls._get_manager(model) try: @@ -116,8 +137,7 @@ class FileField(declarations.PostGenerationDeclaration): DEFAULT_FILENAME = 'example.dat' def __init__(self, **defaults): - if django_files is None: # pragma: no cover - raise import_failure + require_django() self.defaults = defaults super(FileField, self).__init__() diff --git a/tests/test_django.py b/tests/test_django.py index 9d02131..b27562c 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -157,6 +157,35 @@ class DjangoPkSequenceTestCase(django_test.TestCase): self.assertEqual(11, std2.pk) +@unittest.skipIf(django is None, "Django not installed.") +class DjangoModelLoadingTestCase(django_test.TestCase): + """Tests FACTORY_FOR = 'app.Model' pattern.""" + + def test_loading(self): + class ExampleFactory(factory.DjangoModelFactory): + FACTORY_FOR = 'djapp.StandardModel' + + self.assertEqual(models.StandardModel, ExampleFactory._load_target_class()) + + def test_building(self): + class ExampleFactory(factory.DjangoModelFactory): + FACTORY_FOR = 'djapp.StandardModel' + + e = ExampleFactory.build() + self.assertEqual(models.StandardModel, e.__class__) + + def test_cache(self): + class ExampleFactory(factory.DjangoModelFactory): + FACTORY_FOR = 'djapp.StandardModel' + + self.assertEqual('djapp.StandardModel', ExampleFactory._associated_class) + self.assertIsNone(ExampleFactory._associated_model) + + self.assertEqual(models.StandardModel, ExampleFactory._load_target_class()) + self.assertEqual('djapp.StandardModel', ExampleFactory._associated_class) + self.assertEqual(models.StandardModel, ExampleFactory._associated_model) + + @unittest.skipIf(django is None, "Django not installed.") class DjangoNonIntegerPkTestCase(django_test.TestCase): def setUp(self): -- cgit v1.2.3 From 948d8c4191342210a5e9540c9078b807ff4d6829 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 28 Aug 2013 01:32:46 +0200 Subject: Update ChangeLog --- docs/changelog.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 76dd360..75f1c41 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,21 @@ ChangeLog ========= +.. _v2.1.3: + +2.1.3 (current) +--------------- + +*Bugfix:* + + - Removed duplicated :class:`~factory.alchemy.SQLAlchemyModelFactory` lurking in :mod:`factory` + (:issue:`83`) + +*New:* + + - The :class:`~factory.django.DjangoModelFactory` now supports the ``FACTORY_FOR = 'myapp.MyModel'`` + syntax, making it easier to shove all factories in a single module (:issue:`66`). + .. _v2.1.2: 2.1.2 (2013-08-14) -- cgit v1.2.3 From ca393990b35062c5151bc529622131dc93bbed84 Mon Sep 17 00:00:00 2001 From: alex-netquity Date: Mon, 9 Sep 2013 12:52:31 -0400 Subject: Fix undefined error in profile example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Django’s Profile example references `UserFactory` before its definition. The fix is to use the fully-qualified path to the class in question. --- docs/recipes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/recipes.rst b/docs/recipes.rst index 529d837..c1f3700 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -80,7 +80,7 @@ factory_boy allows to define attributes of such profiles dynamically when creati title = 'Dr' # We pass in profile=None to prevent UserFactory from creating another profile # (this disables the RelatedFactory) - user = factory.SubFactory(UserFactory, profile=None) + user = factory.SubFactory('app.factories.UserFactory', profile=None) class UserFactory(factory.django.DjangoModelFactory): FACTORY_FOR = auth_models.User -- cgit v1.2.3 From 676e02fd916a4e41a6080d29ef8f89925c499f35 Mon Sep 17 00:00:00 2001 From: Jonathan Tushman Date: Sat, 7 Sep 2013 16:30:25 -0400 Subject: Adding factory support for mongoengine (Closes #89). --- README | 1 + dev_requirements.txt | 1 + docs/changelog.rst | 1 + docs/orms.rst | 23 ++++++++++++++++++ factory/__init__.py | 1 + factory/mongoengine.py | 45 ++++++++++++++++++++++++++++++++++ tests/__init__.py | 1 + tests/test_mongoengine.py | 62 +++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 135 insertions(+) create mode 100644 factory/mongoengine.py create mode 100644 tests/test_mongoengine.py (limited to 'docs') diff --git a/README b/README index dee01b4..bc5b55f 100644 --- a/README +++ b/README @@ -219,6 +219,7 @@ factory_boy has specific support for a few ORMs, through specific :class:`~facto * Django, with :class:`~factory.django.DjangoModelFactory` * Mogo, with :class:`~factory.mogo.MogoFactory` +* MongoEngine, with :class:`~factory.mongoengine.MongoEngineFactory` * SQLAlchemy, with :class:`~factory.alchemy.SQLAlchemyModelFactory` Contributing diff --git a/dev_requirements.txt b/dev_requirements.txt index f799ef8..e828644 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -2,3 +2,4 @@ coverage Django Pillow sqlalchemy +mongoengine \ No newline at end of file diff --git a/docs/changelog.rst b/docs/changelog.rst index 75f1c41..0367246 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ ChangeLog - The :class:`~factory.django.DjangoModelFactory` now supports the ``FACTORY_FOR = 'myapp.MyModel'`` syntax, making it easier to shove all factories in a single module (:issue:`66`). + - Adding factory support for mongoengine with :class:`~factory.mongoengine.MongoEngineFactory`. .. _v2.1.2: diff --git a/docs/orms.rst b/docs/orms.rst index 8215fe6..74c5c62 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -163,6 +163,29 @@ factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` clas * :func:`~factory.Factory.create()` builds an instance through ``new()`` then saves it. + +MongoEngine +---- + +.. currentmodule:: factory.mongoengine + +factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngineFactory` class. + +`mongoengine`_ is a wrapper around the ``pymongo`` library for MongoDB. + +.. _mongoengine:: http://mongoengine.org/ + +.. class:: MongoEngineFactory(factory.Factory) + + Dedicated class for `MongoEngine`_ models. + + This class provides the following features: + + * :func:`~factory.Factory.build()` calls a model's ``__init__`` method + * :func:`~factory.Factory.create()` builds an instance through ``__init__`` then + saves it. + + SQLAlchemy ---------- diff --git a/factory/__init__.py b/factory/__init__.py index ea45a9a..a27eb40 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -38,6 +38,7 @@ from .base import ( use_strategy, ) +# Backward compatibility; this should be removed soon. from .mogo import MogoFactory from .django import DjangoModelFactory diff --git a/factory/mongoengine.py b/factory/mongoengine.py new file mode 100644 index 0000000..8cd3a67 --- /dev/null +++ b/factory/mongoengine.py @@ -0,0 +1,45 @@ +# -*- 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. + + +from __future__ import unicode_literals + + +"""factory_boy extensions for use with the mongoengine library (pymongo wrapper).""" + + +from . import base + + +class MongoEngineFactory(base.Factory): + """Factory for mongoengine objects.""" + ABSTRACT_FACTORY = True + + @classmethod + def _build(cls, target_class, *args, **kwargs): + return target_class(*args, **kwargs) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + instance = target_class(*args, **kwargs) + instance.save() + return instance diff --git a/tests/__init__.py b/tests/__init__.py index 9960382..d823a87 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,3 +9,4 @@ from .test_fuzzy import * from .test_using import * from .test_utils import * from .test_alchemy import * +from .test_mongoengine import * diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py new file mode 100644 index 0000000..e078aed --- /dev/null +++ b/tests/test_mongoengine.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2013 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 +# 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 factory_boy/SQLAlchemy interactions.""" + +import factory +from .compat import unittest + + +try: + import mongoengine +except ImportError: + mongoengine = None + +if mongoengine: + from factory.mongoengine import MongoEngineFactory + + class Person(mongoengine.Document): + name = mongoengine.StringField() + + class PersonFactory(MongoEngineFactory): + FACTORY_FOR = Person + + name = factory.Sequence(lambda n: 'name%d' % n) + + + +@unittest.skipIf(mongoengine is None, "mongoengine not installed.") +class MongoEngineTestCase(unittest.TestCase): + + def setUp(self): + mongoengine.connect('factory_boy_test') + + def test_build(self): + std = PersonFactory.build() + self.assertEqual('name0', std.name) + self.assertIsNone(std.id) + + def test_creation(self): + std1 = PersonFactory.create() + self.assertEqual('name1', std1.name) + self.assertIsNotNone(std1.id) + + -- cgit v1.2.3 From a8742c973db224968b74bb054027130b2ab458e0 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 17 Sep 2013 00:28:48 +0200 Subject: Add 'factory.debug' context manager. --- README | 8 +++++- docs/changelog.rst | 1 + docs/orms.rst | 4 +-- docs/reference.rst | 32 ++++++++++++++++++++++ factory/__init__.py | 2 ++ factory/helpers.py | 18 ++++++++++++ tests/__init__.py | 1 + tests/compat.py | 5 ++++ tests/test_helpers.py | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/test_helpers.py (limited to 'docs') diff --git a/README b/README index bc5b55f..0371b28 100644 --- a/README +++ b/README @@ -188,10 +188,16 @@ 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: +Detailed logging is available through the ``factory`` logger. + +A helper, :meth:`factory.debug()`, is available to ease debugging: .. code-block:: python + with factory.debug(): + obj = TestModel2Factory() + + import logging logger = logging.getLogger('factory') logger.addHandler(logging.StreamHandler()) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0367246..326b245 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ ChangeLog - The :class:`~factory.django.DjangoModelFactory` now supports the ``FACTORY_FOR = 'myapp.MyModel'`` syntax, making it easier to shove all factories in a single module (:issue:`66`). + - Add :meth:`factory.debug()` helper for easier backtrace analysis - Adding factory support for mongoengine with :class:`~factory.mongoengine.MongoEngineFactory`. .. _v2.1.2: diff --git a/docs/orms.rst b/docs/orms.rst index 74c5c62..a463bfb 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -165,7 +165,7 @@ factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` clas MongoEngine ----- +----------- .. currentmodule:: factory.mongoengine @@ -173,7 +173,7 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin `mongoengine`_ is a wrapper around the ``pymongo`` library for MongoDB. -.. _mongoengine:: http://mongoengine.org/ +.. _mongoengine: http://mongoengine.org/ .. class:: MongoEngineFactory(factory.Factory) diff --git a/docs/reference.rst b/docs/reference.rst index 3d3097d..53584a0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -363,6 +363,38 @@ factory_boy supports two main strategies for generating instances, plus stubs. with a default strategy set to :data:`STUB_STRATEGY`. +.. function:: debug(logger='factory', stream=None) + + :param str logger: The name of the logger to enable debug for + :param file stream: The stream to send debug output to, defaults to :obj:`sys.stderr` + + Context manager to help debugging factory_boy behavior. + It will temporarily put the target logger (e.g ``'factory'``) in debug mode, + sending all output to :obj`~sys.stderr`; + upon leaving the context, the logging levels are reset. + + A typical use case is to understand what happens during a single factory call: + + .. code-block:: python + + with factory.debug(): + obj = TestModel2Factory() + + This will yield messages similar to those (artificial indentation): + + .. code-block:: ini + + BaseFactory: Preparing tests.test_using.TestModel2Factory(extra={}) + LazyStub: Computing values for tests.test_using.TestModel2Factory(two=>) + SubFactory: Instantiating tests.test_using.TestModelFactory(__containers=(,), one=4), create=True + BaseFactory: Preparing tests.test_using.TestModelFactory(extra={'__containers': (,), 'one': 4}) + LazyStub: Computing values for tests.test_using.TestModelFactory(one=4) + LazyStub: Computed values, got tests.test_using.TestModelFactory(one=4) + BaseFactory: Generating tests.test_using.TestModelFactory(one=4) + LazyStub: Computed values, got tests.test_using.TestModel2Factory(two=) + BaseFactory: Generating tests.test_using.TestModel2Factory(two=) + + .. _declarations: Declarations diff --git a/factory/__init__.py b/factory/__init__.py index a27eb40..a71fea5 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -58,6 +58,8 @@ from .declarations import ( ) from .helpers import ( + debug, + build, create, stub, diff --git a/factory/helpers.py b/factory/helpers.py index 8f0d161..37b41bf 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -23,11 +23,29 @@ """Simple wrappers around Factory class definition.""" +import contextlib +import logging from . import base from . import declarations +@contextlib.contextmanager +def debug(logger='factory', stream=None): + logger_obj = logging.getLogger(logger) + old_level = logger_obj.level + + handler = logging.StreamHandler(stream) + handler.setLevel(logging.DEBUG) + logger_obj.addHandler(handler) + logger_obj.setLevel(logging.DEBUG) + + yield + + logger_obj.setLevel(old_level) + logger_obj.removeHandler(handler) + + def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ diff --git a/tests/__init__.py b/tests/__init__.py index d823a87..5b6fc55 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,6 +6,7 @@ from .test_containers import * from .test_declarations import * from .test_django import * from .test_fuzzy import * +from .test_helpers import * from .test_using import * from .test_utils import * from .test_alchemy import * diff --git a/tests/compat.py b/tests/compat.py index f11076c..ff96f13 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -30,6 +30,11 @@ if sys.version_info[0:2] < (2, 7): # pragma: no cover else: # pragma: no cover import unittest +if sys.version_info[0] == 2: # pragma: no cover + import StringIO as io +else: # pragma: no cover + import io + if sys.version_info[0:2] < (3, 3): # pragma: no cover import mock else: # pragma: no cover diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..f5a66e5 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,76 @@ +# -*- 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 logging + +from factory import helpers + +from .compat import io, unittest + + +class DebugTest(unittest.TestCase): + """Tests for the 'factory.debug()' helper.""" + + def test_default_logger(self): + stream1 = io.StringIO() + stream2 = io.StringIO() + + l = logging.getLogger('factory.test') + h = logging.StreamHandler(stream1) + h.setLevel(logging.INFO) + l.addHandler(h) + + # Non-debug: no text gets out + l.debug("Test") + self.assertEqual('', stream1.getvalue()) + + with helpers.debug(stream=stream2): + # Debug: text goes to new stream only + l.debug("Test2") + + self.assertEqual('', stream1.getvalue()) + self.assertEqual("Test2\n", stream2.getvalue()) + + def test_alternate_logger(self): + stream1 = io.StringIO() + stream2 = io.StringIO() + + l1 = logging.getLogger('factory.test') + l2 = logging.getLogger('factory.foo') + h = logging.StreamHandler(stream1) + h.setLevel(logging.DEBUG) + l2.addHandler(h) + + # Non-debug: no text gets out + l1.debug("Test") + self.assertEqual('', stream1.getvalue()) + l2.debug("Test") + self.assertEqual('', stream1.getvalue()) + + with helpers.debug('factory.test', stream=stream2): + # Debug: text goes to new stream only + l1.debug("Test2") + l2.debug("Test3") + + self.assertEqual("", stream1.getvalue()) + self.assertEqual("Test2\n", stream2.getvalue()) + -- cgit v1.2.3 From 7fe9dcaa8494e73d57613d1288b4f86c4cba5bf0 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 17 Sep 2013 01:12:48 +0200 Subject: Properly handle Sequence & inheritance (Closes #93). There was also a nasty bug: with class FactoryB(FactoryA), FactoryB's sequence counter started at the value of FactoryA's counter when FactoryB was first called. --- docs/changelog.rst | 7 +++-- factory/base.py | 74 ++++++++++++++++++++++++++++++++++++++++++--------- tests/test_alchemy.py | 2 +- tests/test_using.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 16 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 326b245..25d6a06 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,15 +1,18 @@ ChangeLog ========= -.. _v2.1.3: +.. _v2.2.0: -2.1.3 (current) +2.2.0 (current) --------------- *Bugfix:* - Removed duplicated :class:`~factory.alchemy.SQLAlchemyModelFactory` lurking in :mod:`factory` (:issue:`83`) + - Properly handle sequences within object inheritance chains. + If FactoryA inherits from FactoryB, and their associated classes share the same link, + sequence counters will be shared (:issue:`93`) *New:* diff --git a/factory/base.py b/factory/base.py index 1b9fa0d..462a60c 100644 --- a/factory/base.py +++ b/factory/base.py @@ -186,7 +186,8 @@ class FactoryMetaClass(type): else: # If inheriting the factory from a parent, keep a link to it. # This allows to use the sequence counters from the parents. - if associated_class == inherited_associated_class: + if (inherited_associated_class is not None + and issubclass(associated_class, inherited_associated_class)): attrs['_base_factory'] = base # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into @@ -212,6 +213,32 @@ class FactoryMetaClass(type): # Factory base classes + +class _Counter(object): + """Simple, naive counter. + + Attributes: + for_class (obj): the class this counter related to + seq (int): the next value + """ + + def __init__(self, seq, for_class): + self.seq = seq + self.for_class = for_class + + def next(self): + value = self.seq + self.seq += 1 + return value + + def reset(self, next_value=0): + self.seq = next_value + + def __repr__(self): + return '<_Counter for %s.%s, next=%d>' % ( + self.for_class.__module__, self.for_class.__name__, self.seq) + + class BaseFactory(object): """Factory base support for sequences, attributes and stubs.""" @@ -224,10 +251,10 @@ class BaseFactory(object): raise FactoryError('You cannot instantiate BaseFactory') # ID to use for the next 'declarations.Sequence' attribute. - _next_sequence = None + _counter = None # Base factory, if this class was inherited from another factory. This is - # used for sharing the _next_sequence counter among factories for the same + # used for sharing the sequence _counter among factories for the same # class. _base_factory = None @@ -245,7 +272,14 @@ class BaseFactory(object): @classmethod def reset_sequence(cls, value=None, force=False): - """Reset the sequence counter.""" + """Reset the sequence counter. + + Args: + value (int or None): the new 'next' sequence value; if None, + recompute the next value from _setup_next_sequence(). + force (bool): whether to force-reset parent sequence counters + in a factory inheritance chain. + """ if cls._base_factory: if force: cls._base_factory.reset_sequence(value=value) @@ -253,10 +287,13 @@ class BaseFactory(object): raise ValueError( "Cannot reset the sequence of a factory subclass. " "Please call reset_sequence() on the root factory, " - "or call reset_sequence(forward=True)." + "or call reset_sequence(force=True)." ) else: - cls._next_sequence = value + cls._setup_counter() + if value is None: + value = cls._setup_next_sequence() + cls._counter.reset(value) @classmethod def _setup_next_sequence(cls): @@ -267,6 +304,19 @@ class BaseFactory(object): """ return 0 + @classmethod + def _setup_counter(cls): + """Ensures cls._counter is set for this class. + + Due to the way inheritance works in Python, we need to ensure that the + ``_counter`` attribute has been initialized for *this* Factory subclass, + not one of its parents. + """ + if cls._counter is None or cls._counter.for_class != cls: + first_seq = cls._setup_next_sequence() + cls._counter = _Counter(for_class=cls, seq=first_seq) + logger.debug("%r: Setting up next sequence (%d)", cls, first_seq) + @classmethod def _generate_next_sequence(cls): """Retrieve a new sequence ID. @@ -279,16 +329,14 @@ class BaseFactory(object): # Rely upon our parents if cls._base_factory: + logger.debug("%r: reusing sequence from %r", cls, cls._base_factory) return cls._base_factory._generate_next_sequence() - # Make sure _next_sequence is initialized - if cls._next_sequence is None: - cls._next_sequence = cls._setup_next_sequence() + # Make sure _counter is initialized + cls._setup_counter() # Pick current value, then increase class counter for the next call. - next_sequence = cls._next_sequence - cls._next_sequence += 1 - return next_sequence + return cls._counter.next() @classmethod def attributes(cls, create=False, extra=None): @@ -577,7 +625,7 @@ Factory = FactoryMetaClass('Factory', (BaseFactory,), { This class has the ability to support multiple ORMs by using custom creation functions. """, - }) +}) # Backwards compatibility diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index cfbc835..4255417 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -65,7 +65,7 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase): def setUp(self): super(SQLAlchemyPkSequenceTestCase, self).setUp() - StandardFactory.reset_sequence() + StandardFactory.reset_sequence(1) NonIntegerPkFactory.FACTORY_SESSION.rollback() def test_pk_first(self): diff --git a/tests/test_using.py b/tests/test_using.py index 0898a13..01e950f 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -730,6 +730,78 @@ class UsingFactoryTestCase(unittest.TestCase): test_object_alt = TestObjectFactory.build() self.assertEqual(None, test_object_alt.three) + def test_inheritance_and_sequences(self): + """Sequence counters should be kept within an inheritance chain.""" + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + one = factory.Sequence(lambda n: n) + + class TestObjectFactory2(TestObjectFactory): + FACTORY_FOR = TestObject + + to1a = TestObjectFactory() + self.assertEqual(0, to1a.one) + to2a = TestObjectFactory2() + self.assertEqual(1, to2a.one) + to1b = TestObjectFactory() + self.assertEqual(2, to1b.one) + to2b = TestObjectFactory2() + self.assertEqual(3, to2b.one) + + def test_inheritance_sequence_inheriting_objects(self): + """Sequence counters are kept with inheritance, incl. misc objects.""" + class TestObject2(TestObject): + pass + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + one = factory.Sequence(lambda n: n) + + class TestObjectFactory2(TestObjectFactory): + FACTORY_FOR = TestObject2 + + to1a = TestObjectFactory() + self.assertEqual(0, to1a.one) + to2a = TestObjectFactory2() + self.assertEqual(1, to2a.one) + to1b = TestObjectFactory() + self.assertEqual(2, to1b.one) + to2b = TestObjectFactory2() + self.assertEqual(3, to2b.one) + + def test_inheritance_sequence_unrelated_objects(self): + """Sequence counters are kept with inheritance, unrelated objects. + + See issue https://github.com/rbarrois/factory_boy/issues/93 + + Problem: sequence counter is somewhat shared between factories + until the "slave" factory has been called. + """ + + class TestObject2(object): + def __init__(self, one): + self.one = one + + class TestObjectFactory(factory.Factory): + FACTORY_FOR = TestObject + + one = factory.Sequence(lambda n: n) + + class TestObjectFactory2(TestObjectFactory): + FACTORY_FOR = TestObject2 + + to1a = TestObjectFactory() + self.assertEqual(0, to1a.one) + to2a = TestObjectFactory2() + self.assertEqual(0, to2a.one) + to1b = TestObjectFactory() + self.assertEqual(1, to1b.one) + to2b = TestObjectFactory2() + self.assertEqual(1, to2b.one) + + def test_inheritance_with_inherited_class(self): class TestObjectFactory(factory.Factory): FACTORY_FOR = TestObject -- cgit v1.2.3 From ea00fa30273c7355c02a361c1193ea4764528edb Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 24 Sep 2013 20:53:09 +0200 Subject: Release v2.2.0 --- docs/changelog.rst | 5 +++-- factory/__init__.py | 2 +- setup.py | 30 +++++++++++++++--------------- 3 files changed, 19 insertions(+), 18 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 25d6a06..3e07a82 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,8 +3,8 @@ ChangeLog .. _v2.2.0: -2.2.0 (current) ---------------- +2.2.0 (2013-09-24) +------------------ *Bugfix:* @@ -13,6 +13,7 @@ ChangeLog - Properly handle sequences within object inheritance chains. If FactoryA inherits from FactoryB, and their associated classes share the same link, sequence counters will be shared (:issue:`93`) + - Properly handle nested :class:`~factory.SubFactory` overrides *New:* diff --git a/factory/__init__.py b/factory/__init__.py index a71fea5..280ab11 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.1.2' +__version__ = '2.2.0' __author__ = 'Raphaël Barrois ' diff --git a/setup.py b/setup.py index 1b49342..1154061 100755 --- a/setup.py +++ b/setup.py @@ -50,21 +50,21 @@ setup( 'mock', ], classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Framework :: Django', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Libraries :: Python Modules' + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Framework :: Django", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.6", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Libraries :: Python Modules" ], test_suite='tests', test_loader=test_loader, -- cgit v1.2.3 From 2e5046bda818771f474a1514465f71bd62be9dcd Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 25 Sep 2013 10:58:12 +0200 Subject: Fix Django sequences with abstract base models. --- docs/changelog.rst | 11 +++++++++++ docs/orms.rst | 15 +++++++++++++++ factory/base.py | 2 +- tests/djapp/models.py | 11 +++++++++++ tests/test_django.py | 21 +++++++++++++++++++++ 5 files changed, 59 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 3e07a82..e5fe303 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ ChangeLog ========= + +.. _v2.2.1: + +2.2.1 (2013-09-24) +------------------ + +*Bugfix:* + + - Fixed sequence counter for :class:`~factory.django.DjangoModelFactory` when a factory + inherits from another factory relating to an abstract model. + .. _v2.2.0: 2.2.0 (2013-09-24) diff --git a/docs/orms.rst b/docs/orms.rst index a463bfb..33b3e0a 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -74,6 +74,21 @@ All factories for a Django :class:`~django.db.models.Model` should use the [, ] +.. note:: If a :class:`DjangoModelFactory` relates to an :obj:`~django.db.models.Options.abstract` + model, be sure to declare the :class:`DjangoModelFactory` as abstract: + + .. code-block:: python + + class MyAbstractModelFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.MyAbstractModel + ABSTRACT_FACTORY = True + + class MyConcreteModelFactory(MyAbstractModel): + FACTORY_FOR = models.MyConcreteModel + + Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. + + .. class:: FileField Custom declarations for :class:`django.db.models.FileField` diff --git a/factory/base.py b/factory/base.py index 462a60c..8183649 100644 --- a/factory/base.py +++ b/factory/base.py @@ -328,7 +328,7 @@ class BaseFactory(object): """ # Rely upon our parents - if cls._base_factory: + if cls._base_factory and not cls._base_factory._abstract_factory: logger.debug("%r: reusing sequence from %r", cls, cls._base_factory) return cls._base_factory._generate_next_sequence() diff --git a/tests/djapp/models.py b/tests/djapp/models.py index cc34643..3f25fbb 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -44,6 +44,17 @@ class NonIntegerPk(models.Model): bar = models.CharField(max_length=20, blank=True) +class AbstractBase(models.Model): + foo = models.CharField(max_length=20) + + class Meta: + abstract = True + + +class ConcreteSon(AbstractBase): + pass + + 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 b27562c..7cebf8d 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -64,6 +64,8 @@ else: # pragma: no cover models = Fake() models.StandardModel = Fake + models.AbstractBase = Fake + models.ConcreteSon = Fake models.NonIntegerPk = Fake models.WithFile = Fake models.WithImage = Fake @@ -106,6 +108,17 @@ class NonIntegerPkFactory(factory.django.DjangoModelFactory): bar = '' +class AbstractBaseFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.AbstractBase + ABSTRACT_FACTORY = True + + foo = factory.Sequence(lambda n: "foo%d" % n) + + +class ConcreteSonFactory(AbstractBaseFactory): + FACTORY_FOR = models.ConcreteSon + + class WithFileFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.WithFile @@ -223,6 +236,14 @@ class DjangoNonIntegerPkTestCase(django_test.TestCase): self.assertEqual('foo1', nonint2.pk) +@unittest.skipIf(django is None, "Django not installed.") +class DjangoAbstractBaseSequenceTestCase(django_test.TestCase): + def test_auto_sequence(self): + with factory.debug(): + obj = ConcreteSonFactory() + self.assertEqual(1, obj.pk) + + @unittest.skipIf(django is None, "Django not installed.") class DjangoFileFieldTestCase(unittest.TestCase): -- cgit v1.2.3 From 6834fd72331f186a1f1fe793e193b3cfba36d5b7 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Sat, 5 Oct 2013 18:43:52 +0200 Subject: doc: Fix typo in django notes. --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/orms.rst b/docs/orms.rst index 33b3e0a..b720ed1 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -83,7 +83,7 @@ All factories for a Django :class:`~django.db.models.Model` should use the FACTORY_FOR = models.MyAbstractModel ABSTRACT_FACTORY = True - class MyConcreteModelFactory(MyAbstractModel): + class MyConcreteModelFactory(MyAbstractModelFactory): FACTORY_FOR = models.MyConcreteModel Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. -- cgit v1.2.3 From 990bfaf44ce39aaa01a2107aadc1933947bcf550 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 4 Oct 2013 10:52:26 -0700 Subject: Added FuzzyText attribute. Useful for unique model attributes where the specific value can be fuzzy. --- docs/fuzzy.rst | 29 +++++++++++++++++++++++++++++ factory/fuzzy.py | 33 +++++++++++++++++++++++++++++++++ tests/test_fuzzy.py | 41 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 102 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index d88cb5a..582a654 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -24,6 +24,35 @@ FuzzyAttribute The callable that generates random values +FuzzyText +--------- + + +.. class:: FuzzyText(length=12, chars=string.ascii_letters, prefix='') + + The :class:`FuzzyText` fuzzer yields random strings beginning with + the given :attr:`prefix`, followed by :attr:`length` charactes chosen + from the :attr:`chars` character set, + and ending with the given :attr:`suffix`. + + .. attribute:: length + + int, the length of the random part + + .. attribute:: prefix + + text, an optional prefix to prepend to the random part + + .. attribute:: suffix + + text, an optional suffix to append to the random part + + .. attribute:: chars + + char iterable, the chars to choose from; defaults to the list of ascii + letters and numbers. + + FuzzyChoice ----------- diff --git a/factory/fuzzy.py b/factory/fuzzy.py index d3b130b..f3e6a31 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -23,8 +23,11 @@ """Additional declarations for "fuzzy" attribute definitions.""" +from __future__ import unicode_literals + import random +import string import datetime from . import compat @@ -60,6 +63,36 @@ class FuzzyAttribute(BaseFuzzyAttribute): return self.fuzzer() +class FuzzyText(BaseFuzzyAttribute): + """Random string with a given prefix. + + Generates a random string of the given length from chosen chars. + If a prefix or a suffix are supplied, they will be prepended / appended + to the generated string. + + Args: + prefix (text): An optional prefix to prepend to the random string + length (int): the length of the random part + suffix (text): An optional suffix to append to the random string + chars (str list): the chars to choose from + + Useful for generating unique attributes where the exact value is + not important. + """ + + def __init__(self, prefix='', length=12, suffix='', + chars=string.ascii_letters, **kwargs): + super(FuzzyText, self).__init__(**kwargs) + self.prefix = prefix + self.suffix = suffix + self.length = length + self.chars = tuple(chars) # Unroll iterators + + def fuzz(self): + chars = [random.choice(self.chars) for _i in range(self.length)] + return self.prefix + ''.join(chars) + self.suffix + + class FuzzyChoice(BaseFuzzyAttribute): """Handles fuzzy choice of an attribute.""" diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 97abece..a521ee2 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -63,7 +63,7 @@ class FuzzyChoiceTestCase(unittest.TestCase): def options(): for i in range(3): yield i - + d = fuzzy.FuzzyChoice(options()) res = d.evaluate(2, None, False) @@ -401,3 +401,42 @@ class FuzzyDateTimeTestCase(unittest.TestCase): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res) + + +class FuzzyTextTestCase(unittest.TestCase): + + def test_unbiased(self): + chars = ['a', 'b', 'c'] + fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=12) + res = fuzz.evaluate(2, None, False) + + self.assertEqual('pre', res[:3]) + self.assertEqual('post', res[-4:]) + self.assertEqual(3 + 12 + 4, len(res)) + + for char in res[3:-4]: + self.assertIn(char, chars) + + def test_mock(self): + fake_choice = lambda chars: chars[0] + + chars = ['a', 'b', 'c'] + fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4) + with mock.patch('random.choice', fake_choice): + res = fuzz.evaluate(2, None, False) + + self.assertEqual('preaaaapost', res) + + def test_generator(self): + def options(): + yield 'a' + yield 'b' + yield 'c' + + fuzz = fuzzy.FuzzyText(chars=options(), length=12) + res = fuzz.evaluate(2, None, False) + + self.assertEqual(12, len(res)) + + for char in res: + self.assertIn(char, ['a', 'b', 'c']) -- cgit v1.2.3 From dc482f67c83be2b2607e2e9c4b259c03a59d9532 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 29 Oct 2013 00:20:17 +0100 Subject: Update Changelog with FuzzyText (Closes #97). --- docs/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index e5fe303..630b45f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,15 @@ ChangeLog ========= +.. _v2.3.0: + +2.3.0 (master) +-------------- + +*New:* + + - Add :class:`~factory.fuzzy.FuzzyText`, thanks to `jdufresne `_ (:issue:`97`) + .. _v2.2.1: 2.2.1 (2013-09-24) -- cgit v1.2.3 From 132eca8fa36bc7360a3b2270da0c8833378bc718 Mon Sep 17 00:00:00 2001 From: Omer Date: Mon, 30 Sep 2013 12:37:15 +0300 Subject: Added a Fuzzy Decimal attribute. --- docs/fuzzy.rst | 35 ++++++++++++++++++++++++++ factory/fuzzy.py | 29 ++++++++++++++++++---- tests/test_fuzzy.py | 71 +++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 120 insertions(+), 15 deletions(-) (limited to 'docs') diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 582a654..1843920 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -97,6 +97,41 @@ FuzzyInteger .. attribute:: high int, the inclusive higher bound of generated integers + +FuzzyDecimal +------------ + +.. class:: FuzzyDecimal(low[, high]) + + The :class:`FuzzyDecimal` fuzzer generates random integers within a given + inclusive range. + + The :attr:`low` bound may be omitted, in which case it defaults to 0: + + .. code-block:: pycon + + >>> FuzzyDecimal(0.5, 42.7) + >>> fi.low, fi.high + 0.5, 42.7 + + >>> fi = FuzzyDecimal(42.7) + >>> fi.low, fi.high + 0.0, 42.7 + + >>> fi = FuzzyDecimal(0.5, 42.7, 3) + >>> fi.low, fi.high, fi.precision + 0.5, 42.7, 3 + + .. attribute:: low + + decimal, the inclusive lower bound of generated decimals + + .. attribute:: high + + decimal, the inclusive higher bound of generated decimals + + .. attribute:: precision + int, the number of digits to generate after the dot. The default is 2 digits. FuzzyDate diff --git a/factory/fuzzy.py b/factory/fuzzy.py index f3e6a31..7fa0908 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -25,7 +25,7 @@ from __future__ import unicode_literals - +from decimal import Decimal import random import string import datetime @@ -121,8 +121,27 @@ class FuzzyInteger(BaseFuzzyAttribute): return random.randint(self.low, self.high) +class FuzzyDecimal(BaseFuzzyAttribute): + """Random decimal within a given range.""" + + def __init__(self, low, high=None, precision=2, **kwargs): + if high is None: + high = low + low = 0.0 + + self.low = low + self.high = high + self.precision = precision + + super(FuzzyDecimal, self).__init__(**kwargs) + + def fuzz(self): + return Decimal(random.uniform(self.low, self.high)).quantize(Decimal(10) ** -self.precision) + + class FuzzyDate(BaseFuzzyAttribute): """Random date within a given date range.""" + def __init__(self, start_date, end_date=None, **kwargs): super(FuzzyDate, self).__init__(**kwargs) if end_date is None: @@ -150,12 +169,12 @@ class BaseFuzzyDateTime(BaseFuzzyAttribute): if start_dt > end_dt: raise ValueError( """%s boundaries should have start <= end, got %r > %r""" % ( - self.__class__.__name__, start_dt, end_dt)) + 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): + 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: diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index a521ee2..2202c8f 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -22,6 +22,7 @@ import datetime +from decimal import Decimal from factory import compat from factory import fuzzy @@ -108,6 +109,56 @@ class FuzzyIntegerTestCase(unittest.TestCase): self.assertEqual(8, res) +class FuzzyDecimalTestCase(unittest.TestCase): + def test_definition(self): + """Tests all ways of defining a FuzzyDecimal.""" + fuzz = fuzzy.FuzzyDecimal(2.0, 3.0) + for _i in range(20): + res = fuzz.evaluate(2, None, False) + self.assertTrue(Decimal(2.0) <= res <= Decimal(3.0), 'value is not between 2.0 and 3.0. It is %d' % res) + + fuzz = fuzzy.FuzzyDecimal(4.0) + for _i in range(20): + res = fuzz.evaluate(2, None, False) + self.assertTrue(Decimal(0.0) <= res <= Decimal(4.0), 'value is not between 0.0 and 4.0. It is %d' % res) + + fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5) + for _i in range(20): + res = fuzz.evaluate(2, None, False) + self.assertTrue(Decimal(0.54) <= res <= Decimal(4.0), 'value is not between 0.54 and 4.0. It is %d' % res) + self.assertTrue(res.as_tuple().exponent, -5) + + def test_biased(self): + fake_uniform = lambda low, high: low + high + + fuzz = fuzzy.FuzzyDecimal(2.0, 8.0) + + with mock.patch('random.uniform', fake_uniform): + res = fuzz.evaluate(2, None, False) + + self.assertEqual(Decimal(10.0), res) + + def test_biased_high_only(self): + fake_uniform = lambda low, high: low + high + + fuzz = fuzzy.FuzzyDecimal(8.0) + + with mock.patch('random.uniform', fake_uniform): + res = fuzz.evaluate(2, None, False) + + self.assertEqual(Decimal(8.0), res) + + def test_precision(self): + fake_uniform = lambda low, high: low + high + 0.001 + + fuzz = fuzzy.FuzzyDecimal(8.0, precision=3) + + with mock.patch('random.uniform', fake_uniform): + res = fuzz.evaluate(2, None, False) + + self.assertEqual(Decimal(8.001).quantize(Decimal(10) ** -3), res) + + class FuzzyDateTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -137,12 +188,12 @@ class FuzzyDateTestCase(unittest.TestCase): def test_invalid_definition(self): self.assertRaises(ValueError, fuzzy.FuzzyDate, - self.jan31, self.jan1) + self.jan31, self.jan1) def test_invalid_partial_definition(self): with utils.mocked_date_today(self.jan1, fuzzy): self.assertRaises(ValueError, fuzzy.FuzzyDate, - self.jan31) + self.jan31) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -197,12 +248,12 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase): 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) + 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)) + self.jan1, self.jan31.replace(tzinfo=compat.UTC)) def test_force_year(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_year=4) @@ -255,12 +306,12 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase): def test_invalid_definition(self): self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime, - self.jan31, self.jan1) + 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) + self.jan31) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -314,22 +365,22 @@ class FuzzyDateTimeTestCase(unittest.TestCase): def test_invalid_definition(self): self.assertRaises(ValueError, fuzzy.FuzzyDateTime, - self.jan31, self.jan1) + 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) + 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) + 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)) + self.jan1, self.jan31.replace(tzinfo=None)) def test_force_year(self): fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_year=4) -- cgit v1.2.3 From 382e319a934d27e16b9d0ae8597923ab85976694 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 29 Oct 2013 00:23:11 +0100 Subject: Style fixes for FuzzyDecimal (Closes #94). --- docs/changelog.rst | 1 + docs/fuzzy.rst | 2 +- factory/fuzzy.py | 5 +++-- tests/test_fuzzy.py | 34 +++++++++++++++++----------------- 4 files changed, 22 insertions(+), 20 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 630b45f..09de792 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog *New:* - Add :class:`~factory.fuzzy.FuzzyText`, thanks to `jdufresne `_ (:issue:`97`) + - Add :class:`~factory.fuzzy.FuzzyDecimal`, thanks to `thedrow `_ (:issue:`94`) .. _v2.2.1: diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 1843920..b94dfa5 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -97,7 +97,7 @@ FuzzyInteger .. attribute:: high int, the inclusive higher bound of generated integers - + FuzzyDecimal ------------ diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 7fa0908..6c7a866 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -25,7 +25,7 @@ from __future__ import unicode_literals -from decimal import Decimal +import decimal import random import string import datetime @@ -136,7 +136,8 @@ class FuzzyDecimal(BaseFuzzyAttribute): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - return Decimal(random.uniform(self.low, self.high)).quantize(Decimal(10) ** -self.precision) + base = decimal.Decimal(random.uniform(self.low, self.high)) + return base.quantize(decimal.Decimal(10) ** -self.precision) class FuzzyDate(BaseFuzzyAttribute): diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 2202c8f..b97f4bd 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -22,7 +22,7 @@ import datetime -from decimal import Decimal +import decimal from factory import compat from factory import fuzzy @@ -115,17 +115,17 @@ class FuzzyDecimalTestCase(unittest.TestCase): fuzz = fuzzy.FuzzyDecimal(2.0, 3.0) for _i in range(20): res = fuzz.evaluate(2, None, False) - self.assertTrue(Decimal(2.0) <= res <= Decimal(3.0), 'value is not between 2.0 and 3.0. It is %d' % res) + self.assertTrue(decimal.Decimal(2.0) <= res <= Decimal(3.0), 'value is not between 2.0 and 3.0. It is %d' % res) fuzz = fuzzy.FuzzyDecimal(4.0) for _i in range(20): res = fuzz.evaluate(2, None, False) - self.assertTrue(Decimal(0.0) <= res <= Decimal(4.0), 'value is not between 0.0 and 4.0. It is %d' % res) + self.assertTrue(decimal.Decimal(0.0) <= res <= Decimal(4.0), 'value is not between 0.0 and 4.0. It is %d' % res) fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5) for _i in range(20): res = fuzz.evaluate(2, None, False) - self.assertTrue(Decimal(0.54) <= res <= Decimal(4.0), 'value is not between 0.54 and 4.0. It is %d' % res) + self.assertTrue(decimal.Decimal(0.54) <= res <= Decimal(4.0), 'value is not between 0.54 and 4.0. It is %d' % res) self.assertTrue(res.as_tuple().exponent, -5) def test_biased(self): @@ -136,7 +136,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): with mock.patch('random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) - self.assertEqual(Decimal(10.0), res) + self.assertEqual(decimal.Decimal(10.0), res) def test_biased_high_only(self): fake_uniform = lambda low, high: low + high @@ -146,7 +146,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): with mock.patch('random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) - self.assertEqual(Decimal(8.0), res) + self.assertEqual(decimal.Decimal(8.0), res) def test_precision(self): fake_uniform = lambda low, high: low + high + 0.001 @@ -156,7 +156,7 @@ class FuzzyDecimalTestCase(unittest.TestCase): with mock.patch('random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) - self.assertEqual(Decimal(8.001).quantize(Decimal(10) ** -3), res) + self.assertEqual(decimal.Decimal(8.001).quantize(Decimal(10) ** -3), res) class FuzzyDateTestCase(unittest.TestCase): @@ -188,12 +188,12 @@ class FuzzyDateTestCase(unittest.TestCase): def test_invalid_definition(self): self.assertRaises(ValueError, fuzzy.FuzzyDate, - self.jan31, self.jan1) + self.jan31, self.jan1) def test_invalid_partial_definition(self): with utils.mocked_date_today(self.jan1, fuzzy): self.assertRaises(ValueError, fuzzy.FuzzyDate, - self.jan31) + self.jan31) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -248,12 +248,12 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase): 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) + 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)) + self.jan1, self.jan31.replace(tzinfo=compat.UTC)) def test_force_year(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_year=4) @@ -306,12 +306,12 @@ class FuzzyNaiveDateTimeTestCase(unittest.TestCase): def test_invalid_definition(self): self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime, - self.jan31, self.jan1) + 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) + self.jan31) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -365,22 +365,22 @@ class FuzzyDateTimeTestCase(unittest.TestCase): def test_invalid_definition(self): self.assertRaises(ValueError, fuzzy.FuzzyDateTime, - self.jan31, self.jan1) + 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) + 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) + 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)) + self.jan1, self.jan31.replace(tzinfo=None)) def test_force_year(self): fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31, force_year=4) -- cgit v1.2.3 From 7ca7d10e6119061128655dcea5a484ad551441b3 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Mon, 25 Nov 2013 00:52:24 +0100 Subject: Fix typo spotted by @philipkimmey (Closes #110). --- docs/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/introduction.rst b/docs/introduction.rst index 41c6f7b..86e2046 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -265,6 +265,6 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the -The default strategy can ba changed by setting the class-level :attr:`~factory.Factory.FACTROY_STRATEGY` attribute. +The default strategy can be changed by setting the class-level :attr:`~factory.Factory.FACTORY_STRATEGY` attribute. -- cgit v1.2.3 From 8a014a343050625b8d30db5a8fa608b875b018c4 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 25 Dec 2013 01:26:06 +0100 Subject: Add doc for MongoEngineFactory's support of EmbeddedDocument. Closes #100, #112. --- docs/changelog.rst | 1 + docs/orms.rst | 5 +++++ 2 files changed, 6 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 09de792..c9d7e2f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Add :class:`~factory.fuzzy.FuzzyText`, thanks to `jdufresne `_ (:issue:`97`) - Add :class:`~factory.fuzzy.FuzzyDecimal`, thanks to `thedrow `_ (:issue:`94`) + - Add support for :class:`~mongoengine.EmbeddedDocument`, thanks to `imiric `_ (:issue:`100`) .. _v2.2.1: diff --git a/docs/orms.rst b/docs/orms.rst index b720ed1..e50e706 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -200,6 +200,11 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin * :func:`~factory.Factory.create()` builds an instance through ``__init__`` then saves it. + .. note:: If the :attr:`associated class ` is a :class:`mongoengine.EmbeddedDocument`, + the :meth:`~MongoEngineFactory.create` function won't "save" it, since this wouldn't make sense. + + This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document. + SQLAlchemy ---------- -- cgit v1.2.3 From 0c29413e374147cc258c329ab50d96a4cb0c675f Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 25 Dec 2013 01:53:06 +0100 Subject: Release v2.3.0 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index c9d7e2f..adb42a8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.3.0: -2.3.0 (master) --------------- +2.3.0 (2013-12-25) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index c40274f..b4e63be 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.2.1' +__version__ = '2.3.0' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3 From 3feb4f51a58e39a7aaf82222bff27ba181920b2e Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Tue, 21 Jan 2014 21:12:53 +0100 Subject: Update ChangeLog --- docs/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index adb42a8..d18f8e2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,15 @@ ChangeLog ========= +.. _v2.3.1: + +2.3.1 (maint) +------------- + +*Bugfix:* + + - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:issue:`126`) + .. _v2.3.0: 2.3.0 (2013-12-25) -- cgit v1.2.3 From fb794bd1bcfcb84ec82e72d4f9c3b67624724fdf Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 22 Jan 2014 00:26:13 +0100 Subject: Update ChangeLog. --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index d18f8e2..4609821 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ ChangeLog *Bugfix:* - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:issue:`126`) + - Don't crash when handling objects whose __repr__ is non-pure-ascii bytes on Py2, + discovered by `mbertheau `_ (:issue:`123`) .. _v2.3.0: -- cgit v1.2.3 From 497bee605dab138a89661228e071c4e7236c350a Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 22 Jan 2014 22:41:28 +0100 Subject: Fix log_pprint for *args (Closes #127). --- docs/changelog.rst | 2 +- factory/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index 4609821..e75593e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,7 +11,7 @@ ChangeLog - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:issue:`126`) - Don't crash when handling objects whose __repr__ is non-pure-ascii bytes on Py2, - discovered by `mbertheau `_ (:issue:`123`) + discovered by `mbertheau `_ (:issue:`123`) and `strycore `_ (:issue:`127`) .. _v2.3.0: diff --git a/factory/utils.py b/factory/utils.py index b27fd77..276977a 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -113,7 +113,7 @@ def _safe_repr(obj): def log_pprint(args=(), kwargs=None): kwargs = kwargs or {} return ', '.join( - [repr(arg) for arg in args] + + [_safe_repr(arg) for arg in args] + [ '%s=%s' % (key, _safe_repr(value)) for key, value in kwargs.items() -- cgit v1.2.3 From 90db123ada9739a19f3b408b50e006700923f651 Mon Sep 17 00:00:00 2001 From: Raphaël Barrois Date: Wed, 22 Jan 2014 22:54:11 +0100 Subject: Release v2.3.1 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'docs') diff --git a/docs/changelog.rst b/docs/changelog.rst index e75593e..4917578 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.3.1: -2.3.1 (maint) -------------- +2.3.1 (2014-01-22) +------------------ *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index b4e63be..aa550e8 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.3.0' +__version__ = '2.3.1' __author__ = 'Raphaël Barrois ' -- cgit v1.2.3