From ccd6a9927483b8789dee13c2d68a4407e2d37f94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 25 Nov 2013 00:51:31 +0100 Subject: [PATCH 001/714] django: Fix lazy loading of 'son' factories (Closes #109). --- factory/base.py | 18 +++++++++++++----- factory/django.py | 19 +++++++------------ tests/djapp/models.py | 4 ++++ tests/test_django.py | 43 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 60 insertions(+), 24 deletions(-) diff --git a/factory/base.py b/factory/base.py index 81836492..3c6571c0 100644 --- a/factory/base.py +++ b/factory/base.py @@ -175,11 +175,13 @@ def __new__(mcs, class_name, bases, attrs): is_abstract = attrs.pop('ABSTRACT_FACTORY', False) base = parent_factories[0] - inherited_associated_class = getattr(base, - CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + inherited_associated_class = base._get_target_class() associated_class = mcs._discover_associated_class(class_name, attrs, inherited_associated_class) + # Invoke 'lazy-loading' hooks. + associated_class = base._load_target_class(associated_class) + if associated_class is None: is_abstract = True @@ -379,13 +381,19 @@ def _adjust_kwargs(cls, **kwargs): return kwargs @classmethod - def _load_target_class(cls): + def _load_target_class(cls, class_definition): """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) + return class_definition + + @classmethod + def _get_target_class(cls): + """Retrieve the actual, associated target class.""" + definition = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + return cls._load_target_class(definition) @classmethod def _prepare(cls, create, **kwargs): @@ -395,7 +403,7 @@ def _prepare(cls, create, **kwargs): create: bool, whether to create or to build the object **kwargs: arguments to pass to the creation function """ - target_class = cls._load_target_class() + target_class = cls._get_target_class() kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. diff --git a/factory/django.py b/factory/django.py index 016586d2..fee8e524 100644 --- a/factory/django.py +++ b/factory/django.py @@ -58,20 +58,15 @@ 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() + def _load_target_class(cls, definition): - 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 + if is_string(definition) and '.' in definition: + app, model = definition.split('.', 1) + from django.db.models import loading as django_loading + return django_loading.get_model(app, model) - return associated_class + return definition @classmethod def _get_manager(cls, target_class): @@ -84,7 +79,7 @@ def _get_manager(cls, target_class): def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" - model = cls._load_target_class() # pylint: disable=E1101 + model = cls._get_target_class() # pylint: disable=E1101 manager = cls._get_manager(model) try: diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 3f25fbbe..e98279da 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -55,6 +55,10 @@ class ConcreteSon(AbstractBase): pass +class StandardSon(StandardModel): + 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 94101e92..e4bbc2bd 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -64,6 +64,7 @@ class Fake(object): models = Fake() models.StandardModel = Fake + models.StandardSon = None models.AbstractBase = Fake models.ConcreteSon = Fake models.NonIntegerPk = Fake @@ -211,7 +212,7 @@ def test_loading(self): class ExampleFactory(factory.DjangoModelFactory): FACTORY_FOR = 'djapp.StandardModel' - self.assertEqual(models.StandardModel, ExampleFactory._load_target_class()) + self.assertEqual(models.StandardModel, ExampleFactory._get_target_class()) def test_building(self): class ExampleFactory(factory.DjangoModelFactory): @@ -220,16 +221,44 @@ class ExampleFactory(factory.DjangoModelFactory): e = ExampleFactory.build() self.assertEqual(models.StandardModel, e.__class__) - def test_cache(self): + def test_inherited_loading(self): + """Proper loading of a model within 'child' factories. + + See https://github.com/rbarrois/factory_boy/issues/109. + """ + class ExampleFactory(factory.DjangoModelFactory): + FACTORY_FOR = 'djapp.StandardModel' + + class Example2Factory(ExampleFactory): + pass + + e = Example2Factory.build() + self.assertEqual(models.StandardModel, e.__class__) + + def test_inherited_loading_and_sequence(self): + """Proper loading of a model within 'child' factories. + + See https://github.com/rbarrois/factory_boy/issues/109. + """ class ExampleFactory(factory.DjangoModelFactory): FACTORY_FOR = 'djapp.StandardModel' - self.assertEqual('djapp.StandardModel', ExampleFactory._associated_class) - self.assertIsNone(ExampleFactory._associated_model) + foo = factory.Sequence(lambda n: n) + + class Example2Factory(ExampleFactory): + FACTORY_FOR = 'djapp.StandardSon' + + self.assertEqual(models.StandardSon, Example2Factory._get_target_class()) - self.assertEqual(models.StandardModel, ExampleFactory._load_target_class()) - self.assertEqual('djapp.StandardModel', ExampleFactory._associated_class) - self.assertEqual(models.StandardModel, ExampleFactory._associated_model) + e1 = ExampleFactory.build() + e2 = Example2Factory.build() + e3 = ExampleFactory.build() + self.assertEqual(models.StandardModel, e1.__class__) + self.assertEqual(models.StandardSon, e2.__class__) + self.assertEqual(models.StandardModel, e3.__class__) + self.assertEqual(1, e1.foo) + self.assertEqual(2, e2.foo) + self.assertEqual(3, e3.foo) @unittest.skipIf(django is None, "Django not installed.") From 7ca7d10e6119061128655dcea5a484ad551441b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 25 Nov 2013 00:52:24 +0100 Subject: [PATCH 002/714] Fix typo spotted by @philipkimmey (Closes #110). --- docs/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index 41c6f7be..86e20463 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. From b2c15880cc0bfe9a0c149ce3124813e95e8592b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 25 Dec 2013 01:20:08 +0100 Subject: [PATCH 003/714] Packaging: Use codecs.open (Closes #114, #118). --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 11540618..54e4caa1 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import codecs import os import re import sys @@ -13,8 +14,8 @@ def get_version(package_name): version_re = re.compile(r"^__version__ = [\"']([\w_.-]+)[\"']$") package_components = package_name.split('.') - path_components = package_components + ['__init__.py'] - with open(os.path.join(root_dir, *path_components)) as f: + init_path = os.path.join(root_dir, *(package_components + ['__init__.py'])) + with codecs.open(init_path, 'r', 'utf-8') as f: for line in f: match = version_re.match(line[:-1]) if match: From 924d8a6ac279ca6ad560a3cf5efa1b141cedd253 Mon Sep 17 00:00:00 2001 From: Ivan Miric Date: Wed, 16 Oct 2013 22:25:26 +0200 Subject: [PATCH 004/714] Added SubFactory support for MongoEngine's EmbeddedDocument --- factory/mongoengine.py | 3 ++- tests/test_mongoengine.py | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/factory/mongoengine.py b/factory/mongoengine.py index 8cd3a672..462f5f23 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -41,5 +41,6 @@ def _build(cls, target_class, *args, **kwargs): @classmethod def _create(cls, target_class, *args, **kwargs): instance = target_class(*args, **kwargs) - instance.save() + if instance._is_document: + instance.save() return instance diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index f26eb858..803607ac 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -19,7 +19,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -"""Tests for factory_boy/SQLAlchemy interactions.""" +"""Tests for factory_boy/MongoEngine interactions.""" import factory import os @@ -34,14 +34,23 @@ if mongoengine: from factory.mongoengine import MongoEngineFactory + class Address(mongoengine.EmbeddedDocument): + street = mongoengine.StringField() + class Person(mongoengine.Document): name = mongoengine.StringField() + address = mongoengine.EmbeddedDocumentField(Address) + + class AddressFactory(MongoEngineFactory): + FACTORY_FOR = Address + + street = factory.Sequence(lambda n: 'street%d' % n) class PersonFactory(MongoEngineFactory): FACTORY_FOR = Person name = factory.Sequence(lambda n: 'name%d' % n) - + address = factory.SubFactory(AddressFactory) @unittest.skipIf(mongoengine is None, "mongoengine not installed.") @@ -65,11 +74,11 @@ def setUp(self): def test_build(self): std = PersonFactory.build() self.assertEqual('name0', std.name) + self.assertEqual('street0', std.address.street) self.assertIsNone(std.id) def test_creation(self): std1 = PersonFactory.create() self.assertEqual('name1', std1.name) + self.assertEqual('street1', std1.address.street) self.assertIsNotNone(std1.id) - - From 8a014a343050625b8d30db5a8fa608b875b018c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 25 Dec 2013 01:26:06 +0100 Subject: [PATCH 005/714] Add doc for MongoEngineFactory's support of EmbeddedDocument. Closes #100, #112. --- docs/changelog.rst | 1 + docs/orms.rst | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 09de7928..c9d7e2f8 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 b720ed13..e50e706a 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 ---------- From 09c0258f30429970993b5cbc6de603bb0fabcce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 25 Dec 2013 01:45:07 +0100 Subject: [PATCH 006/714] Don't install Pillow on PyPy on travis. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3edfd111..e3617e41 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ script: install: - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - pip install Django Pillow sqlalchemy --use-mirrors + - pip install Django sqlalchemy --use-mirrors + - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors notifications: email: false From 20a0956ca96c5c75c01c217dd00d0378e81da84b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 25 Dec 2013 01:47:12 +0100 Subject: [PATCH 007/714] Typo in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e3617e41..2bfb978e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ script: install: - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - pip install Django sqlalchemy --use-mirrors - - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors + - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi notifications: email: false From 0c29413e374147cc258c329ab50d96a4cb0c675f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 25 Dec 2013 01:53:06 +0100 Subject: [PATCH 008/714] Release v2.3.0 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c9d7e2f8..adb42a81 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 c40274fc..b4e63bef 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 ' From 02e4cfd9bb0e0d1124385f52108bb709fc5d72bf Mon Sep 17 00:00:00 2001 From: Ilya Pirogov Date: Fri, 27 Dec 2013 13:07:34 +0400 Subject: [PATCH 009/714] Added FuzzyInteger support for step --- factory/fuzzy.py | 5 +++-- tests/test_fuzzy.py | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 34949c5c..2ea544a5 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -107,18 +107,19 @@ def fuzz(self): class FuzzyInteger(BaseFuzzyAttribute): """Random integer within a given range.""" - def __init__(self, low, high=None, **kwargs): + def __init__(self, low, high=None, step=1, **kwargs): if high is None: high = low low = 0 self.low = low self.high = high + self.step = step super(FuzzyInteger, self).__init__(**kwargs) def fuzz(self): - return random.randint(self.low, self.high) + return random.randrange(self.low, self.high + 1, self.step) class FuzzyDecimal(BaseFuzzyAttribute): diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index d6f33bba..1caeb0a2 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -89,24 +89,34 @@ def test_definition(self): self.assertIn(res, [0, 1, 2, 3, 4]) def test_biased(self): - fake_randint = lambda low, high: low + high + fake_randrange = lambda low, high, step: (low + high) * step fuzz = fuzzy.FuzzyInteger(2, 8) - with mock.patch('random.randint', fake_randint): + with mock.patch('random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) - self.assertEqual(10, res) + self.assertEqual((2 + 8 + 1) * 1, res) def test_biased_high_only(self): - fake_randint = lambda low, high: low + high + fake_randrange = lambda low, high, step: (low + high) * step fuzz = fuzzy.FuzzyInteger(8) - with mock.patch('random.randint', fake_randint): + with mock.patch('random.randrange', fake_randrange): + res = fuzz.evaluate(2, None, False) + + self.assertEqual((0 + 8 + 1) * 1, res) + + def test_biased_with_step(self): + fake_randrange = lambda low, high, step: (low + high) * step + + fuzz = fuzzy.FuzzyInteger(5, 8, 3) + + with mock.patch('random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) - self.assertEqual(8, res) + self.assertEqual((5 + 8 + 1) * 3, res) class FuzzyDecimalTestCase(unittest.TestCase): From 874b5361d2972dd6feb2d26b74e37eba2434ba04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 30 Dec 2013 14:08:03 +0100 Subject: [PATCH 010/714] Document FuzzyInteger.step --- docs/changelog.rst | 9 +++++++++ docs/fuzzy.rst | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index adb42a81..6658a962 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,15 @@ ChangeLog ========= +.. _v2.4.0: + +2.4.0 (master) +-------------- + +*New:* + + - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) + .. _v2.3.0: 2.3.0 (2013-12-25) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index b94dfa58..2fe60b86 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -73,7 +73,7 @@ FuzzyChoice FuzzyInteger ------------ -.. class:: FuzzyInteger(low[, high]) +.. class:: FuzzyInteger(low[, high[, step]]) The :class:`FuzzyInteger` fuzzer generates random integers within a given inclusive range. @@ -82,7 +82,7 @@ FuzzyInteger .. code-block:: pycon - >>> FuzzyInteger(0, 42) + >>> fi = FuzzyInteger(0, 42) >>> fi.low, fi.high 0, 42 @@ -98,6 +98,12 @@ FuzzyInteger int, the inclusive higher bound of generated integers + .. attribute:: step + + int, the step between values in the range; for instance, a ``FuzzyInteger(0, 42, step=3)`` + might only yield values from ``[0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42]``. + + FuzzyDecimal ------------ From c404da5080c92527e26c6b82d5cc5d7d88ba3d9a Mon Sep 17 00:00:00 2001 From: Robrecht De Rouck Date: Wed, 25 Dec 2013 01:46:22 +0100 Subject: [PATCH 011/714] Document custom manager method recipe (Closes #119). --- docs/recipes.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index c1f37009..9e074137 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -291,3 +291,25 @@ Here, we want: name = "ACME, Inc." country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country')) + + +Custom manager methods +---------------------- + +Sometimes you need a factory to call a specific manager method other then the +default :meth:`Model.objects.create() ` method: + +.. code-block:: python + + class UserFactory(factory.DjangoModelFactory): + FACTORY_FOR = UserenaSignup + username = "l7d8s" + email = "my_name@example.com" + password = "my_password" + + @classmethod + def _create(cls, target_class, *args, **kwargs): + """Override the default ``_create`` with our custom call.""" + manager = cls._get_manager(target_class) + # The default would use ``manager.create(*args, **kwargs)`` + return manager.create_user(*args, **kwargs) From acadd1476d45126a8b7eb4b9a8ac4e1e1faa6478 Mon Sep 17 00:00:00 2001 From: Christoph Sieghart Date: Tue, 21 Jan 2014 20:19:21 +0100 Subject: [PATCH 012/714] Fix bug in LazyStub.__getattr__ When run with optimizations turned on, the assert statement is not executed. This results in incorrect behaviour. --- factory/containers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/factory/containers.py b/factory/containers.py index 7a4c5db3..4537e441 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -103,7 +103,8 @@ def __getattr__(self, name): if isinstance(val, LazyValue): self.__pending.append(name) val = val.evaluate(self, self.__containers) - assert name == self.__pending.pop() + last = self.__pending.pop() + assert name == last self.__values[name] = val return val else: From 3feb4f51a58e39a7aaf82222bff27ba181920b2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Jan 2014 21:12:53 +0100 Subject: [PATCH 013/714] Update ChangeLog --- docs/changelog.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index adb42a81..d18f8e29 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) From 9323fbeea374394833987cb710ac9becb7726a44 Mon Sep 17 00:00:00 2001 From: Ilya Pirogov Date: Mon, 13 Jan 2014 17:53:03 +0400 Subject: [PATCH 014/714] Added "prevent_signals" decorator/context manager --- factory/__init__.py | 2 ++ factory/django.py | 60 ++++++++++++++++++++++++++++++++ factory/helpers.py | 5 +++ tests/djapp/models.py | 4 +++ tests/test_django.py | 80 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/factory/__init__.py b/factory/__init__.py index b4e63bef..251306a3 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -79,5 +79,7 @@ lazy_attribute_sequence, container_attribute, post_generation, + + prevent_signals, ) diff --git a/factory/django.py b/factory/django.py index fee8e524..b502923c 100644 --- a/factory/django.py +++ b/factory/django.py @@ -25,6 +25,9 @@ from __future__ import unicode_literals import os +import types +import logging +import functools """factory_boy extensions for use with the Django framework.""" @@ -39,6 +42,9 @@ from . import declarations from .compat import BytesIO, is_string +logger = logging.getLogger('factory.generate') + + def require_django(): """Simple helper to ensure Django is available.""" @@ -214,3 +220,57 @@ def _make_data(self, params): thumb.save(thumb_io, format=image_format) return thumb_io.getvalue() + +class PreventSignals(object): + """Temporarily disables and then restores any django signals. + + Args: + *signals (django.dispatch.dispatcher.Signal): any django signals + + Examples: + with prevent_signals(pre_init): + user = UserFactory.build() + ... + + @prevent_signals(pre_save, post_save) + class UserFactory(factory.Factory): + ... + + @prevent_signals(post_save) + def generate_users(): + UserFactory.create_batch(10) + """ + + def __init__(self, *signals): + self.signals = signals + self.paused = {} + + def __enter__(self): + for signal in self.signals: + logger.debug('PreventSignals: Disabling signal handlers %r', + signal.receivers) + + self.paused[signal] = signal.receivers + signal.receivers = [] + + def __exit__(self, exc_type, exc_value, traceback): + for signal, receivers in self.paused.items(): + logger.debug('PreventSignals: Restoring signal handlers %r', + receivers) + + signal.receivers = receivers + self.paused = {} + + def __call__(self, func): + if isinstance(func, types.FunctionType): + @functools.wraps(func) + def wrapper(*args, **kwargs): + with self: + return func(*args, **kwargs) + return wrapper + + generate_method = getattr(func, '_generate', None) + if generate_method: + func._generate = classmethod(self(generate_method.__func__)) + + return func \ No newline at end of file diff --git a/factory/helpers.py b/factory/helpers.py index 37b41bfb..719d76d7 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -28,6 +28,7 @@ from . import base from . import declarations +from . import django @contextlib.contextmanager @@ -139,3 +140,7 @@ def container_attribute(func): def post_generation(fun): return declarations.PostGeneration(fun) + + +def prevent_signals(*signals): + return django.PreventSignals(*signals) \ No newline at end of file diff --git a/tests/djapp/models.py b/tests/djapp/models.py index e98279da..a65b50a7 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -74,3 +74,7 @@ class WithImage(models.Model): else: class WithImage(models.Model): pass + + +class WithSignals(models.Model): + foo = models.CharField(max_length=20) \ No newline at end of file diff --git a/tests/test_django.py b/tests/test_django.py index e4bbc2bd..18ffa6b5 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -24,6 +24,7 @@ import factory import factory.django +from factory.helpers import prevent_signals try: @@ -42,7 +43,7 @@ Image = None -from .compat import is_python2, unittest +from .compat import is_python2, unittest, mock from . import testdata from . import tools @@ -55,6 +56,7 @@ from django.db import models as django_models from django.test import simple as django_test_simple from django.test import utils as django_test_utils + from django.db.models import signals from .djapp import models else: # pragma: no cover django_test = unittest @@ -70,6 +72,7 @@ class Fake(object): models.NonIntegerPk = Fake models.WithFile = Fake models.WithImage = Fake + models.WithSignals = Fake test_state = {} @@ -142,6 +145,10 @@ class WithImageFactory(factory.django.DjangoModelFactory): animage = factory.django.ImageField() +class WithSignalsFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.WithSignals + + @unittest.skipIf(django is None, "Django not installed.") class DjangoPkSequenceTestCase(django_test.TestCase): def setUp(self): @@ -511,5 +518,76 @@ def test_no_file(self): self.assertFalse(o.animage) +@unittest.skipIf(django is None, "Django not installed.") +class PreventSignalsTestCase(unittest.TestCase): + def setUp(self): + self.handlers = mock.MagicMock() + + signals.pre_init.connect(self.handlers.pre_init) + signals.pre_save.connect(self.handlers.pre_save) + signals.post_save.connect(self.handlers.post_save) + + def tearDown(self): + signals.pre_init.disconnect(self.handlers.pre_init) + signals.pre_save.disconnect(self.handlers.pre_save) + signals.post_save.disconnect(self.handlers.post_save) + + def test_signals(self): + WithSignalsFactory() + + self.assertEqual(self.handlers.pre_save.call_count, 1) + self.assertEqual(self.handlers.post_save.call_count, 1) + + def test_context_manager(self): + with prevent_signals(signals.pre_save, signals.post_save): + WithSignalsFactory() + + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.test_signals() + + def test_class_decorator(self): + @prevent_signals(signals.pre_save, signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.WithSignals + + WithSignalsDecoratedFactory() + + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.test_signals() + + def test_function_decorator(self): + @prevent_signals(signals.pre_save, signals.post_save) + def foo(): + WithSignalsFactory() + + foo() + + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.test_signals() + + def test_classmethod_decorator(self): + class Foo(object): + @classmethod + @prevent_signals(signals.pre_save, signals.post_save) + def generate(cls): + WithSignalsFactory() + + Foo.generate() + + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.test_signals() + if __name__ == '__main__': # pragma: no cover unittest.main() From dccb37f551d19d9dba68d35a888941cde64f861e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Jan 2014 23:21:03 +0100 Subject: [PATCH 015/714] Improve mute_signals (Closes #122). --- factory/__init__.py | 2 -- factory/django.py | 36 +++++++++++++++++++++--------------- factory/helpers.py | 4 ---- tests/test_django.py | 32 ++++++++++++++++++++++---------- 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index 251306a3..b4e63bef 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -79,7 +79,5 @@ lazy_attribute_sequence, container_attribute, post_generation, - - prevent_signals, ) diff --git a/factory/django.py b/factory/django.py index b502923c..6f39c343 100644 --- a/factory/django.py +++ b/factory/django.py @@ -221,22 +221,22 @@ def _make_data(self, params): return thumb_io.getvalue() -class PreventSignals(object): +class mute_signals(object): """Temporarily disables and then restores any django signals. Args: *signals (django.dispatch.dispatcher.Signal): any django signals Examples: - with prevent_signals(pre_init): + with mute_signals(pre_init): user = UserFactory.build() ... - @prevent_signals(pre_save, post_save) + @mute_signals(pre_save, post_save) class UserFactory(factory.Factory): ... - @prevent_signals(post_save) + @mute_signals(post_save) def generate_users(): UserFactory.create_batch(10) """ @@ -247,7 +247,7 @@ def __init__(self, *signals): def __enter__(self): for signal in self.signals: - logger.debug('PreventSignals: Disabling signal handlers %r', + logger.debug('mute_signals: Disabling signal handlers %r', signal.receivers) self.paused[signal] = signal.receivers @@ -255,22 +255,28 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): for signal, receivers in self.paused.items(): - logger.debug('PreventSignals: Restoring signal handlers %r', + logger.debug('mute_signals: Restoring signal handlers %r', receivers) signal.receivers = receivers self.paused = {} - def __call__(self, func): - if isinstance(func, types.FunctionType): - @functools.wraps(func) + def __call__(self, callable_obj): + if isinstance(callable_obj, base.FactoryMetaClass): + generate_method = getattr(callable_obj, '_generate') + + @functools.wraps(generate_method) + def wrapped_generate(*args, **kwargs): + with self: + return generate_method(*args, **kwargs) + + callable_obj._generate = wrapped_generate + return callable_obj + + else: + @functools.wraps(callable_obj) def wrapper(*args, **kwargs): with self: - return func(*args, **kwargs) + return callable_obj(*args, **kwargs) return wrapper - generate_method = getattr(func, '_generate', None) - if generate_method: - func._generate = classmethod(self(generate_method.__func__)) - - return func \ No newline at end of file diff --git a/factory/helpers.py b/factory/helpers.py index 719d76d7..4a2a2548 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -140,7 +140,3 @@ def container_attribute(func): def post_generation(fun): return declarations.PostGeneration(fun) - - -def prevent_signals(*signals): - return django.PreventSignals(*signals) \ No newline at end of file diff --git a/tests/test_django.py b/tests/test_django.py index 18ffa6b5..50a67a3f 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -24,7 +24,6 @@ import factory import factory.django -from factory.helpers import prevent_signals try: @@ -532,24 +531,24 @@ def tearDown(self): signals.pre_save.disconnect(self.handlers.pre_save) signals.post_save.disconnect(self.handlers.post_save) - def test_signals(self): + def assertSignalsReactivated(self): WithSignalsFactory() self.assertEqual(self.handlers.pre_save.call_count, 1) self.assertEqual(self.handlers.post_save.call_count, 1) def test_context_manager(self): - with prevent_signals(signals.pre_save, signals.post_save): + with factory.django.mute_signals(signals.pre_save, signals.post_save): WithSignalsFactory() self.assertEqual(self.handlers.pre_init.call_count, 1) self.assertFalse(self.handlers.pre_save.called) self.assertFalse(self.handlers.post_save.called) - self.test_signals() + self.assertSignalsReactivated() def test_class_decorator(self): - @prevent_signals(signals.pre_save, signals.post_save) + @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.WithSignals @@ -559,10 +558,23 @@ class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): self.assertFalse(self.handlers.pre_save.called) self.assertFalse(self.handlers.post_save.called) - self.test_signals() + self.assertSignalsReactivated() + + def test_class_decorator_build(self): + @factory.django.mute_signals(signals.pre_save, signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.WithSignals + + WithSignalsDecoratedFactory.build() + + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.assertSignalsReactivated() def test_function_decorator(self): - @prevent_signals(signals.pre_save, signals.post_save) + @factory.django.mute_signals(signals.pre_save, signals.post_save) def foo(): WithSignalsFactory() @@ -572,12 +584,12 @@ def foo(): self.assertFalse(self.handlers.pre_save.called) self.assertFalse(self.handlers.post_save.called) - self.test_signals() + self.assertSignalsReactivated() def test_classmethod_decorator(self): class Foo(object): @classmethod - @prevent_signals(signals.pre_save, signals.post_save) + @factory.django.mute_signals(signals.pre_save, signals.post_save) def generate(cls): WithSignalsFactory() @@ -587,7 +599,7 @@ def generate(cls): self.assertFalse(self.handlers.pre_save.called) self.assertFalse(self.handlers.post_save.called) - self.test_signals() + self.assertSignalsReactivated() if __name__ == '__main__': # pragma: no cover unittest.main() From 86718596359110b7fce5efbb7c28cba10f11b77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Jan 2014 23:33:02 +0100 Subject: [PATCH 016/714] Add doc for factory.django.mute_signals. --- docs/changelog.rst | 1 + docs/orms.rst | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0bf0eb3c..c051916b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog *New:* - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) + - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) .. _v2.3.1: diff --git a/docs/orms.rst b/docs/orms.rst index e50e706a..c893cacd 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -89,6 +89,10 @@ All factories for a Django :class:`~django.db.models.Model` should use the Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. +Extra fields +"""""""""""" + + .. class:: FileField Custom declarations for :class:`django.db.models.FileField` @@ -157,6 +161,43 @@ All factories for a Django :class:`~django.db.models.Model` should use the None +Disabling signals +""""""""""""""""" + +Signals are often used to plug some custom code into external components code; +for instance to create ``Profile`` objects on-the-fly when a new ``User`` object is saved. + +This may interfere with finely tuned :class:`factories `, which would +create both using :class:`~factory.RelatedFactory`. + +To work around this problem, use the :meth:`mute_signals()` decorator/context manager: + +.. method:: mute_signals(signal1, ...) + + Disable the list of selected signals when calling the factory, and reactivate them upon leaving. + +.. code-block:: python + + # foo/factories.py + + import factory + import factory.django + + from . import models + from . import signals + + @factory.django.mute_signals(signals.pre_save, signals.post_save) + class FooFactory(factory.django.DjangoModelFactory): + FACTORY_FOR = models.Foo + + # ... + + def make_chain(): + with factory.django.mute_signals(signals.pre_save, signals.post_save): + # pre_save/post_save won't be called here. + return SomeFactory(), SomeOtherFactory() + + Mogo ---- From 5cd2ba8b89a1dc575b818f8c7be20dd3f0f6c05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Jan 2014 23:20:26 +0100 Subject: [PATCH 017/714] Update requirements --- dev_requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index e828644f..bdc23d05 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -2,4 +2,5 @@ coverage Django Pillow sqlalchemy -mongoengine \ No newline at end of file +mongoengine +mock From f907c405cf233b4ef14817952c11979125b4732c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 00:03:10 +0100 Subject: [PATCH 018/714] Fix log_pprint (Closes #123). --- factory/utils.py | 21 +++++++++++++++++-- tests/test_utils.py | 51 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/factory/utils.py b/factory/utils.py index 48c6eede..b27fd774 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals + import collections #: String for splitting an attribute name into a @@ -96,11 +98,26 @@ def import_object(module_name, attribute_name): return getattr(module, attribute_name) +def _safe_repr(obj): + try: + obj_repr = repr(obj) + except UnicodeError: + return '' % id(obj) + + try: # Convert to "text type" (= unicode) + return '%s' % obj_repr + except UnicodeError: # non-ascii bytes repr on Py2 + return obj_repr.decode('utf-8') + + 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()] + [repr(arg) for arg in args] + + [ + '%s=%s' % (key, _safe_repr(value)) + for key, value in kwargs.items() + ] ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 8c739350..d321c2ab 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -20,11 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals + + import itertools from factory import utils -from .compat import unittest +from .compat import is_python2, unittest class ExtractDictTestCase(unittest.TestCase): @@ -233,6 +236,52 @@ def test_invalid_module(self): 'this-is-an-invalid-module', '__name__') +class LogPPrintTestCase(unittest.TestCase): + def test_nothing(self): + txt = utils.log_pprint() + self.assertEqual('', txt) + + def test_only_args(self): + txt = utils.log_pprint((1, 2, 3)) + self.assertEqual('1, 2, 3', txt) + + def test_only_kwargs(self): + txt = utils.log_pprint(kwargs={'a': 1, 'b': 2}) + self.assertIn(txt, ['a=1, b=2', 'b=2, a=1']) + + def test_bytes_args(self): + txt = utils.log_pprint((b'\xe1\xe2',)) + expected = "b'\\xe1\\xe2'" + if is_python2: + expected = expected.lstrip('b') + self.assertEqual(expected, txt) + + def test_text_args(self): + txt = utils.log_pprint(('ŧêßŧ',)) + expected = "'ŧêßŧ'" + if is_python2: + expected = "u'\\u0167\\xea\\xdf\\u0167'" + self.assertEqual(expected, txt) + + def test_bytes_kwargs(self): + txt = utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'}) + expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'" + expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'" + if is_python2: + expected1 = expected1.replace('b', '') + expected2 = expected2.replace('b', '') + self.assertIn(txt, (expected1, expected2)) + + def test_text_kwargs(self): + txt = utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'}) + expected1 = "x='ŧêßŧ', y='ŧßêŧ'" + expected2 = "y='ŧßêŧ', x='ŧêßŧ'" + if is_python2: + expected1 = "x=u'\\u0167\\xea\\xdf\\u0167', y=u'\\u0167\\xdf\\xea\\u0167'" + expected2 = "y=u'\\u0167\\xdf\\xea\\u0167', x=u'\\u0167\\xea\\xdf\\u0167'" + self.assertIn(txt, (expected1, expected2)) + + class ResetableIteratorTestCase(unittest.TestCase): def test_no_reset(self): i = utils.ResetableIterator([1, 2, 3]) From e72d94b45312443d076d01ca833f19e8a5295847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 00:23:57 +0100 Subject: [PATCH 019/714] Fix mute_signals' decorator. --- factory/django.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/factory/django.py b/factory/django.py index 6f39c343..a3dfdfcd 100644 --- a/factory/django.py +++ b/factory/django.py @@ -263,8 +263,10 @@ def __exit__(self, exc_type, exc_value, traceback): def __call__(self, callable_obj): if isinstance(callable_obj, base.FactoryMetaClass): - generate_method = getattr(callable_obj, '_generate') + # Retrieve __func__, the *actual* callable object. + generate_method = callable_obj._generate.__func__ + @classmethod @functools.wraps(generate_method) def wrapped_generate(*args, **kwargs): with self: From 6f5169ab871e7bedb098c9ebcafcb46decad59ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 00:03:10 +0100 Subject: [PATCH 020/714] Fix log_pprint (Closes #123). --- factory/utils.py | 21 +++++++++++++++++-- tests/test_utils.py | 51 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/factory/utils.py b/factory/utils.py index 48c6eede..b27fd774 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals + import collections #: String for splitting an attribute name into a @@ -96,11 +98,26 @@ def import_object(module_name, attribute_name): return getattr(module, attribute_name) +def _safe_repr(obj): + try: + obj_repr = repr(obj) + except UnicodeError: + return '' % id(obj) + + try: # Convert to "text type" (= unicode) + return '%s' % obj_repr + except UnicodeError: # non-ascii bytes repr on Py2 + return obj_repr.decode('utf-8') + + 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()] + [repr(arg) for arg in args] + + [ + '%s=%s' % (key, _safe_repr(value)) + for key, value in kwargs.items() + ] ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 8c739350..d321c2ab 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -20,11 +20,14 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals + + import itertools from factory import utils -from .compat import unittest +from .compat import is_python2, unittest class ExtractDictTestCase(unittest.TestCase): @@ -233,6 +236,52 @@ def test_invalid_module(self): 'this-is-an-invalid-module', '__name__') +class LogPPrintTestCase(unittest.TestCase): + def test_nothing(self): + txt = utils.log_pprint() + self.assertEqual('', txt) + + def test_only_args(self): + txt = utils.log_pprint((1, 2, 3)) + self.assertEqual('1, 2, 3', txt) + + def test_only_kwargs(self): + txt = utils.log_pprint(kwargs={'a': 1, 'b': 2}) + self.assertIn(txt, ['a=1, b=2', 'b=2, a=1']) + + def test_bytes_args(self): + txt = utils.log_pprint((b'\xe1\xe2',)) + expected = "b'\\xe1\\xe2'" + if is_python2: + expected = expected.lstrip('b') + self.assertEqual(expected, txt) + + def test_text_args(self): + txt = utils.log_pprint(('ŧêßŧ',)) + expected = "'ŧêßŧ'" + if is_python2: + expected = "u'\\u0167\\xea\\xdf\\u0167'" + self.assertEqual(expected, txt) + + def test_bytes_kwargs(self): + txt = utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'}) + expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'" + expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'" + if is_python2: + expected1 = expected1.replace('b', '') + expected2 = expected2.replace('b', '') + self.assertIn(txt, (expected1, expected2)) + + def test_text_kwargs(self): + txt = utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'}) + expected1 = "x='ŧêßŧ', y='ŧßêŧ'" + expected2 = "y='ŧßêŧ', x='ŧêßŧ'" + if is_python2: + expected1 = "x=u'\\u0167\\xea\\xdf\\u0167', y=u'\\u0167\\xdf\\xea\\u0167'" + expected2 = "y=u'\\u0167\\xdf\\xea\\u0167', x=u'\\u0167\\xea\\xdf\\u0167'" + self.assertIn(txt, (expected1, expected2)) + + class ResetableIteratorTestCase(unittest.TestCase): def test_no_reset(self): i = utils.ResetableIterator([1, 2, 3]) From fb794bd1bcfcb84ec82e72d4f9c3b67624724fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 00:26:13 +0100 Subject: [PATCH 021/714] Update ChangeLog. --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d18f8e29..46098218 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: From ecbe5557f99f2f4b14b6c9b4bb79993269906fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 22:41:28 +0100 Subject: [PATCH 022/714] Fix log_pprint for *args (Closes #127). --- docs/changelog.rst | 2 +- factory/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b86d865a..57b29c4d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,7 +21,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 b27fd774..276977a1 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() From 497bee605dab138a89661228e071c4e7236c350a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 22:41:28 +0100 Subject: [PATCH 023/714] Fix log_pprint for *args (Closes #127). --- docs/changelog.rst | 2 +- factory/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 46098218..e75593e8 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 b27fd774..276977a1 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() From 39383fea8bd5bd58b063d9c9fbb44301e781fd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 22:47:11 +0100 Subject: [PATCH 024/714] fuzzy: Add FuzzyFloat (Closes #124). As suggested by @savingschampion --- docs/changelog.rst | 1 + docs/fuzzy.rst | 30 ++++++++++++++++++++++++++++-- factory/fuzzy.py | 17 +++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 57b29c4d..aba1d76f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) + - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`) .. _v2.3.1: diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 2fe60b86..14804192 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -107,9 +107,9 @@ FuzzyInteger FuzzyDecimal ------------ -.. class:: FuzzyDecimal(low[, high]) +.. class:: FuzzyDecimal(low[, high[, precision=2]]) - The :class:`FuzzyDecimal` fuzzer generates random integers within a given + The :class:`FuzzyDecimal` fuzzer generates random :class:`decimals ` within a given inclusive range. The :attr:`low` bound may be omitted, in which case it defaults to 0: @@ -140,6 +140,32 @@ FuzzyDecimal int, the number of digits to generate after the dot. The default is 2 digits. +FuzzyFloat +---------- + +.. class:: FuzzyFloat(low[, high]) + + The :class:`FuzzyFloat` fuzzer provides random :class:`float` objects within a given inclusive range. + + .. code-block:: pycon + + >>> FuzzyFloat(0.5, 42.7) + >>> fi.low, fi.high + 0.5, 42.7 + + >>> fi = FuzzyFloat(42.7) + >>> fi.low, fi.high + 0.0, 42.7 + + + .. attribute:: low + + decimal, the inclusive lower bound of generated floats + + .. attribute:: high + + decimal, the inclusive higher bound of generated floats + FuzzyDate --------- diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 2ea544a5..94599b72 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -141,6 +141,23 @@ def fuzz(self): return base.quantize(decimal.Decimal(10) ** -self.precision) +class FuzzyFloat(BaseFuzzyAttribute): + """Random float within a given range.""" + + def __init__(self, low, high=None, **kwargs): + if high is None: + high = low + low = 0 + + self.low = low + self.high = high + + super(FuzzyFloat, self).__init__(**kwargs) + + def fuzz(self): + return random.uniform(self.low, self.high) + + class FuzzyDate(BaseFuzzyAttribute): """Random date within a given date range.""" From 90db123ada9739a19f3b408b50e006700923f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 22 Jan 2014 22:54:11 +0100 Subject: [PATCH 025/714] Release v2.3.1 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e75593e8..49175786 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 b4e63bef..aa550e8d 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 ' From 420bd3703717623491e710cc63ba57fa1561ab8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 26 Jan 2014 23:46:13 +0100 Subject: [PATCH 026/714] Typo --- docs/recipes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 9e074137..7a6bf23b 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -66,7 +66,7 @@ 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``. +using a :class:`~django.db.models.OneToOneField` from the ``Profile`` to the ``User``. A typical way to create those profiles was to hook a post-save signal to the ``User`` model. From 595c47244b0bfce3023c14908c0cc6b6bb4e0aec Mon Sep 17 00:00:00 2001 From: anentropic Date: Mon, 3 Feb 2014 14:19:42 +0000 Subject: [PATCH 027/714] Make safe repr more safe --- factory/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/utils.py b/factory/utils.py index 276977a1..7b48a1e1 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -101,7 +101,7 @@ def import_object(module_name, attribute_name): def _safe_repr(obj): try: obj_repr = repr(obj) - except UnicodeError: + except: return '' % id(obj) try: # Convert to "text type" (= unicode) From e23b48847dddb27e7dc7400b994c3fd830b0f48f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 28 Feb 2014 00:07:30 +0100 Subject: [PATCH 028/714] Add test ensuring that classmethods aren't altered (See #135). --- tests/test_base.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_base.py b/tests/test_base.py index 8cea6fc4..ba691641 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -88,6 +88,20 @@ class TestObjectFactory(base.Factory): self.assertRaises(base.FactoryError, TestObjectFactory.create) +class DeclarationParsingTests(unittest.TestCase): + def test_classmethod(self): + class TestObjectFactory(base.Factory): + FACTORY_FOR = TestObject + + @classmethod + def some_classmethod(cls): + return cls.create() + + self.assertTrue(hasattr(TestObjectFactory, 'some_classmethod')) + obj = TestObjectFactory.some_classmethod() + self.assertEqual(TestObject, obj.__class__) + + class FactoryTestCase(unittest.TestCase): def test_factory_for(self): class TestObjectFactory(base.Factory): From e320cabc58bdb637f24d4b20df8c318a7420a55e Mon Sep 17 00:00:00 2001 From: anentropic Date: Thu, 13 Mar 2014 18:59:32 +0000 Subject: [PATCH 029/714] Update utils.py --- factory/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/utils.py b/factory/utils.py index 7b48a1e1..6f0c763d 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -101,7 +101,7 @@ def import_object(module_name, attribute_name): def _safe_repr(obj): try: obj_repr = repr(obj) - except: + except Exception: return '' % id(obj) try: # Convert to "text type" (= unicode) From 42d3d4b8c543850668186440d5a3ce93e2832c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 20 Apr 2014 11:59:10 +0200 Subject: [PATCH 030/714] Fix 'gif' image tests. The latest pillow has changed the default gif palette, so we'll use a normalized RGB palette instead. --- tests/test_django.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 50a67a3f..29453e62 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -445,9 +445,9 @@ def test_gif(self): self.assertEqual('django/example.jpg', o.animage.name) i = Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name)) - colors = i.getcolors() - # 169 pixels with color 190 from the GIF palette - self.assertEqual([(169, 190)], colors) + colors = i.convert('RGB').getcolors() + # 169 pixels with rgb(0, 0, 255) + self.assertEqual([(169, (0, 0, 255))], colors) self.assertEqual('GIF', i.format) def test_with_file(self): From 1fdba9d9417e8f69b39784ee19129a6c43128620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 May 2014 14:49:09 +0200 Subject: [PATCH 031/714] Improve README. --- README.rst | 51 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 0371b28a..b35adc55 100644 --- a/README.rst +++ b/README.rst @@ -6,20 +6,61 @@ factory_boy factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. -Its features include: +As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures +with easy-to-use factories for complex object. -- Straightforward syntax +Instead of building an exhaustive test setup with every possible combination of corner cases, +``factory_boy`` allows you to use objects customized for the current test, +while only declaring the test-specific fields: + +.. code-block:: python + + class FooTests(unittest.TestCase): + + def test_with_factory_boy(self): + # We need a 200€, paid order, shipping to australia, for a VIP customer + order = OrderFactory( + amount=200, + status='PAID', + customer__is_vip=True, + address__country='AU', + ) + # Run the tests here + + def test_without_factory_boy(self): + address = Address( + street="42 fubar street", + zipcode="42Z42", + city="Sydney", + country="AU", + ) + customer = Customer( + first_name="John", + last_name="Doe", + phone="+1234", + email="john.doe@example.org", + active=True, + is_vip=True, + address=address, + ) + # etc. + +factory_boy is designed to work well with various ORMs (Django, Mogo, SQLAlchemy), +and can easily be extended for other libraries. + +Its main features include: + +- Straightforward declarative syntax +- Chaining factory calls while retaining the global context - Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects) -- 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, SQLAlchemy) Links ----- * Documentation: http://factoryboy.readthedocs.org/ -* Official repository: https://github.com/rbarrois/factory_boy +* Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library. From 69894fce7977ea55f8cc3ad141840bab49330859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 May 2014 18:47:04 +0200 Subject: [PATCH 032/714] Switch FACTORY_FOR and related to 'class Meta'. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is easier to declare, avoids cluttering the namespace, and provides entry points for ORM-specific customization. Signed-off-by: Raphaël Barrois --- factory/base.py | 350 +++++++++++++++++++++++++------------------- factory/django.py | 31 +++- factory/helpers.py | 4 +- setup.py | 2 +- tests/alter_time.py | 2 +- tests/test_base.py | 30 ++-- 6 files changed, 244 insertions(+), 175 deletions(-) diff --git a/factory/base.py b/factory/base.py index 3c6571c0..cc1fb57d 100644 --- a/factory/base.py +++ b/factory/base.py @@ -23,6 +23,7 @@ import logging from . import containers +from . import declarations from . import utils logger = logging.getLogger('factory.generate') @@ -33,15 +34,6 @@ STUB_STRATEGY = 'stub' -# Special declarations -FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' - -# Factory class attributes -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): """Any exception raised by factory_boy.""" @@ -66,6 +58,14 @@ def get_factory_bases(bases): return [b for b in bases if issubclass(b, BaseFactory)] +def resolve_attribute(name, bases, default=None): + """Find the first definition of an attribute according to MRO order.""" + for base in bases: + if hasattr(base, name): + return getattr(base, name) + return default + + class FactoryMetaClass(type): """Factory metaclass for handling ordered declarations.""" @@ -75,142 +75,194 @@ def __call__(cls, **kwargs): Returns an instance of the associated class. """ - if cls.FACTORY_STRATEGY == BUILD_STRATEGY: + if cls._meta.strategy == BUILD_STRATEGY: return cls.build(**kwargs) - elif cls.FACTORY_STRATEGY == CREATE_STRATEGY: + elif cls._meta.strategy == CREATE_STRATEGY: return cls.create(**kwargs) - elif cls.FACTORY_STRATEGY == STUB_STRATEGY: + elif cls._meta.strategy == STUB_STRATEGY: return cls.stub(**kwargs) else: raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format( - cls.FACTORY_STRATEGY)) + cls._meta.strategy)) - @classmethod - def _discover_associated_class(mcs, class_name, attrs, inherited=None): - """Try to find the class associated with this factory. + def __new__(mcs, class_name, bases, attrs): + """Record attributes as a pattern for later instance construction. - In order, the following tests will be performed: - - Lookup the FACTORY_CLASS_DECLARATION attribute - - If an inherited associated class was provided, use it. + This is called when a new Factory subclass is defined; it will collect + attribute declaration from the class definition. Args: - class_name (str): the name of the factory class being created - attrs (dict): the dict of attributes from the factory class + class_name (str): the name of the class being created + bases (list of class): the parents of the class being created + attrs (str => obj dict): the attributes as defined in the class definition - inherited (class): the optional associated class inherited from a - parent factory Returns: - class: the class to associate with this factory + A new class """ - if FACTORY_CLASS_DECLARATION in attrs: - return attrs[FACTORY_CLASS_DECLARATION] + parent_factories = get_factory_bases(bases) + if parent_factories: + base_factory = parent_factories[0] + else: + base_factory = None - # No specific associated class was given, and one was defined for our - # parent, use it. - if inherited is not None: - return inherited + attrs_meta = attrs.pop('Meta', None) - # Nothing found, return None. - return None + oldstyle_attrs = {} + for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items(): + if old_name in attrs: + oldstyle_attrs[new_name] = attrs.pop(old_name) + if oldstyle_attrs: + attrs_meta = type('Meta', (object,), oldstyle_attrs) - @classmethod - def _extract_declarations(mcs, bases, attributes): - """Extract declarations from a class definition. + base_meta = resolve_attribute('_meta', bases) + options_class = resolve_attribute('_options_class', bases, FactoryOptions) - Args: - bases (class list): parent Factory subclasses - attributes (dict): attributes declared in the class definition + meta = options_class() + attrs['_meta'] = meta - Returns: - dict: the original attributes, where declarations have been moved to - _declarations and post-generation declarations to - _postgen_declarations. - """ - declarations = containers.DeclarationDict() - postgen_declarations = containers.PostGenerationDeclarationDict() + new_class = super(FactoryMetaClass, mcs).__new__( + mcs, class_name, bases, attrs) - # Add parent declarations in reverse order. - for base in reversed(bases): - # Import parent PostGenerationDeclaration - postgen_declarations.update_with_public( - getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {})) - # Import all 'public' attributes (avoid those starting with _) - declarations.update_with_public( - getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) + meta.contribute_to_class(new_class, + meta=attrs_meta, + base_meta=base_meta, + base_factory=base_factory, + ) - # Import attributes from the class definition - attributes = postgen_declarations.update_with_public(attributes) - # Store protected/private attributes in 'non_factory_attrs'. - attributes = declarations.update_with_public(attributes) + return new_class - # Store the DeclarationDict in the attributes of the newly created class - attributes[CLASS_ATTRIBUTE_DECLARATIONS] = declarations - attributes[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations + def __str__(cls): + if cls._meta.abstract: + return '<%s (abstract)>' % cls.__name__ + else: + return '<%s for %s>' % (cls.__name__, cls._meta.target) - return attributes - def __new__(mcs, class_name, bases, attrs): - """Record attributes as a pattern for later instance construction. +class BaseMeta: + abstract = True + strategy = CREATE_STRATEGY - This is called when a new Factory subclass is defined; it will collect - attribute declaration from the class definition. - Args: - class_name (str): the name of the class being created - bases (list of class): the parents of the class being created - attrs (str => obj dict): the attributes as defined in the class - definition +class OptionDefault(object): + def __init__(self, name, value, inherit=False): + self.name = name + self.value = value + self.inherit = inherit - Returns: - A new class - """ - parent_factories = get_factory_bases(bases) - if not parent_factories: - return super(FactoryMetaClass, mcs).__new__( - mcs, class_name, bases, attrs) - - extra_attrs = {} + def apply(self, meta, base_meta): + value = self.value + if self.inherit and base_meta is not None: + value = getattr(base_meta, self.name, value) + if meta is not None: + value = getattr(meta, self.name, value) + return value - is_abstract = attrs.pop('ABSTRACT_FACTORY', False) + def __str__(self): + return '%s(%r, %r, inherit=%r)' % ( + self.__class__.__name__, + self.name, self.value, self.inherit) - base = parent_factories[0] - inherited_associated_class = base._get_target_class() - associated_class = mcs._discover_associated_class(class_name, attrs, - inherited_associated_class) - # Invoke 'lazy-loading' hooks. - associated_class = base._load_target_class(associated_class) +class FactoryOptions(object): + def __init__(self): + self.factory = None + self.base_factory = None + self.declarations = {} + self.postgen_declarations = {} - if associated_class is None: - is_abstract = True + def _build_default_options(self): + """"Provide the default value for all allowed fields. + Custom FactoryOptions classes should override this method + to update() its return value. + """ + return [ + OptionDefault('target', None, inherit=True), + OptionDefault('abstract', False, inherit=False), + OptionDefault('strategy', CREATE_STRATEGY, inherit=True), + OptionDefault('arg_parameters', (), inherit=True), + OptionDefault('hidden_args', (), inherit=True), + ] + + def _fill_from_meta(self, meta, base_meta): + # Exclude private/protected fields from the meta + if meta is None: + meta_attrs = {} + else: + meta_attrs = dict((k, v) + for (k, v) in vars(meta).items() + if not k.startswith('_') + ) + + for option in self._build_default_options(): + assert not hasattr(self, option.name), "Can't override field %s." % option.name + value = option.apply(meta, base_meta) + meta_attrs.pop(option.name, None) + setattr(self, option.name, value) + + if meta_attrs: + # Some attributes in the Meta aren't allowed here + raise TypeError("'class Meta' for %r got unknown attribute(s) %s" + % (self.factory, ','.join(sorted(meta_attrs.keys())))) + + def contribute_to_class(self, factory, + meta=None, base_meta=None, base_factory=None): + + self.factory = factory + self.base_factory = base_factory + + self._fill_from_meta(meta=meta, base_meta=base_meta) + + self.target = self.factory._load_target_class(self.target) + if self.target is None: + self.abstract = True + + if (self.target is not None + and self.base_factory is not None + and self.base_factory._meta.target is not None + and issubclass(self.target, base_factory._meta.target)): + self.counter_reference = self.base_factory else: - # If inheriting the factory from a parent, keep a link to it. - # This allows to use the sequence counters from the parents. - if (inherited_associated_class is not None - and issubclass(associated_class, inherited_associated_class)): - attrs['_base_factory'] = base + self.counter_reference = self.factory - # 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 + for parent in self.factory.__mro__[1:]: + if not hasattr(parent, '_meta'): + continue + self.declarations.update(parent._meta.declarations) + self.postgen_declarations.update(parent._meta.postgen_declarations) - extra_attrs[CLASS_ATTRIBUTE_IS_ABSTRACT] = is_abstract + for k, v in vars(self.factory).items(): + if self._is_declaration(k, v): + self.declarations[k] = v + if self._is_postgen_declaration(k, v): + self.postgen_declarations[k] = v - # Extract pre- and post-generation declarations - attributes = mcs._extract_declarations(parent_factories, attrs) - attributes.update(extra_attrs) + def _is_declaration(self, name, value): + """Determines if a class attribute is a field value declaration. - return super(FactoryMetaClass, mcs).__new__( - mcs, class_name, bases, attributes) + Based on the name and value of the class attribute, return ``True`` if + it looks like a declaration of a default field value, ``False`` if it + is private (name starts with '_') or a classmethod or staticmethod. - def __str__(cls): - if cls._abstract_factory: - return '<%s (abstract)>' - else: - return '<%s for %s>' % (cls.__name__, - getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + """ + if isinstance(value, (classmethod, staticmethod)): + return False + elif isinstance(value, declarations.OrderedDeclaration): + return True + elif isinstance(value, declarations.PostGenerationDeclaration): + return False + return not name.startswith("_") + + def _is_postgen_declaration(self, name, value): + """Captures instances of PostGenerationDeclaration.""" + return isinstance(value, declarations.PostGenerationDeclaration) + + def __str__(self): + return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__) + + def __repr__(self): + return str(self) # Factory base classes @@ -252,25 +304,18 @@ def __new__(cls, *args, **kwargs): """Would be called if trying to instantiate the class.""" raise FactoryError('You cannot instantiate BaseFactory') - # ID to use for the next 'declarations.Sequence' attribute. - _counter = None - - # Base factory, if this class was inherited from another factory. This is - # used for sharing the sequence _counter among factories for the same - # class. - _base_factory = None - - # Holds the target class, once resolved. - _associated_class = None + _meta = FactoryOptions() - # Whether this factory is considered "abstract", thus uncallable. - _abstract_factory = False + _OLDSTYLE_ATTRIBUTES = { + 'FACTORY_FOR': 'target', + 'ABSTRACT_FACTORY': 'abstract', + 'FACTORY_STRATEGY': 'strategy', + 'FACTORY_ARG_PARAMETERS': 'arg_parameters', + 'FACTORY_HIDDEN_ARGS': 'hidden_args', + } - # List of arguments that should be passed as *args instead of **kwargs - FACTORY_ARG_PARAMETERS = () - - # List of attributes that should not be passed to the underlying class - FACTORY_HIDDEN_ARGS = () + # ID to use for the next 'declarations.Sequence' attribute. + _counter = None @classmethod def reset_sequence(cls, value=None, force=False): @@ -282,9 +327,9 @@ def reset_sequence(cls, value=None, force=False): force (bool): whether to force-reset parent sequence counters in a factory inheritance chain. """ - if cls._base_factory: + if cls._meta.counter_reference is not cls: if force: - cls._base_factory.reset_sequence(value=value) + cls._meta.base_factory.reset_sequence(value=value) else: raise ValueError( "Cannot reset the sequence of a factory subclass. " @@ -330,9 +375,9 @@ def _generate_next_sequence(cls): """ # Rely upon our parents - 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() + if cls._meta.counter_reference is not cls: + logger.debug("%r: reusing sequence from %r", cls, cls._meta.base_factory) + return cls._meta.base_factory._generate_next_sequence() # Make sure _counter is initialized cls._setup_counter() @@ -373,7 +418,9 @@ def declarations(cls, extra_defs=None): extra_defs (dict): additional definitions to insert into the retrieved DeclarationDict. """ - return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs) + decls = cls._meta.declarations.copy() + decls.update(extra_defs) + return decls @classmethod def _adjust_kwargs(cls, **kwargs): @@ -392,7 +439,7 @@ def _load_target_class(cls, class_definition): @classmethod def _get_target_class(cls): """Retrieve the actual, associated target class.""" - definition = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + definition = cls._meta.target return cls._load_target_class(definition) @classmethod @@ -407,11 +454,11 @@ def _prepare(cls, create, **kwargs): kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. - for arg in cls.FACTORY_HIDDEN_ARGS: + for arg in cls._meta.hidden_args: del kwargs[arg] # Extract *args from **kwargs - args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) + args = tuple(kwargs.pop(key) for key in cls._meta.arg_parameters) logger.debug('BaseFactory: Generating %s.%s(%s)', cls.__module__, @@ -431,15 +478,14 @@ def _generate(cls, create, attrs): create (bool): whether to 'build' or 'create' the object attrs (dict): attributes to use for generating the object """ - if cls._abstract_factory: + if cls._meta.abstract: 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)) + "Ensure %(f)s.Meta.target is set and %(f)s.Meta.abstract " + "is either not set or False." % dict(f=cls.__name__)) # Extract declarations used for post-generation - postgen_declarations = getattr(cls, - CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) + postgen_declarations = cls._meta.postgen_declarations postgen_attributes = {} for name, decl in sorted(postgen_declarations.items()): postgen_attributes[name] = decl.extract(name, attrs) @@ -626,8 +672,7 @@ def simple_generate_batch(cls, create, size, **kwargs): Factory = FactoryMetaClass('Factory', (BaseFactory,), { - 'ABSTRACT_FACTORY': True, - 'FACTORY_STRATEGY': CREATE_STRATEGY, + 'Meta': BaseMeta, '__doc__': """Factory base with build and create support. This class has the ability to support multiple ORMs by using custom creation @@ -642,8 +687,9 @@ def simple_generate_batch(cls, create, size, **kwargs): class StubFactory(Factory): - FACTORY_STRATEGY = STUB_STRATEGY - FACTORY_FOR = containers.StubObject + class Meta: + strategy = STUB_STRATEGY + target = containers.StubObject @classmethod def build(cls, **kwargs): @@ -656,13 +702,14 @@ def create(cls, **kwargs): class BaseDictFactory(Factory): """Factory for dictionary-like classes.""" - ABSTRACT_FACTORY = True + class Meta: + abstract = True @classmethod def _build(cls, target_class, *args, **kwargs): if args: raise ValueError( - "DictFactory %r does not support FACTORY_ARG_PARAMETERS.", cls) + "DictFactory %r does not support Meta.arg_parameters.", cls) return target_class(**kwargs) @classmethod @@ -671,18 +718,20 @@ def _create(cls, target_class, *args, **kwargs): class DictFactory(BaseDictFactory): - FACTORY_FOR = dict + class Meta: + target = dict class BaseListFactory(Factory): """Factory for list-like classes.""" - ABSTRACT_FACTORY = True + class Meta: + abstract = True @classmethod def _build(cls, target_class, *args, **kwargs): if args: raise ValueError( - "ListFactory %r does not support FACTORY_ARG_PARAMETERS.", cls) + "ListFactory %r does not support Meta.arg_parameters.", cls) values = [v for k, v in sorted(kwargs.items())] return target_class(values) @@ -693,7 +742,8 @@ def _create(cls, target_class, *args, **kwargs): class ListFactory(BaseListFactory): - FACTORY_FOR = list + class Meta: + target = list def use_strategy(new_strategy): @@ -702,6 +752,6 @@ def use_strategy(new_strategy): This is an alternative to setting default_strategy in the class definition. """ def wrapped_class(klass): - klass.FACTORY_STRATEGY = new_strategy + klass._meta.strategy = new_strategy return klass return wrapped_class diff --git a/factory/django.py b/factory/django.py index a3dfdfcd..9a4e07aa 100644 --- a/factory/django.py +++ b/factory/django.py @@ -52,6 +52,13 @@ def require_django(): raise import_failure +class DjangoOptions(base.FactoryOptions): + def _build_default_options(self): + return super(DjangoOptions, self)._build_default_options() + [ + base.OptionDefault('django_get_or_create', (), inherit=True), + ] + + class DjangoModelFactory(base.Factory): """Factory for Django models. @@ -61,8 +68,18 @@ class DjangoModelFactory(base.Factory): handle those for non-numerical primary keys. """ - ABSTRACT_FACTORY = True # Optional, but explicit. - FACTORY_DJANGO_GET_OR_CREATE = () + _options_class = DjangoOptions + class Meta: + abstract = True # Optional, but explicit. + + @classmethod + def _get_blank_options(cls): + return DjangoOptions() + + _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() + _OLDSTYLE_ATTRIBUTES.update({ + 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create', + }) @classmethod def _load_target_class(cls, definition): @@ -101,13 +118,13 @@ 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, ( + assert 'defaults' not in cls._meta.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)) + "(in %s._meta.django_get_or_create=%r)" + % (cls, cls._meta.django_get_or_create)) key_fields = {} - for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + for field in cls._meta.django_get_or_create: key_fields[field] = kwargs.pop(field) key_fields['defaults'] = kwargs @@ -119,7 +136,7 @@ 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: + if cls._meta.django_get_or_create: return cls._get_or_create(target_class, *args, **kwargs) return manager.create(*args, **kwargs) diff --git a/factory/helpers.py b/factory/helpers.py index 4a2a2548..0c387d04 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -50,7 +50,9 @@ def debug(logger='factory', stream=None): def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ - kwargs[base.FACTORY_CLASS_DECLARATION] = klass + class Meta: + target = klass + kwargs['Meta'] = Meta base_class = kwargs.pop('FACTORY_CLASS', base.Factory) factory_class = type(base.Factory).__new__( diff --git a/setup.py b/setup.py index 54e4caa1..f637a481 100755 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ def get_version(package_name): 'setuptools>=0.8', ], tests_require=[ - 'mock', + #'mock', ], classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/tests/alter_time.py b/tests/alter_time.py index db0a6111..aa2db3b9 100644 --- a/tests/alter_time.py +++ b/tests/alter_time.py @@ -7,7 +7,7 @@ from __future__ import print_function import datetime -import mock +from .compat import mock real_datetime_class = datetime.datetime diff --git a/tests/test_base.py b/tests/test_base.py index ba691641..2edf829d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -107,7 +107,7 @@ def test_factory_for(self): class TestObjectFactory(base.Factory): FACTORY_FOR = TestObject - self.assertEqual(TestObject, TestObjectFactory.FACTORY_FOR) + self.assertEqual(TestObject, TestObjectFactory._meta.target) obj = TestObjectFactory.build() self.assertFalse(hasattr(obj, 'FACTORY_FOR')) @@ -226,13 +226,13 @@ class SubTestObjectFactory(self.TestObjectFactory): class FactoryDefaultStrategyTestCase(unittest.TestCase): def setUp(self): - self.default_strategy = base.Factory.FACTORY_STRATEGY + self.default_strategy = base.Factory._meta.strategy def tearDown(self): - base.Factory.FACTORY_STRATEGY = self.default_strategy + base.Factory._meta.strategy = self.default_strategy def test_build_strategy(self): - base.Factory.FACTORY_STRATEGY = base.BUILD_STRATEGY + base.Factory._meta.strategy = base.BUILD_STRATEGY class TestModelFactory(base.Factory): FACTORY_FOR = TestModel @@ -256,7 +256,7 @@ class TestModelFactory(FakeModelFactory): self.assertTrue(test_model.id) def test_stub_strategy(self): - base.Factory.FACTORY_STRATEGY = base.STUB_STRATEGY + base.Factory._meta.strategy = base.STUB_STRATEGY class TestModelFactory(base.Factory): FACTORY_FOR = TestModel @@ -268,7 +268,7 @@ class TestModelFactory(base.Factory): self.assertFalse(hasattr(test_model, 'id')) # We should have a plain old object def test_unknown_strategy(self): - base.Factory.FACTORY_STRATEGY = 'unknown' + base.Factory._meta.strategy = 'unknown' class TestModelFactory(base.Factory): FACTORY_FOR = TestModel @@ -283,11 +283,11 @@ class TestModelFactory(base.StubFactory): one = 'one' - TestModelFactory.FACTORY_STRATEGY = base.CREATE_STRATEGY + TestModelFactory._meta.strategy = base.CREATE_STRATEGY self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) - TestModelFactory.FACTORY_STRATEGY = base.BUILD_STRATEGY + TestModelFactory._meta.strategy = base.BUILD_STRATEGY self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) def test_change_strategy(self): @@ -297,7 +297,7 @@ class TestModelFactory(base.StubFactory): one = 'one' - self.assertEqual(base.CREATE_STRATEGY, TestModelFactory.FACTORY_STRATEGY) + self.assertEqual(base.CREATE_STRATEGY, TestModelFactory._meta.strategy) class FactoryCreationTestCase(unittest.TestCase): @@ -311,7 +311,7 @@ def test_stub(self): class TestFactory(base.StubFactory): pass - self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY) + self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY) def test_inheritance_with_stub(self): class TestObjectFactory(base.StubFactory): @@ -322,7 +322,7 @@ class TestObjectFactory(base.StubFactory): class TestFactory(TestObjectFactory): pass - self.assertEqual(TestFactory.FACTORY_STRATEGY, base.STUB_STRATEGY) + self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY) def test_custom_creation(self): class TestModelFactory(FakeModelFactory): @@ -349,7 +349,7 @@ def test_no_associated_class(self): class Test(base.Factory): pass - self.assertTrue(Test._abstract_factory) + self.assertTrue(Test._meta.abstract) class PostGenerationParsingTestCase(unittest.TestCase): @@ -360,7 +360,7 @@ class TestObjectFactory(base.Factory): foo = declarations.PostGenerationDeclaration() - self.assertIn('foo', TestObjectFactory._postgen_declarations) + self.assertIn('foo', TestObjectFactory._meta.postgen_declarations) def test_classlevel_extraction(self): class TestObjectFactory(base.Factory): @@ -369,8 +369,8 @@ class TestObjectFactory(base.Factory): foo = declarations.PostGenerationDeclaration() foo__bar = 42 - self.assertIn('foo', TestObjectFactory._postgen_declarations) - self.assertIn('foo__bar', TestObjectFactory._declarations) + self.assertIn('foo', TestObjectFactory._meta.postgen_declarations) + self.assertIn('foo__bar', TestObjectFactory._meta.declarations) From 80eaa0c8711f2c3ca82eb7953db49c7c61bd9ffa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 12:16:51 +0200 Subject: [PATCH 033/714] factory.django: Fix counter inheritance with abstract models. --- factory/base.py | 19 ++++++++++++------- factory/django.py | 16 ++++++++++++---- tests/djapp/models.py | 11 ++++++++++- tests/test_django.py | 17 +++++++++++++++-- 4 files changed, 49 insertions(+), 14 deletions(-) diff --git a/factory/base.py b/factory/base.py index cc1fb57d..d255dbff 100644 --- a/factory/base.py +++ b/factory/base.py @@ -218,13 +218,7 @@ def contribute_to_class(self, factory, if self.target is None: self.abstract = True - if (self.target is not None - and self.base_factory is not None - and self.base_factory._meta.target is not None - and issubclass(self.target, base_factory._meta.target)): - self.counter_reference = self.base_factory - else: - self.counter_reference = self.factory + self.counter_reference = self._get_counter_reference() for parent in self.factory.__mro__[1:]: if not hasattr(parent, '_meta'): @@ -238,6 +232,17 @@ def contribute_to_class(self, factory, if self._is_postgen_declaration(k, v): self.postgen_declarations[k] = v + def _get_counter_reference(self): + """Identify which factory should be used for a shared counter.""" + + if (self.target is not None + and self.base_factory is not None + and self.base_factory._meta.target is not None + and issubclass(self.target, self.base_factory._meta.target)): + return self.base_factory + else: + return self.factory + def _is_declaration(self, name, value): """Determines if a class attribute is a field value declaration. diff --git a/factory/django.py b/factory/django.py index 9a4e07aa..77afd8cc 100644 --- a/factory/django.py +++ b/factory/django.py @@ -58,6 +58,18 @@ def _build_default_options(self): base.OptionDefault('django_get_or_create', (), inherit=True), ] + def _get_counter_reference(self): + counter_reference = super(DjangoOptions, self)._get_counter_reference() + if (counter_reference == self.base_factory + and self.base_factory._meta.target is not None + and self.base_factory._meta.target._meta.abstract + and self.target is not None + and not self.target._meta.abstract): + # Target factory is for an abstract model, yet we're for another, + # concrete subclass => don't reuse the counter. + return self.factory + return counter_reference + class DjangoModelFactory(base.Factory): """Factory for Django models. @@ -72,10 +84,6 @@ class DjangoModelFactory(base.Factory): class Meta: abstract = True # Optional, but explicit. - @classmethod - def _get_blank_options(cls): - return DjangoOptions() - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() _OLDSTYLE_ATTRIBUTES.update({ 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create', diff --git a/tests/djapp/models.py b/tests/djapp/models.py index a65b50a7..9b211814 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -55,6 +55,15 @@ class ConcreteSon(AbstractBase): pass +class AbstractSon(AbstractBase): + class Meta: + abstract = True + + +class ConcreteGrandSon(AbstractSon): + pass + + class StandardSon(StandardModel): pass @@ -77,4 +86,4 @@ class WithImage(models.Model): class WithSignals(models.Model): - foo = models.CharField(max_length=20) \ No newline at end of file + foo = models.CharField(max_length=20) diff --git a/tests/test_django.py b/tests/test_django.py index 29453e62..37bf7a55 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -130,6 +130,14 @@ class ConcreteSonFactory(AbstractBaseFactory): FACTORY_FOR = models.ConcreteSon +class AbstractSonFactory(AbstractBaseFactory): + FACTORY_FOR = models.AbstractSon + + +class ConcreteGrandSonFactory(AbstractBaseFactory): + FACTORY_FOR = models.ConcreteGrandSon + + class WithFileFactory(factory.django.DjangoModelFactory): FACTORY_FOR = models.WithFile @@ -307,8 +315,13 @@ def test_force_pk(self): @unittest.skipIf(django is None, "Django not installed.") class DjangoAbstractBaseSequenceTestCase(django_test.TestCase): def test_auto_sequence(self): - with factory.debug(): - obj = ConcreteSonFactory() + """The sequence of the concrete son of an abstract model should be autonomous.""" + obj = ConcreteSonFactory() + self.assertEqual(1, obj.pk) + + def test_auto_sequence(self): + """The sequence of the concrete grandson of an abstract model should be autonomous.""" + obj = ConcreteGrandSonFactory() self.assertEqual(1, obj.pk) From 92bb395e7f6d422ce239b2ef7303424fde43ab1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 12:17:19 +0200 Subject: [PATCH 034/714] Migrate factory.alchemy to class Meta --- factory/alchemy.py | 23 ++++++++++++++++++----- tests/test_alchemy.py | 4 ++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index cec15c92..b956d7e7 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -24,17 +24,30 @@ from . import base +class SQLAlchemyOptions(base.FactoryOptions): + def _build_default_options(self): + return super(SQLAlchemyOptions, self)._build_default_options() + [ + base.OptionDefault('sqlalchemy_session', None, inherit=True), + ] + + class SQLAlchemyModelFactory(base.Factory): """Factory for SQLAlchemy models. """ - ABSTRACT_FACTORY = True - FACTORY_SESSION = None + _options_class = SQLAlchemyOptions + class Meta: + abstract = True + + _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() + _OLDSTYLE_ATTRIBUTES.update({ + 'FACTORY_SESSION': 'sqlalchemy_session', + }) @classmethod def _setup_next_sequence(cls, *args, **kwargs): """Compute the next available PK, based on the 'pk' database field.""" - session = cls.FACTORY_SESSION - model = cls.FACTORY_FOR + session = cls._meta.sqlalchemy_session + model = cls._meta.target pk = getattr(model, model.__mapper__.primary_key[0].name) max_pk = session.query(max(pk)).one()[0] if isinstance(max_pk, int): @@ -45,7 +58,7 @@ def _setup_next_sequence(cls, *args, **kwargs): @classmethod def _create(cls, target_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" - session = cls.FACTORY_SESSION + session = cls._meta.sqlalchemy_session obj = target_class(*args, **kwargs) session.add(obj) return obj diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 4255417a..c94e4255 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -66,7 +66,7 @@ class SQLAlchemyPkSequenceTestCase(unittest.TestCase): def setUp(self): super(SQLAlchemyPkSequenceTestCase, self).setUp() StandardFactory.reset_sequence(1) - NonIntegerPkFactory.FACTORY_SESSION.rollback() + NonIntegerPkFactory._meta.sqlalchemy_session.rollback() def test_pk_first(self): std = StandardFactory.build() @@ -104,7 +104,7 @@ class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): super(SQLAlchemyNonIntegerPkTestCase, self).setUp() NonIntegerPkFactory.reset_sequence() - NonIntegerPkFactory.FACTORY_SESSION.rollback() + NonIntegerPkFactory._meta.sqlalchemy_session.rollback() def test_first(self): nonint = NonIntegerPkFactory.build() From 47b34e933f94e71e7aabd81cd1e065a807a55276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 12:26:08 +0200 Subject: [PATCH 035/714] Add tests for class Meta --- tests/test_base.py | 123 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 2edf829d..be36363d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -67,18 +67,21 @@ def test_base_factory(self): class AbstractFactoryTestCase(unittest.TestCase): def test_factory_for_optional(self): - """Ensure that FACTORY_FOR is optional for ABSTRACT_FACTORY.""" + """Ensure that target= is optional for abstract=True.""" class TestObjectFactory(base.Factory): - ABSTRACT_FACTORY = True + class Meta: + abstract = True - # Passed + self.assertTrue(TestObjectFactory._meta.abstract) + self.assertIsNone(TestObjectFactory._meta.target) def test_factory_for_and_abstract_factory_optional(self): - """Ensure that ABSTRACT_FACTORY is optional.""" + """Ensure that Meta.abstract is optional.""" class TestObjectFactory(base.Factory): pass - # passed + self.assertTrue(TestObjectFactory._meta.abstract) + self.assertIsNone(TestObjectFactory._meta.target) def test_abstract_factory_cannot_be_called(self): class TestObjectFactory(base.Factory): @@ -87,6 +90,116 @@ class TestObjectFactory(base.Factory): self.assertRaises(base.FactoryError, TestObjectFactory.build) self.assertRaises(base.FactoryError, TestObjectFactory.create) + def test_abstract_factory_not_inherited(self): + """abstract=True isn't propagated to child classes.""" + + class TestObjectFactory(base.Factory): + class Meta: + abstract = True + target = TestObject + + class TestObjectChildFactory(TestObjectFactory): + pass + + self.assertFalse(TestObjectChildFactory._meta.abstract) + + def test_abstract_or_target_is_required(self): + class TestObjectFactory(base.Factory): + class Meta: + abstract = False + target = None + + self.assertRaises(base.FactoryError, TestObjectFactory.build) + self.assertRaises(base.FactoryError, TestObjectFactory.create) + + +class OptionsTests(unittest.TestCase): + def test_base_attrs(self): + class AbstractFactory(base.Factory): + pass + + # Declarative attributes + self.assertTrue(AbstractFactory._meta.abstract) + self.assertIsNone(AbstractFactory._meta.target) + self.assertEqual((), AbstractFactory._meta.arg_parameters) + self.assertEqual((), AbstractFactory._meta.hidden_args) + self.assertEqual(base.CREATE_STRATEGY, AbstractFactory._meta.strategy) + + # Non-declarative attributes + self.assertEqual({}, AbstractFactory._meta.declarations) + self.assertEqual({}, AbstractFactory._meta.postgen_declarations) + self.assertEqual(AbstractFactory, AbstractFactory._meta.factory) + self.assertEqual(base.Factory, AbstractFactory._meta.base_factory) + self.assertEqual(AbstractFactory, AbstractFactory._meta.counter_reference) + + def test_declaration_collecting(self): + lazy = declarations.LazyAttribute(lambda _o: 1) + postgen = declarations.PostGenerationDeclaration() + + class AbstractFactory(base.Factory): + x = 1 + y = lazy + z = postgen + + # Declarations aren't removed + self.assertEqual(1, AbstractFactory.x) + self.assertEqual(lazy, AbstractFactory.y) + self.assertEqual(postgen, AbstractFactory.z) + + # And are available in class Meta + self.assertEqual({'x': 1, 'y': lazy}, AbstractFactory._meta.declarations) + self.assertEqual({'z': postgen}, AbstractFactory._meta.postgen_declarations) + + def test_inherited_declaration_collecting(self): + lazy = declarations.LazyAttribute(lambda _o: 1) + lazy2 = declarations.LazyAttribute(lambda _o: 2) + postgen = declarations.PostGenerationDeclaration() + postgen2 = declarations.PostGenerationDeclaration() + + class AbstractFactory(base.Factory): + x = 1 + y = lazy + z = postgen + + class OtherFactory(AbstractFactory): + a = lazy2 + b = postgen2 + + # Declarations aren't removed + self.assertEqual(lazy2, OtherFactory.a) + self.assertEqual(postgen2, OtherFactory.b) + self.assertEqual(1, OtherFactory.x) + self.assertEqual(lazy, OtherFactory.y) + self.assertEqual(postgen, OtherFactory.z) + + # And are available in class Meta + self.assertEqual({'x': 1, 'y': lazy, 'a': lazy2}, OtherFactory._meta.declarations) + self.assertEqual({'z': postgen, 'b': postgen2}, OtherFactory._meta.postgen_declarations) + + def test_inherited_declaration_shadowing(self): + lazy = declarations.LazyAttribute(lambda _o: 1) + lazy2 = declarations.LazyAttribute(lambda _o: 2) + postgen = declarations.PostGenerationDeclaration() + postgen2 = declarations.PostGenerationDeclaration() + + class AbstractFactory(base.Factory): + x = 1 + y = lazy + z = postgen + + class OtherFactory(AbstractFactory): + y = lazy2 + z = postgen2 + + # Declarations aren't removed + self.assertEqual(1, OtherFactory.x) + self.assertEqual(lazy2, OtherFactory.y) + self.assertEqual(postgen2, OtherFactory.z) + + # And are available in class Meta + self.assertEqual({'x': 1, 'y': lazy2}, OtherFactory._meta.declarations) + self.assertEqual({'z': postgen2}, OtherFactory._meta.postgen_declarations) + class DeclarationParsingTests(unittest.TestCase): def test_classmethod(self): From 395744736691b3412dbedaffa8735e248a70c3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 12:34:19 +0200 Subject: [PATCH 036/714] Switch tests to class Meta. --- factory/base.py | 4 +- factory/mogo.py | 3 +- factory/mongoengine.py | 4 +- tests/cyclic/bar.py | 3 +- tests/cyclic/foo.py | 3 +- tests/test_alchemy.py | 13 +- tests/test_base.py | 64 ++++--- tests/test_django.py | 58 ++++-- tests/test_mongoengine.py | 6 +- tests/test_using.py | 380 +++++++++++++++++++++++++------------- 10 files changed, 353 insertions(+), 185 deletions(-) diff --git a/factory/base.py b/factory/base.py index d255dbff..7db0c762 100644 --- a/factory/base.py +++ b/factory/base.py @@ -40,7 +40,7 @@ class FactoryError(Exception): class AssociatedClassError(FactoryError): - """Exception for Factory subclasses lacking FACTORY_FOR.""" + """Exception for Factory subclasses lacking Meta.target.""" class UnknownStrategy(FactoryError): @@ -82,7 +82,7 @@ def __call__(cls, **kwargs): elif cls._meta.strategy == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format( + raise UnknownStrategy('Unknown Meta.strategy: {0}'.format( cls._meta.strategy)) def __new__(mcs, class_name, bases, attrs): diff --git a/factory/mogo.py b/factory/mogo.py index 48d9677d..0214119e 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -32,7 +32,8 @@ class MogoFactory(base.Factory): """Factory for mogo objects.""" - ABSTRACT_FACTORY = True + class Meta: + abstract = True @classmethod def _build(cls, target_class, *args, **kwargs): diff --git a/factory/mongoengine.py b/factory/mongoengine.py index 462f5f23..729ebb12 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -32,7 +32,9 @@ class MongoEngineFactory(base.Factory): """Factory for mongoengine objects.""" - ABSTRACT_FACTORY = True + + class Meta: + abstract = True @classmethod def _build(cls, target_class, *args, **kwargs): diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index fed06029..8674c4d9 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -30,7 +30,8 @@ def __init__(self, foo, y): class BarFactory(factory.Factory): - FACTORY_FOR = Bar + class Meta: + target = Bar y = 13 foo = factory.SubFactory('cyclic.foo.FooFactory') diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index e584ed19..5310b1e0 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -32,7 +32,8 @@ def __init__(self, bar, x): class FooFactory(factory.Factory): - FACTORY_FOR = Foo + class Meta: + target = Foo x = 42 bar = factory.SubFactory(bar_mod.BarFactory) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index c94e4255..4526ad1b 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -36,7 +36,8 @@ else: class Fake(object): - FACTORY_SESSION = None + class Meta: + sqlalchemy_session = None models = Fake() models.StandardModel = Fake() @@ -46,16 +47,18 @@ class Fake(object): class StandardFactory(SQLAlchemyModelFactory): - FACTORY_FOR = models.StandardModel - FACTORY_SESSION = models.session + class Meta: + target = models.StandardModel + sqlalchemy_session = models.session id = factory.Sequence(lambda n: n) foo = factory.Sequence(lambda n: 'foo%d' % n) class NonIntegerPkFactory(SQLAlchemyModelFactory): - FACTORY_FOR = models.NonIntegerPk - FACTORY_SESSION = models.session + class Meta: + target = models.NonIntegerPk + sqlalchemy_session = models.session id = factory.Sequence(lambda n: 'foo%d' % n) diff --git a/tests/test_base.py b/tests/test_base.py index be36363d..c44ebd53 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -49,7 +49,8 @@ def __init__(self, **kwargs): class FakeModelFactory(base.Factory): - ABSTRACT_FACTORY = True + class Meta: + abstract = True @classmethod def _create(cls, target_class, *args, **kwargs): @@ -204,7 +205,8 @@ class OtherFactory(AbstractFactory): class DeclarationParsingTests(unittest.TestCase): def test_classmethod(self): class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @classmethod def some_classmethod(cls): @@ -216,24 +218,28 @@ def some_classmethod(cls): class FactoryTestCase(unittest.TestCase): - def test_factory_for(self): + def test_magic_happens(self): + """Calling a FooFactory doesn't yield a FooFactory instance.""" class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject self.assertEqual(TestObject, TestObjectFactory._meta.target) obj = TestObjectFactory.build() - self.assertFalse(hasattr(obj, 'FACTORY_FOR')) + self.assertFalse(hasattr(obj, '_meta')) def test_display(self): class TestObjectFactory(base.Factory): - FACTORY_FOR = FakeDjangoModel + class Meta: + target = FakeDjangoModel self.assertIn('TestObjectFactory', str(TestObjectFactory)) self.assertIn('FakeDjangoModel', str(TestObjectFactory)) def test_lazy_attribute_non_existent_param(self): class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = declarations.LazyAttribute(lambda a: a.does_not_exist ) @@ -242,12 +248,14 @@ class TestObjectFactory(base.Factory): def test_inheritance_with_sequence(self): """Tests that sequence IDs are shared between parent and son.""" class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = declarations.Sequence(lambda a: a) class TestSubFactory(TestObjectFactory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject pass @@ -264,7 +272,8 @@ def setUp(self): super(FactorySequenceTestCase, self).setUp() class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = declarations.Sequence(lambda n: n) self.TestObjectFactory = TestObjectFactory @@ -348,7 +357,8 @@ def test_build_strategy(self): base.Factory._meta.strategy = base.BUILD_STRATEGY class TestModelFactory(base.Factory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -357,10 +367,11 @@ class TestModelFactory(base.Factory): self.assertFalse(test_model.id) def test_create_strategy(self): - # Default FACTORY_STRATEGY + # Default Meta.strategy class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -372,7 +383,8 @@ def test_stub_strategy(self): base.Factory._meta.strategy = base.STUB_STRATEGY class TestModelFactory(base.Factory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -384,7 +396,8 @@ def test_unknown_strategy(self): base.Factory._meta.strategy = 'unknown' class TestModelFactory(base.Factory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -392,7 +405,8 @@ class TestModelFactory(base.Factory): def test_stub_with_non_stub_strategy(self): class TestModelFactory(base.StubFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -406,7 +420,8 @@ class TestModelFactory(base.StubFactory): def test_change_strategy(self): @base.use_strategy(base.CREATE_STRATEGY) class TestModelFactory(base.StubFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -416,7 +431,8 @@ class TestModelFactory(base.StubFactory): class FactoryCreationTestCase(unittest.TestCase): def test_factory_for(self): class TestFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject self.assertTrue(isinstance(TestFactory.build(), TestObject)) @@ -428,7 +444,8 @@ class TestFactory(base.StubFactory): def test_inheritance_with_stub(self): class TestObjectFactory(base.StubFactory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject pass @@ -439,7 +456,8 @@ class TestFactory(TestObjectFactory): def test_custom_creation(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel @classmethod def _prepare(cls, create, **kwargs): @@ -469,7 +487,8 @@ class PostGenerationParsingTestCase(unittest.TestCase): def test_extraction(self): class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject foo = declarations.PostGenerationDeclaration() @@ -477,7 +496,8 @@ class TestObjectFactory(base.Factory): def test_classlevel_extraction(self): class TestObjectFactory(base.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject foo = declarations.PostGenerationDeclaration() foo__bar = 42 diff --git a/tests/test_django.py b/tests/test_django.py index 37bf7a55..2bc5fe28 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -99,61 +99,71 @@ def tearDownModule(): class StandardFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.StandardModel + class Meta: + target = models.StandardModel foo = factory.Sequence(lambda n: "foo%d" % n) class StandardFactoryWithPKField(factory.django.DjangoModelFactory): - FACTORY_FOR = models.StandardModel - FACTORY_DJANGO_GET_OR_CREATE = ('pk',) + class Meta: + target = models.StandardModel + django_get_or_create = ('pk',) foo = factory.Sequence(lambda n: "foo%d" % n) pk = None class NonIntegerPkFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.NonIntegerPk + class Meta: + target = models.NonIntegerPk foo = factory.Sequence(lambda n: "foo%d" % n) bar = '' class AbstractBaseFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.AbstractBase - ABSTRACT_FACTORY = True + class Meta: + target = models.AbstractBase + abstract = True foo = factory.Sequence(lambda n: "foo%d" % n) class ConcreteSonFactory(AbstractBaseFactory): - FACTORY_FOR = models.ConcreteSon + class Meta: + target = models.ConcreteSon class AbstractSonFactory(AbstractBaseFactory): - FACTORY_FOR = models.AbstractSon + class Meta: + target = models.AbstractSon class ConcreteGrandSonFactory(AbstractBaseFactory): - FACTORY_FOR = models.ConcreteGrandSon + class Meta: + target = models.ConcreteGrandSon class WithFileFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.WithFile + class Meta: + target = models.WithFile if django is not None: afile = factory.django.FileField() class WithImageFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.WithImage + class Meta: + target = models.WithImage if django is not None: animage = factory.django.ImageField() class WithSignalsFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.WithSignals + class Meta: + target = models.WithSignals @unittest.skipIf(django is None, "Django not installed.") @@ -220,17 +230,20 @@ def test_reuse_pk(self): @unittest.skipIf(django is None, "Django not installed.") class DjangoModelLoadingTestCase(django_test.TestCase): - """Tests FACTORY_FOR = 'app.Model' pattern.""" + """Tests class Meta: + target = 'app.Model' pattern.""" def test_loading(self): class ExampleFactory(factory.DjangoModelFactory): - FACTORY_FOR = 'djapp.StandardModel' + class Meta: + target = 'djapp.StandardModel' self.assertEqual(models.StandardModel, ExampleFactory._get_target_class()) def test_building(self): class ExampleFactory(factory.DjangoModelFactory): - FACTORY_FOR = 'djapp.StandardModel' + class Meta: + target = 'djapp.StandardModel' e = ExampleFactory.build() self.assertEqual(models.StandardModel, e.__class__) @@ -241,7 +254,8 @@ def test_inherited_loading(self): See https://github.com/rbarrois/factory_boy/issues/109. """ class ExampleFactory(factory.DjangoModelFactory): - FACTORY_FOR = 'djapp.StandardModel' + class Meta: + target = 'djapp.StandardModel' class Example2Factory(ExampleFactory): pass @@ -255,12 +269,14 @@ def test_inherited_loading_and_sequence(self): See https://github.com/rbarrois/factory_boy/issues/109. """ class ExampleFactory(factory.DjangoModelFactory): - FACTORY_FOR = 'djapp.StandardModel' + class Meta: + target = 'djapp.StandardModel' foo = factory.Sequence(lambda n: n) class Example2Factory(ExampleFactory): - FACTORY_FOR = 'djapp.StandardSon' + class Meta: + target = 'djapp.StandardSon' self.assertEqual(models.StandardSon, Example2Factory._get_target_class()) @@ -563,7 +579,8 @@ def test_context_manager(self): def test_class_decorator(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.WithSignals + class Meta: + target = models.WithSignals WithSignalsDecoratedFactory() @@ -576,7 +593,8 @@ class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): def test_class_decorator_build(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.WithSignals + class Meta: + target = models.WithSignals WithSignalsDecoratedFactory.build() diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 803607ac..39594c01 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -42,12 +42,14 @@ class Person(mongoengine.Document): address = mongoengine.EmbeddedDocumentField(Address) class AddressFactory(MongoEngineFactory): - FACTORY_FOR = Address + class Meta: + target = Address street = factory.Sequence(lambda n: 'street%d' % n) class PersonFactory(MongoEngineFactory): - FACTORY_FOR = Person + class Meta: + target = Person name = factory.Sequence(lambda n: 'name%d' % n) address = factory.SubFactory(AddressFactory) diff --git a/tests/test_using.py b/tests/test_using.py index 3979cd0f..6e7ed64a 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -78,7 +78,8 @@ def __init__(self, **kwargs): class FakeModelFactory(factory.Factory): - ABSTRACT_FACTORY = True + class Meta: + abstract = True @classmethod def _create(cls, target_class, *args, **kwargs): @@ -292,7 +293,8 @@ def test_make_factory(self): class UsingFactoryTestCase(unittest.TestCase): def test_attribute(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'one' @@ -302,7 +304,8 @@ class TestObjectFactory(factory.Factory): def test_inheriting_target_class(self): @factory.use_strategy(factory.BUILD_STRATEGY) class TestObjectFactory(factory.Factory, TestObject): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'one' @@ -311,18 +314,22 @@ class TestObjectFactory(factory.Factory, TestObject): def test_abstract(self): class SomeAbstractFactory(factory.Factory): - ABSTRACT_FACTORY = True + class Meta: + abstract = True + one = 'one' class InheritedFactory(SomeAbstractFactory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject test_object = InheritedFactory.build() self.assertEqual(test_object.one, 'one') def test_sequence(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: 'one%d' % n) two = factory.Sequence(lambda n: 'two%d' % n) @@ -337,7 +344,8 @@ class TestObjectFactory(factory.Factory): def test_sequence_custom_begin(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @classmethod def _setup_next_sequence(cls): @@ -356,7 +364,8 @@ def _setup_next_sequence(cls): def test_sequence_override(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: 'one%d' % n) @@ -372,7 +381,8 @@ class TestObjectFactory(factory.Factory): def test_custom_create(self): class TestModelFactory(factory.Factory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel two = 2 @@ -395,7 +405,8 @@ def __init__(self, x, y=2): self.y = y class NonDjangoFactory(factory.Factory): - FACTORY_FOR = NonDjango + class Meta: + target = NonDjango x = 3 @@ -405,7 +416,8 @@ class NonDjangoFactory(factory.Factory): def test_sequence_batch(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: 'one%d' % n) two = factory.Sequence(lambda n: 'two%d' % n) @@ -420,7 +432,8 @@ class TestObjectFactory(factory.Factory): def test_lazy_attribute(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.LazyAttribute(lambda a: 'abc' ) two = factory.LazyAttribute(lambda a: a.one + ' xyz') @@ -431,7 +444,8 @@ class TestObjectFactory(factory.Factory): def test_lazy_attribute_sequence(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.LazyAttributeSequence(lambda a, n: 'abc%d' % n) two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz%d' % n) @@ -446,7 +460,8 @@ class TestObjectFactory(factory.Factory): def test_lazy_attribute_decorator(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @factory.lazy_attribute def one(a): @@ -460,7 +475,8 @@ class TmpObj(object): n = 3 class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'xx' two = factory.SelfAttribute('one') @@ -479,12 +495,14 @@ class TestModel2(FakeModel): pass class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 3 three = factory.SelfAttribute('..bar') class TestModel2Factory(FakeModelFactory): - FACTORY_FOR = TestModel2 + class Meta: + target = TestModel2 bar = 4 two = factory.SubFactory(TestModelFactory, one=1) @@ -493,7 +511,8 @@ class TestModel2Factory(FakeModelFactory): def test_sequence_decorator(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @factory.sequence def one(n): @@ -504,7 +523,8 @@ def one(n): def test_lazy_attribute_sequence_decorator(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @factory.lazy_attribute_sequence def one(a, n): @@ -519,7 +539,8 @@ def two(a, n): def test_build_with_parameters(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: 'one%d' % n) two = factory.Sequence(lambda n: 'two%d' % n) @@ -535,7 +556,8 @@ class TestObjectFactory(factory.Factory): def test_create(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -545,7 +567,8 @@ class TestModelFactory(FakeModelFactory): def test_create_batch(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -561,7 +584,8 @@ class TestModelFactory(FakeModelFactory): def test_generate_build(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -571,7 +595,8 @@ class TestModelFactory(FakeModelFactory): def test_generate_create(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -581,7 +606,8 @@ class TestModelFactory(FakeModelFactory): def test_generate_stub(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -591,7 +617,8 @@ class TestModelFactory(FakeModelFactory): def test_generate_batch_build(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -607,7 +634,8 @@ class TestModelFactory(FakeModelFactory): def test_generate_batch_create(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -623,7 +651,8 @@ class TestModelFactory(FakeModelFactory): def test_generate_batch_stub(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -639,7 +668,8 @@ class TestModelFactory(FakeModelFactory): def test_simple_generate_build(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -649,7 +679,8 @@ class TestModelFactory(FakeModelFactory): def test_simple_generate_create(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -659,7 +690,8 @@ class TestModelFactory(FakeModelFactory): def test_simple_generate_batch_build(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -675,7 +707,8 @@ class TestModelFactory(FakeModelFactory): def test_simple_generate_batch_create(self): class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 'one' @@ -691,7 +724,8 @@ class TestModelFactory(FakeModelFactory): def test_stub_batch(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'one' two = factory.LazyAttribute(lambda a: a.one + ' two') @@ -710,13 +744,15 @@ class TestObjectFactory(factory.Factory): def test_inheritance(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'one' two = factory.LazyAttribute(lambda a: a.one + ' two') class TestObjectFactory2(TestObjectFactory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject three = 'three' four = factory.LazyAttribute(lambda a: a.three + ' four') @@ -733,12 +769,14 @@ class TestObjectFactory2(TestObjectFactory): def test_inheritance_and_sequences(self): """Sequence counters should be kept within an inheritance chain.""" class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: n) class TestObjectFactory2(TestObjectFactory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject to1a = TestObjectFactory() self.assertEqual(0, to1a.one) @@ -755,12 +793,14 @@ class TestObject2(TestObject): pass class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: n) class TestObjectFactory2(TestObjectFactory): - FACTORY_FOR = TestObject2 + class Meta: + target = TestObject2 to1a = TestObjectFactory() self.assertEqual(0, to1a.one) @@ -785,12 +825,14 @@ def __init__(self, one): self.one = one class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: n) class TestObjectFactory2(TestObjectFactory): - FACTORY_FOR = TestObject2 + class Meta: + target = TestObject2 to1a = TestObjectFactory() self.assertEqual(0, to1a.one) @@ -804,7 +846,8 @@ class TestObjectFactory2(TestObjectFactory): def test_inheritance_with_inherited_class(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'one' two = factory.LazyAttribute(lambda a: a.one + ' two') @@ -821,12 +864,14 @@ class TestFactory(TestObjectFactory): def test_dual_inheritance(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 'one' class TestOtherFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject two = 'two' four = 'four' @@ -841,7 +886,8 @@ class TestFactory(TestObjectFactory, TestOtherFactory): def test_class_method_accessible(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @classmethod def alt_create(cls, **kwargs): @@ -851,7 +897,8 @@ def alt_create(cls, **kwargs): def test_static_method_accessible(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @staticmethod def alt_create(**kwargs): @@ -866,8 +913,9 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - FACTORY_ARG_PARAMETERS = ('x', 'y') + class Meta: + target = TestObject + arg_parameters = ('x', 'y') x = 1 y = 2 @@ -885,8 +933,9 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - FACTORY_HIDDEN_ARGS = ('x', 'z') + class Meta: + target = TestObject + hidden_args = ('x', 'z') x = 1 y = 2 @@ -904,9 +953,10 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - FACTORY_HIDDEN_ARGS = ('x', 'z') - FACTORY_ARG_PARAMETERS = ('y',) + class Meta: + target = TestObject + hidden_args = ('x', 'z') + arg_parameters = ('y',) x = 1 y = 2 @@ -927,8 +977,9 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - FACTORY_ARG_PARAMETERS = ('one', 'two',) + class Meta: + target = TestObject + arg_parameters = ('one', 'two',) one = 1 two = 2 @@ -952,8 +1003,9 @@ def create(cls, *args, **kwargs): return inst class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject - FACTORY_ARG_PARAMETERS = ('one', 'two') + class Meta: + target = TestObject + arg_parameters = ('one', 'two') one = 1 two = 2 @@ -978,7 +1030,8 @@ def __init__(self, *args, **kwargs): self.kwargs = kwargs class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @classmethod def _adjust_kwargs(cls, **kwargs): @@ -996,11 +1049,13 @@ class TestModel2(FakeModel): pass class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 3 class TestModel2Factory(FakeModelFactory): - FACTORY_FOR = TestModel2 + class Meta: + target = TestModel2 two = factory.SubFactory(TestModelFactory, one=1) test_model = TestModel2Factory(two__one=4) @@ -1013,10 +1068,12 @@ class TestModel2(FakeModel): pass class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel class TestModel2Factory(FakeModelFactory): - FACTORY_FOR = TestModel2 + class Meta: + target = TestModel2 two = factory.SubFactory(TestModelFactory, one=factory.Sequence(lambda n: 'x%dx' % n), two=factory.LazyAttribute(lambda o: '%s%s' % (o.one, o.one)), @@ -1033,12 +1090,14 @@ def __init__(self, **kwargs): setattr(self, k, v) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Sequence(lambda n: int(n)) class WrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrapped = factory.SubFactory(TestObjectFactory) @@ -1054,7 +1113,8 @@ def __init__(self, **kwargs): setattr(self, k, v) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject class OtherTestObject(object): @@ -1063,7 +1123,8 @@ def __init__(self, **kwargs): setattr(self, k, v) class WrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = OtherTestObject + class Meta: + target = OtherTestObject wrapped = factory.SubFactory(TestObjectFactory, two=2, four=4) wrapped__two = 4 @@ -1083,16 +1144,19 @@ def __init__(self, **kwargs): setattr(self, k, v) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject class WrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrapped = factory.SubFactory(TestObjectFactory) wrapped_bis = factory.SubFactory(TestObjectFactory, one=1) class OuterWrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrap = factory.SubFactory(WrappingTestObjectFactory, wrapped__two=2) @@ -1109,17 +1173,20 @@ def __init__(self, **kwargs): setattr(self, k, v) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject two = 'two' class WrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrapped = factory.SubFactory(TestObjectFactory) friend = factory.LazyAttribute(lambda o: o.wrapped.two.four + 1) class OuterWrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrap = factory.SubFactory(WrappingTestObjectFactory, wrapped__two=factory.SubFactory(TestObjectFactory, four=4)) @@ -1139,12 +1206,14 @@ def __init__(self, two='one', wrapped=None): # Innermost factory class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject two = 'two' # Intermediary factory class WrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrapped = factory.SubFactory(TestObjectFactory) wrapped__two = 'three' @@ -1162,11 +1231,13 @@ def __init__(self, **kwargs): setattr(self, k, v) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject two = 'two' class WrappingTestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject wrapped = factory.SubFactory(TestObjectFactory) friend = factory.LazyAttribute(lambda o: o.wrapped.two + 1) @@ -1200,20 +1271,24 @@ def __init__(self, foo, side_a, side_b): self.side_b = side_b class InnerMostFactory(factory.Factory): - FACTORY_FOR = InnerMost + class Meta: + target = InnerMost a = 15 b = 20 class SideAFactory(factory.Factory): - FACTORY_FOR = SideA + class Meta: + target = SideA inner_from_a = factory.SubFactory(InnerMostFactory, a=20) class SideBFactory(factory.Factory): - FACTORY_FOR = SideB + class Meta: + target = SideB inner_from_b = factory.SubFactory(InnerMostFactory, b=15) class OuterMostFactory(factory.Factory): - FACTORY_FOR = OuterMost + class Meta: + target = OuterMost foo = 30 side_a = factory.SubFactory(SideAFactory, @@ -1238,12 +1313,14 @@ class TestModel2(FakeModel): pass class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 3 two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=False) class TestModel2Factory(FakeModelFactory): - FACTORY_FOR = TestModel2 + class Meta: + target = TestModel2 one = 1 two = factory.SubFactory(TestModelFactory, one=1) @@ -1261,12 +1338,14 @@ class TestModel2(FakeModel): pass class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 3 two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=True) class TestModel2Factory(FakeModelFactory): - FACTORY_FOR = TestModel2 + class Meta: + target = TestModel2 one = 1 two = factory.SubFactory(TestModelFactory, one=1) @@ -1282,7 +1361,8 @@ class TestModel2(FakeModel): pass class TestModelFactory(FakeModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel one = 3 @factory.container_attribute @@ -1292,7 +1372,8 @@ def two(self, containers): return 42 class TestModel2Factory(FakeModelFactory): - FACTORY_FOR = TestModel2 + class Meta: + target = TestModel2 one = 1 two = factory.SubFactory(TestModelFactory, one=1) @@ -1310,7 +1391,8 @@ class IteratorTestCase(unittest.TestCase): def test_iterator(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Iterator(range(10, 30)) @@ -1323,7 +1405,8 @@ class TestObjectFactory(factory.Factory): @tools.disable_warnings def test_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Iterator([j * 3 for j in range(5)]) @@ -1334,7 +1417,8 @@ class TestObjectFactory(factory.Factory): @tools.disable_warnings def test_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Iterator([_j * 3 for _j in range(5)]) @@ -1347,7 +1431,8 @@ class TestObjectFactory(factory.Factory): def test_iterator_decorator(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject @factory.iterator def one(): @@ -1397,7 +1482,8 @@ def __init__(self, **kwargs): class DjangoModelFactoryTestCase(unittest.TestCase): def test_simple(self): class FakeModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = FakeModel + class Meta: + target = FakeModel obj = FakeModelFactory(one=1) self.assertEqual(1, obj.one) @@ -1411,8 +1497,9 @@ class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1}, prev) class MyFakeModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = MyFakeModel - FACTORY_DJANGO_GET_OR_CREATE = ('x',) + class Meta: + target = MyFakeModel + django_get_or_create = ('x',) x = 1 y = 4 z = 6 @@ -1432,8 +1519,9 @@ class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev) class MyFakeModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = MyFakeModel - FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z') + class Meta: + target = MyFakeModel + django_get_or_create = ('x', 'y', 'z') x = 1 y = 4 z = 6 @@ -1453,8 +1541,9 @@ class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1}, prev) class MyFakeModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = MyFakeModel - FACTORY_DJANGO_GET_OR_CREATE = ('x',) + class Meta: + target = MyFakeModel + django_get_or_create = ('x',) x = 1 y = 4 z = 6 @@ -1474,8 +1563,9 @@ class MyFakeModel(BetterFakeModel): objects = BetterFakeModelManager({'x': 1, 'y': 2, 'z': 3}, prev) class MyFakeModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = MyFakeModel - FACTORY_DJANGO_GET_OR_CREATE = ('x', 'y', 'z') + class Meta: + target = MyFakeModel + django_get_or_create = ('x', 'y', 'z') x = 1 y = 4 z = 6 @@ -1489,7 +1579,8 @@ class MyFakeModelFactory(factory.django.DjangoModelFactory): def test_sequence(self): class TestModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1507,7 +1598,8 @@ class TestModelFactory(factory.django.DjangoModelFactory): def test_no_get_or_create(self): class TestModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = TestModel + class Meta: + target = TestModel a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1518,8 +1610,9 @@ class TestModelFactory(factory.django.DjangoModelFactory): def test_get_or_create(self): class TestModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = TestModel - FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b') + class Meta: + target = TestModel + django_get_or_create = ('a', 'b') a = factory.Sequence(lambda n: 'foo_%s' % n) b = 2 @@ -1537,8 +1630,9 @@ class TestModelFactory(factory.django.DjangoModelFactory): def test_full_get_or_create(self): """Test a DjangoModelFactory with all fields in get_or_create.""" class TestModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = TestModel - FACTORY_DJANGO_GET_OR_CREATE = ('a', 'b', 'c', 'd') + class Meta: + target = TestModel + django_get_or_create = ('a', 'b', 'c', 'd') a = factory.Sequence(lambda n: 'foo_%s' % n) b = 2 @@ -1557,7 +1651,8 @@ class TestModelFactory(factory.django.DjangoModelFactory): class PostGenerationTestCase(unittest.TestCase): def test_post_generation(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 1 @@ -1575,7 +1670,8 @@ def incr_one(self, _create, _increment): def test_post_generation_hook(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 1 @@ -1596,7 +1692,8 @@ def _after_postgeneration(cls, obj, create, results): def test_post_generation_extraction(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 1 @@ -1621,7 +1718,8 @@ def my_lambda(obj, create, extracted, **kwargs): self.assertEqual(kwargs, {'foo': 13}) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject bar = factory.PostGeneration(my_lambda) @@ -1640,7 +1738,8 @@ def call(self, *args, **kwargs): self.extra = (args, kwargs) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 3 two = 2 post_call = factory.PostGenerationMethodCall('call', one=1) @@ -1664,12 +1763,14 @@ def __init__(self, obj=None, one=None, two=None): self.three = obj class TestRelatedObjectFactory(factory.Factory): - FACTORY_FOR = TestRelatedObject + class Meta: + target = TestRelatedObject one = 1 two = factory.LazyAttribute(lambda o: o.one + 1) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 3 two = 2 three = factory.RelatedFactory(TestRelatedObjectFactory, name='obj') @@ -1709,12 +1810,14 @@ def __init__(self, obj=None, one=None, two=None): self.three = obj class TestRelatedObjectFactory(factory.Factory): - FACTORY_FOR = TestRelatedObject + class Meta: + target = TestRelatedObject one = 1 two = factory.LazyAttribute(lambda o: o.one + 1) class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 3 two = 2 three = factory.RelatedFactory(TestRelatedObjectFactory) @@ -1755,10 +1858,12 @@ def __init__(subself, obj): obj.related = subself class TestRelatedObjectFactory(factory.Factory): - FACTORY_FOR = TestRelatedObject + class Meta: + target = TestRelatedObject class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') self.TestRelatedObject = TestRelatedObject @@ -1805,7 +1910,8 @@ def test_example(self): class DictTestCase(unittest.TestCase): def test_empty_dict(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Dict({}) o = TestObjectFactory() @@ -1813,7 +1919,8 @@ class TestObjectFactory(factory.Factory): def test_naive_dict(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Dict({'a': 1}) o = TestObjectFactory() @@ -1821,7 +1928,8 @@ class TestObjectFactory(factory.Factory): def test_sequence_dict(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Dict({'a': factory.Sequence(lambda n: n + 2)}) o1 = TestObjectFactory() @@ -1832,7 +1940,8 @@ class TestObjectFactory(factory.Factory): def test_dict_override(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Dict({'a': 1}) o = TestObjectFactory(one__a=2) @@ -1840,7 +1949,8 @@ class TestObjectFactory(factory.Factory): def test_dict_extra_key(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.Dict({'a': 1}) o = TestObjectFactory(one__b=2) @@ -1848,7 +1958,8 @@ class TestObjectFactory(factory.Factory): def test_dict_merged_fields(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject two = 13 one = factory.Dict({ 'one': 1, @@ -1861,7 +1972,8 @@ class TestObjectFactory(factory.Factory): def test_nested_dicts(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = 1 two = factory.Dict({ 'one': 3, @@ -1889,7 +2001,8 @@ class TestObjectFactory(factory.Factory): class ListTestCase(unittest.TestCase): def test_empty_list(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.List([]) o = TestObjectFactory() @@ -1897,7 +2010,8 @@ class TestObjectFactory(factory.Factory): def test_naive_list(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.List([1]) o = TestObjectFactory() @@ -1905,7 +2019,8 @@ class TestObjectFactory(factory.Factory): def test_sequence_list(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.List([factory.Sequence(lambda n: n + 2)]) o1 = TestObjectFactory() @@ -1916,7 +2031,8 @@ class TestObjectFactory(factory.Factory): def test_list_override(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.List([1]) o = TestObjectFactory(one__0=2) @@ -1924,7 +2040,8 @@ class TestObjectFactory(factory.Factory): def test_list_extra_key(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject one = factory.List([1]) o = TestObjectFactory(one__1=2) @@ -1932,7 +2049,8 @@ class TestObjectFactory(factory.Factory): def test_list_merged_fields(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject two = 13 one = factory.List([ 1, @@ -1945,7 +2063,9 @@ class TestObjectFactory(factory.Factory): def test_nested_lists(self): class TestObjectFactory(factory.Factory): - FACTORY_FOR = TestObject + class Meta: + target = TestObject + one = 1 two = factory.List([ 3, From d26f41368e7c8936306cf1c34d73fff40d958128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 12:40:25 +0200 Subject: [PATCH 037/714] Remove containers.DeclarationsDict. Now replaced with a simple dict. --- factory/containers.py | 55 --------------------- tests/test_containers.py | 101 +-------------------------------------- 2 files changed, 1 insertion(+), 155 deletions(-) diff --git a/factory/containers.py b/factory/containers.py index 4537e441..c0c5e246 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -121,61 +121,6 @@ def __setattr__(self, name, value): raise AttributeError('Setting of object attributes is not allowed') -class DeclarationDict(dict): - """Slightly extended dict to work with OrderedDeclaration.""" - - def is_declaration(self, name, value): - """Determines if a class attribute is a field value declaration. - - Based on the name and value of the class attribute, return ``True`` if - it looks like a declaration of a default field value, ``False`` if it - is private (name starts with '_') or a classmethod or staticmethod. - - """ - if isinstance(value, (classmethod, staticmethod)): - return False - elif isinstance(value, declarations.OrderedDeclaration): - return True - return (not name.startswith("_") and not name.startswith("FACTORY_")) - - def update_with_public(self, d): - """Updates the DeclarationDict from a class definition dict. - - Takes into account all public attributes and OrderedDeclaration - instances; ignores all class/staticmethods and private attributes - (starting with '_'). - - Returns a dict containing all remaining elements. - """ - remaining = {} - for k, v in d.items(): - if self.is_declaration(k, v): - self[k] = v - else: - remaining[k] = v - return remaining - - def copy(self, extra=None): - """Copy this DeclarationDict into another one, including extra values. - - Args: - extra (dict): additional attributes to include in the copy. - """ - new = self.__class__() - new.update(self) - if extra: - new.update(extra) - return new - - -class PostGenerationDeclarationDict(DeclarationDict): - """Alternate DeclarationDict for PostGenerationDeclaration.""" - - def is_declaration(self, name, value): - """Captures instances of PostGenerationDeclaration.""" - return isinstance(value, declarations.PostGenerationDeclaration) - - class LazyValue(object): """Some kind of "lazy evaluating" object.""" diff --git a/tests/test_containers.py b/tests/test_containers.py index 8b78dc77..8a9e990a 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -100,105 +100,6 @@ class RandomObj(object): self.assertIn('one', str(stub)) -class OrderedDeclarationMock(declarations.OrderedDeclaration): - pass - - -class DeclarationDictTestCase(unittest.TestCase): - def test_basics(self): - one = OrderedDeclarationMock() - two = 2 - three = OrderedDeclarationMock() - - d = containers.DeclarationDict(dict(one=one, two=two, three=three)) - - self.assertTrue('one' in d) - self.assertTrue('two' in d) - self.assertTrue('three' in d) - - self.assertEqual(one, d['one']) - self.assertEqual(two, d['two']) - self.assertEqual(three, d['three']) - - self.assertEqual(one, d.pop('one')) - self.assertFalse('one' in d) - - d['one'] = one - self.assertTrue('one' in d) - self.assertEqual(one, d['one']) - - self.assertEqual(set(['one', 'two', 'three']), - set(d)) - - def test_insert(self): - one = OrderedDeclarationMock() - two = 2 - three = OrderedDeclarationMock() - four = OrderedDeclarationMock() - - d = containers.DeclarationDict(dict(one=one, two=two, four=four)) - - self.assertEqual(set(['two', 'one', 'four']), set(d)) - - d['three'] = three - self.assertEqual(set(['two', 'one', 'three', 'four']), set(d)) - - def test_replace(self): - one = OrderedDeclarationMock() - two = 2 - three = OrderedDeclarationMock() - four = OrderedDeclarationMock() - - d = containers.DeclarationDict(dict(one=one, two=two, three=three)) - - self.assertEqual(set(['two', 'one', 'three']), set(d)) - - d['three'] = four - self.assertEqual(set(['two', 'one', 'three']), set(d)) - self.assertEqual(set([two, one, four]), set(d.values())) - - def test_copy(self): - one = OrderedDeclarationMock() - two = 2 - three = OrderedDeclarationMock() - four = OrderedDeclarationMock() - - d = containers.DeclarationDict(dict(one=one, two=two, three=three)) - d2 = d.copy({'five': 5}) - - self.assertEqual(5, d2['five']) - self.assertFalse('five' in d) - - d.pop('one') - self.assertEqual(one, d2['one']) - - d2['two'] = four - self.assertEqual(four, d2['two']) - self.assertEqual(two, d['two']) - - def test_update_with_public(self): - d = containers.DeclarationDict() - d.update_with_public({ - 'one': 1, - '_two': 2, - 'three': 3, - 'classmethod': classmethod(lambda c: 1), - 'staticmethod': staticmethod(lambda: 1), - }) - self.assertEqual(set(['one', 'three']), set(d)) - self.assertEqual(set([1, 3]), set(d.values())) - - def test_update_with_public_ignores_factory_attributes(self): - """Ensure that a DeclarationDict ignores FACTORY_ keys.""" - d = containers.DeclarationDict() - d.update_with_public({ - 'one': 1, - 'FACTORY_FOR': 2, - 'FACTORY_ARG_PARAMETERS': 3, - }) - self.assertEqual(['one'], list(d)) - self.assertEqual([1], list(d.values())) - class AttributeBuilderTestCase(unittest.TestCase): def test_empty(self): @@ -320,7 +221,7 @@ def test_lazy_attribute(self): class FakeFactory(object): @classmethod def declarations(cls, extra): - d = containers.DeclarationDict({'one': 1, 'two': la}) + d = {'one': 1, 'two': la} d.update(extra) return d From c7b2ac71acd93b5afdf5cb4d958ffa1bbcd464e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 12:54:39 +0200 Subject: [PATCH 038/714] Add DeprecationWarning for FACTORY_* kwargs --- factory/base.py | 15 ++++++++++-- tests/__init__.py | 1 + tests/test_deprecation.py | 49 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 tests/test_deprecation.py diff --git a/factory/base.py b/factory/base.py index 7db0c762..862556e5 100644 --- a/factory/base.py +++ b/factory/base.py @@ -21,6 +21,7 @@ # THE SOFTWARE. import logging +import warnings from . import containers from . import declarations @@ -109,11 +110,21 @@ def __new__(mcs, class_name, bases, attrs): attrs_meta = attrs.pop('Meta', None) oldstyle_attrs = {} + converted_attrs = {} for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items(): if old_name in attrs: - oldstyle_attrs[new_name] = attrs.pop(old_name) + oldstyle_attrs[old_name] = new_name + converted_attrs[new_name] = attrs.pop(old_name) if oldstyle_attrs: - attrs_meta = type('Meta', (object,), oldstyle_attrs) + warnings.warn( + "Declaring any of %s at class-level is deprecated" + " and will be removed in the future. Please set them" + " as %s attributes of a 'class Meta' attribute." % ( + ', '.join(oldstyle_attrs.keys()), + ', '.join(oldstyle_attrs.values()), + ), + PendingDeprecationWarning, 2) + attrs_meta = type('Meta', (object,), converted_attrs) base_meta = resolve_attribute('_meta', bases) options_class = resolve_attribute('_options_class', bases, FactoryOptions) diff --git a/tests/__init__.py b/tests/__init__.py index 5b6fc55b..855beea4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,6 +4,7 @@ from .test_base import * from .test_containers import * from .test_declarations import * +from .test_deprecation import * from .test_django import * from .test_fuzzy import * from .test_helpers import * diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py new file mode 100644 index 00000000..bccc3517 --- /dev/null +++ b/tests/test_deprecation.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Tests for deprecated features.""" + +import warnings + +import factory + +from .compat import mock, unittest +from . import tools + + +class DeprecationTests(unittest.TestCase): + def test_factory_for(self): + class Foo(object): + pass + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + class FooFactory(factory.Factory): + FACTORY_FOR = Foo + + self.assertEqual(1, len(w)) + warning = w[0] + # Message is indeed related to the current file + # This is to ensure error messages are readable by end users. + self.assertEqual(__file__, warning.filename) + self.assertIn('FACTORY_FOR', str(warning.message)) + self.assertIn('target', str(warning.message)) From fd3d2583580fc18ff1531b5be02238c8c2addccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 14:20:26 +0200 Subject: [PATCH 039/714] Update ChangeLog --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 47d1139a..e5d76c71 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,11 @@ ChangeLog - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`) + - Declare target model and other non-declaration fields in a ``class Meta`` section. + +*Deprecation:* + + - Use of ``FACTORY_FOR`` and other ``FACTORY`` class-level attributes is deprecated and will be removed in 2.5. .. _v2.3.1: From b245a83019a8735d0c80c07275cd426bc60dd9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 14:46:03 +0200 Subject: [PATCH 040/714] Update docs for class Meta. --- README.rst | 21 ++-- docs/examples.rst | 6 +- docs/introduction.rst | 41 +++++--- docs/orms.rst | 47 ++++++--- docs/recipes.rst | 40 +++++--- docs/reference.rst | 219 +++++++++++++++++++++++++++++++----------- 6 files changed, 265 insertions(+), 109 deletions(-) diff --git a/README.rst b/README.rst index b35adc55..787d7541 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,8 @@ Usage Defining factories """""""""""""""""" -Factories declare a set of attributes used to instantiate an object. The class of the object must be defined in the FACTORY_FOR attribute: +Factories declare a set of attributes used to instantiate an object. +The class of the object must be defined in the ``target`` field of a ``class Meta:`` attribute: .. code-block:: python @@ -102,7 +103,8 @@ Factories declare a set of attributes used to instantiate an object. The class o from . import models class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User first_name = 'John' last_name = 'Doe' @@ -110,7 +112,8 @@ Factories declare a set of attributes used to instantiate an object. The class o # Another, different, factory for the same object class AdminFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User first_name = 'Admin' last_name = 'User' @@ -164,7 +167,9 @@ These "lazy" attributes can be added as follows: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User + first_name = 'Joe' last_name = 'Blow' email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower()) @@ -183,7 +188,9 @@ Unique values in a specific format (for example, e-mail addresses) can be genera .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User + email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n)) >>> UserFactory().email @@ -201,7 +208,9 @@ This is handled by the ``SubFactory`` helper: .. code-block:: python class PostFactory(factory.Factory): - FACTORY_FOR = models.Post + class Meta: + target = models.Post + author = factory.SubFactory(UserFactory) diff --git a/docs/examples.rst b/docs/examples.rst index aab990aa..52a5ef64 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -56,14 +56,16 @@ And now, we'll define the related factories: class AccountFactory(factory.Factory): - FACTORY_FOR = objects.Account + class Meta: + target = objects.Account username = factory.Sequence(lambda n: 'john%s' % n) email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username) class ProfileFactory(factory.Factory): - FACTORY_FOR = objects.Profile + class Meta: + target = objects.Profile account = factory.SubFactory(AccountFactory) gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) diff --git a/docs/introduction.rst b/docs/introduction.rst index 86e20463..6ea6b5e8 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -18,10 +18,11 @@ Basic usage ----------- -Factories declare a set of attributes used to instantiate an object, whose class is defined in the FACTORY_FOR attribute: +Factories declare a set of attributes used to instantiate an object, whose class is defined in the ``class Meta``'s ``target`` attribute: - Subclass ``factory.Factory`` (or a more suitable subclass) -- Set its ``FACTORY_FOR`` attribute to the target class +- Add a ``class Meta:`` block +- Set its ``target`` attribute to the target class - Add defaults for keyword args to pass to the associated class' ``__init__`` method @@ -31,7 +32,8 @@ Factories declare a set of attributes used to instantiate an object, whose class from . import base class UserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + target = base.User firstname = "John" lastname = "Doe" @@ -56,7 +58,8 @@ A given class may be associated to many :class:`~factory.Factory` subclasses: .. code-block:: python class EnglishUserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + target = base.User firstname = "John" lastname = "Doe" @@ -64,7 +67,8 @@ A given class may be associated to many :class:`~factory.Factory` subclasses: class FrenchUserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + target = base.User firstname = "Jean" lastname = "Dupont" @@ -88,7 +92,8 @@ This is achieved with the :class:`~factory.Sequence` declaration: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User username = factory.Sequence(lambda n: 'user%d' % n) @@ -104,7 +109,8 @@ This is achieved with the :class:`~factory.Sequence` declaration: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User @factory.sequence def username(n): @@ -121,7 +127,8 @@ taking the object being built and returning the value for the field: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User username = factory.Sequence(lambda n: 'user%d' % n) email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username) @@ -146,7 +153,8 @@ taking the object being built and returning the value for the field: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = models.User + class Meta: + target = models.User username = factory.Sequence(lambda n: 'user%d' % n) @@ -168,7 +176,8 @@ and update them with its own declarations: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = base.User + class Meta: + target = base.User firstname = "John" lastname = "Doe" @@ -209,13 +218,14 @@ Non-kwarg arguments Some classes take a few, non-kwarg arguments first. -This is handled by the :data:`~factory.Factory.FACTORY_ARG_PARAMETERS` attribute: +This is handled by the :data:`~factory.FactoryOptions.arg_parameters` attribute: .. code-block:: python class MyFactory(factory.Factory): - FACTORY_FOR = MyClass - FACTORY_ARG_PARAMETERS = ('x', 'y') + class Meta: + target = MyClass + arg_parameters = ('x', 'y') x = 1 y = 2 @@ -251,7 +261,8 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the .. code-block:: python class MyFactory(factory.Factory): - FACTORY_FOR = MyClass + class Meta: + target = MyClass .. code-block:: pycon @@ -265,6 +276,6 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the -The default strategy can be changed by setting the class-level :attr:`~factory.Factory.FACTORY_STRATEGY` attribute. +The default strategy can be changed by setting the ``class Meta`` :attr:`~factory.FactoryOptions.strategy` attribute. diff --git a/docs/orms.rst b/docs/orms.rst index c893cacd..5ef8568e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -32,7 +32,7 @@ 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'`` + * The :attr:`~factory.FactoryOption.target` 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 @@ -40,7 +40,12 @@ All factories for a Django :class:`~django.db.models.Model` should use the attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. - .. attribute:: FACTORY_DJANGO_GET_OR_CREATE + +.. class:: DjangoOptions(factory.base.FactoryOptions) + + The ``class Meta`` on a :class:`~DjangoModelFactory` supports an extra parameter: + + .. attribute:: django_get_or_create Fields whose name are passed in this list will be used to perform a :meth:`Model.objects.get_or_create() ` @@ -49,8 +54,9 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. code-block:: python class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = 'myapp.User' # Equivalent to ``FACTORY_FOR = myapp.models.User`` - FACTORY_DJANGO_GET_OR_CREATE = ('username',) + class Meta: + target = 'myapp.User' # Equivalent to ``target = myapp.models.User`` + django_get_or_create = ('username',) username = 'john' @@ -80,11 +86,13 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. code-block:: python class MyAbstractModelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.MyAbstractModel - ABSTRACT_FACTORY = True + class Meta: + target = models.MyAbstractModel + abstract = True class MyConcreteModelFactory(MyAbstractModelFactory): - FACTORY_FOR = models.MyConcreteModel + class Meta: + target = models.MyConcreteModel Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. @@ -112,7 +120,8 @@ Extra fields .. code-block:: python class MyFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.MyModel + class Meta: + target = models.MyModel the_file = factory.django.FileField(filename='the_file.dat') @@ -149,7 +158,8 @@ Extra fields .. code-block:: python class MyFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.MyModel + class Meta: + target = models.MyModel the_image = factory.django.ImageField(color='blue') @@ -188,7 +198,8 @@ To work around this problem, use the :meth:`mute_signals()` decorator/context ma @factory.django.mute_signals(signals.pre_save, signals.post_save) class FooFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Foo + class Meta: + target = models.Foo # ... @@ -241,7 +252,7 @@ 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`, + .. 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. @@ -255,7 +266,7 @@ SQLAlchemy 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. +To work, this class needs an `SQLAlchemy`_ session object affected to the ``Meta.sqlalchemy_session`` attribute. .. _SQLAlchemy: http://www.sqlalchemy.org/ @@ -268,7 +279,12 @@ To work, this class needs an `SQLAlchemy`_ session object affected to "FACTORY_S * :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 +.. class:: SQLAlchemyOptions(factory.base.FactoryOptions) + + In addition to the usual parameters available in :class:`class Meta `, + a :class:`SQLAlchemyModelFactory` also supports the following settings: + + .. attribute:: sqlalchemy_session Fields whose SQLAlchemy session object are passed will be used to communicate with the database @@ -297,8 +313,9 @@ A (very) simple exemple: class UserFactory(SQLAlchemyModelFactory): - FACTORY_FOR = User - FACTORY_SESSION = session # the SQLAlchemy session object + class Meta: + target = User + sqlalchemy_session = session # the SQLAlchemy session object id = factory.Sequence(lambda n: n) name = factory.Sequence(lambda n: u'User %d' % n) diff --git a/docs/recipes.rst b/docs/recipes.rst index 7a6bf23b..917bc3c4 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -26,7 +26,8 @@ use the :class:`~factory.SubFactory` declaration: from . import models class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + target = models.User first_name = factory.Sequence(lambda n: "Agent %03d" % n) group = factory.SubFactory(GroupFactory) @@ -53,7 +54,8 @@ use a :class:`~factory.RelatedFactory` declaration: # factories.py class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + target = models.User log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) @@ -75,7 +77,8 @@ factory_boy allows to define attributes of such profiles dynamically when creati .. code-block:: python class ProfileFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = my_models.Profile + class Meta: + target = my_models.Profile title = 'Dr' # We pass in profile=None to prevent UserFactory from creating another profile @@ -83,7 +86,8 @@ factory_boy allows to define attributes of such profiles dynamically when creati user = factory.SubFactory('app.factories.UserFactory', profile=None) class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = auth_models.User + class Meta: + target = auth_models.User username = factory.Sequence(lambda n: "user_%d" % n) @@ -145,12 +149,14 @@ hook: # factories.py class GroupFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Group + class Meta: + target = models.Group name = factory.Sequence(lambda n: "Group #%s" % n) class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + target = models.User name = "John Doe" @@ -200,17 +206,20 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: # factories.py class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + target = models.User name = "John Doe" class GroupFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Group + class Meta: + target = models.Group name = "Admins" class GroupLevelFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.GroupLevel + class Meta: + target = models.GroupLevel user = factory.SubFactory(UserFactory) group = factory.SubFactory(GroupFactory) @@ -273,20 +282,23 @@ Here, we want: # factories.py class CountryFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Country + class Meta: + target = models.Country name = factory.Iterator(["France", "Italy", "Spain"]) lang = factory.Iterator(['fr', 'it', 'es']) class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + target = models.User name = "John" lang = factory.SelfAttribute('country.lang') country = factory.SubFactory(CountryFactory) class CompanyFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.Company + class Meta: + target = models.Company name = "ACME, Inc." country = factory.SubFactory(CountryFactory) @@ -302,7 +314,9 @@ default :meth:`Model.objects.create() ` .. code-block:: python class UserFactory(factory.DjangoModelFactory): - FACTORY_FOR = UserenaSignup + class Meta: + target = UserenaSignup + username = "l7d8s" email = "my_name@example.com" password = "my_password" diff --git a/docs/reference.rst b/docs/reference.rst index 53584a0a..f19b44ea 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -11,37 +11,54 @@ For internals and customization points, please refer to the :doc:`internals` sec The :class:`Factory` class -------------------------- -.. class:: Factory +.. class:: FactoryOptions + + .. versionadded:: 2.4.0 + + A :class:`Factory`'s behaviour can be tuned through a few settings. + + For convenience, they are declared in a single ``class Meta`` attribute: - The :class:`Factory` class is the base of factory_boy features. + .. code-block:: python - It accepts a few specific attributes (must be specified on class declaration): + class MyFactory(factory.Factory): + class Meta: + target = MyObject + abstract = False - .. attribute:: FACTORY_FOR + .. attribute:: target This optional attribute describes the class of objects to generate. If unset, it will be inherited from parent :class:`Factory` subclasses. - .. attribute:: ABSTRACT_FACTORY + .. versionadded:: 2.4.0 + + .. attribute:: abstract 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. + subclass nor its parents define the :attr:`~FactoryOptions.target` attribute. + + .. warning:: This flag is reset to ``False`` When a :class:`Factory` subclasses + another one if a :attr:`~FactoryOptions.target` is set. + + .. versionadded:: 2.4.0 - .. attribute:: FACTORY_ARG_PARAMETERS + .. attribute:: arg_parameters Some factories require non-keyword arguments to their :meth:`~object.__init__`. - They should be listed, in order, in the :attr:`FACTORY_ARG_PARAMETERS` + They should be listed, in order, in the :attr:`arg_parameters` attribute: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User - FACTORY_ARG_PARAMETERS = ('login', 'email') + class Meta: + target = User + arg_parameters = ('login', 'email') login = 'john' email = factory.LazyAttribute(lambda o: '%s@example.com' % o.login) @@ -53,22 +70,25 @@ The :class:`Factory` class >>> User('john', 'john@example.com', firstname="John") # actual call - .. attribute:: FACTORY_HIDDEN_ARGS + .. versionadded:: 2.4.0 + + .. attribute:: hidden_args While writing a :class:`Factory` for some object, it may be useful to have general fields helping defining others, but that should not be passed to the target class; for instance, a field named 'now' that would hold a reference time used by other objects. - Factory fields whose name are listed in :attr:`FACTORY_HIDDEN_ARGS` will + Factory fields whose name are listed in :attr:`hidden_args` will be removed from the set of args/kwargs passed to the underlying class; they can be any valid factory_boy declaration: .. code-block:: python class OrderFactory(factory.Factory): - FACTORY_FOR = Order - FACTORY_HIDDEN_ARGS = ('now',) + class Meta: + target = Order + hidden_args = ('now',) now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow()) started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1)) @@ -83,6 +103,55 @@ The :class:`Factory` class >>> OrderFactory(now=datetime.datetime(2013, 4, 1, 10)) + .. versionadded:: 2.4.0 + + +.. class:: Factory + + .. note:: In previous versions, the fields of :class:`class Meta ` were + defined as class attributes on :class:`Factory`. This is now deprecated and will be removed + in 2.5.0. + + .. attribute:: FACTORY_FOR + + .. deprecated:: 2.4.0 + See :attr:`FactoryOptions.target`. + + .. attribute:: ABSTRACT_FACTORY + + .. deprecated:: 2.4.0 + See :attr:`FactoryOptions.abstract`. + + .. attribute:: FACTORY_ARG_PARAMETERS + + .. deprecated:: 2.4.0 + See :attr:`FactoryOptions.arg_parameters`. + + .. attribute:: FACTORY_HIDDEN_ARGS + + .. deprecated:: 2.4.0 + See :attr:`FactoryOptions.hidden_args`. + + + **Class-level attributes:** + + .. attribute:: _meta + + .. versionadded:: 2.4.0 + + The :class:`FactoryOptions` instance attached to a :class:`Factory` class is available + as a :attr:`_meta` attribute. + + .. attribute:: _options_class + + .. versionadded:: 2.4.0 + + If a :class:`Factory` subclass needs to define additional, extra options, it has to + provide a custom :class:`FactoryOptions` subclass. + + A pointer to that custom class should be provided as :attr:`_options_class` so that + the :class:`Factory`-building metaclass can use it instead. + **Base functions:** @@ -162,7 +231,7 @@ The :class:`Factory` class The :meth:`_adjust_kwargs` extension point allows for late fields tuning. It is called once keyword arguments have been resolved and post-generation - items removed, but before the :attr:`FACTORY_ARG_PARAMETERS` extraction + items removed, but before the :attr:`~FactoryOptions.arg_parameters` extraction phase. .. code-block:: python @@ -194,7 +263,7 @@ The :class:`Factory` class .. OHAI_VIM* This class method is called whenever a new instance needs to be built. - It receives the target class (provided to :attr:`FACTORY_FOR`), and + It receives the target class (provided to :attr:`~FactoryOptions.target`), and the positional and keyword arguments to use for the class once all has been computed. @@ -214,7 +283,8 @@ The :class:`Factory` class .. code-block:: python class BaseBackendFactory(factory.Factory): - ABSTRACT_FACTORY = True # Optional + class Meta: + abstract = True # Optional def _create(cls, target_class, *args, **kwargs): obj = target_class(*args, **kwargs) @@ -254,7 +324,7 @@ The :class:`Factory` class >>> SomeFactory._next_sequence 4 - Since subclasses of a non-:attr:`abstract ` + 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. @@ -293,7 +363,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. but not persisted to any datastore. It is usually a simple call to the :meth:`~object.__init__` method of the - :attr:`~Factory.FACTORY_FOR` class. + :attr:`~FactoryOptions.target` class. .. data:: CREATE_STRATEGY @@ -316,7 +386,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. when using the ``create`` strategy. That policy will be used if the - :attr:`associated class ` has an ``objects`` + :attr:`associated class ` has an ``objects`` attribute *and* the :meth:`~Factory._create` classmethod of the :class:`Factory` wasn't overridden. @@ -337,7 +407,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. data:: STUB_STRATEGY The 'stub' strategy is an exception in the factory_boy world: it doesn't return - an instance of the :attr:`~Factory.FACTORY_FOR` class, and actually doesn't + an instance of the :attr:`~FactoryOptions.target` class, and actually doesn't require one to be present. Instead, it returns an instance of :class:`StubObject` whose attributes have been @@ -359,7 +429,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. class:: StubFactory(Factory) - An :attr:`abstract ` :class:`Factory`, + An :attr:`abstract ` :class:`Factory`, with a default strategy set to :data:`STUB_STRATEGY`. @@ -414,7 +484,8 @@ accept the object being built as sole argument, and return a value. .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User username = 'john' email = factory.LazyAttribute(lambda o: '%s@example.com' % o.username) @@ -449,7 +520,8 @@ return value of the method: .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + target = User name = u"Jean" @@ -487,7 +559,8 @@ This declaration takes a single argument, a function accepting a single paramete .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + target = User phone = factory.Sequence(lambda n: '123-555-%04d' % n) @@ -512,7 +585,8 @@ be the sequence counter - this might be confusing: .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + target = User @factory.sequence def phone(n): @@ -537,7 +611,8 @@ The sequence counter is shared across all :class:`Sequence` attributes of the .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User phone = factory.Sequence(lambda n: '%04d' % n) office = factory.Sequence(lambda n: 'A23-B%03d' % n) @@ -561,7 +636,8 @@ sequence counter is shared: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User phone = factory.Sequence(lambda n: '123-555-%04d' % n) @@ -596,7 +672,8 @@ class-level value. .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User uid = factory.Sequence(int) @@ -631,7 +708,8 @@ It takes a single argument, a function whose two parameters are, in order: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User login = 'john' email = factory.LazyAttributeSequence(lambda o, n: '%s@s%d.example.com' % (o.login, n)) @@ -655,7 +733,8 @@ handles more complex cases: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User login = 'john' @@ -692,7 +771,8 @@ The :class:`SubFactory` attribute should be called with: .. code-block:: python class FooFactory(factory.Factory): - FACTORY_FOR = Foo + class Meta: + target = Foo bar = factory.SubFactory(BarFactory) # Not BarFactory() @@ -705,7 +785,8 @@ Definition # A standard factory class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User # Various fields first_name = 'John' @@ -714,7 +795,8 @@ Definition # A factory for an object with a 'User' field class CompanyFactory(factory.Factory): - FACTORY_FOR = Company + class Meta: + target = Company name = factory.Sequence(lambda n: 'FactoryBoyz' + 'z' * n) @@ -794,13 +876,15 @@ This issue can be handled by passing the absolute import path to the target .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User username = 'john' main_group = factory.SubFactory('users.factories.GroupFactory') class GroupFactory(factory.Factory): - FACTORY_FOR = Group + class Meta: + target = Group name = "MyGroup" owner = factory.SubFactory(UserFactory) @@ -828,7 +912,8 @@ That declaration takes a single argument, a dot-delimited path to the attribute .. code-block:: python class UserFactory(factory.Factory) - FACTORY_FOR = User + class Meta: + target = User birthdate = factory.Sequence(lambda n: datetime.date(2000, 1, 1) + datetime.timedelta(days=n)) birthmonth = factory.SelfAttribute('birthdate.month') @@ -854,13 +939,15 @@ gains an "upward" semantic through the double-dot notation, as used in Python im .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User language = 'en' class CompanyFactory(factory.Factory): - FACTORY_FOR = Company + class Meta: + target = Company country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, language=factory.SelfAttribute('..country.language')) @@ -888,7 +975,8 @@ through the :attr:`~containers.LazyStub.factory_parent` attribute of the passed- .. code-block:: python class CompanyFactory(factory.Factory): - FACTORY_FOR = Company + class Meta: + target = Company country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, language=factory.LazyAttribute(lambda user: user.factory_parent.country.language), @@ -966,7 +1054,8 @@ adequate value. .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User # CATEGORY_CHOICES is a list of (key, title) tuples category = factory.Iterator(User.CATEGORY_CHOICES, getter=lambda c: c[0]) @@ -987,7 +1076,8 @@ use the :func:`iterator` decorator: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User @factory.iterator def name(): @@ -1030,7 +1120,8 @@ with the :class:`Dict` and :class:`List` attributes: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User is_superuser = False roles = factory.Dict({ @@ -1066,7 +1157,8 @@ with the :class:`Dict` and :class:`List` attributes: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User flags = factory.List([ 'user', @@ -1113,7 +1205,8 @@ For instance, a :class:`PostGeneration` hook is declared as ``post``: .. code-block:: python class SomeFactory(factory.Factory): - FACTORY_FOR = SomeObject + class Meta: + target = SomeObject @post_generation def post(self, create, extracted, **kwargs): @@ -1128,7 +1221,7 @@ When calling the factory, some arguments will be extracted for this method: - Any argument starting with ``post__XYZ`` will be extracted, its ``post__`` prefix removed, and added to the kwargs passed to the post-generation hook. -Extracted arguments won't be passed to the :attr:`~Factory.FACTORY_FOR` class. +Extracted arguments won't be passed to the :attr:`~FactoryOptions.target` class. Thus, in the following call: @@ -1142,7 +1235,7 @@ Thus, in the following call: ) The ``post`` hook will receive ``1`` as ``extracted`` and ``{'y': 3, 'z__t': 42}`` -as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory.FACTORY_FOR``. +as keyword arguments; ``{'post_x': 2}`` will be passed to ``SomeFactory._meta.target``. RelatedFactory @@ -1184,7 +1277,8 @@ RelatedFactory .. code-block:: python class FooFactory(factory.Factory): - FACTORY_FOR = Foo + class Meta: + target = Foo bar = factory.RelatedFactory(BarFactory) # Not BarFactory() @@ -1192,13 +1286,15 @@ RelatedFactory .. code-block:: python class CityFactory(factory.Factory): - FACTORY_FOR = City + class Meta: + target = City capital_of = None name = "Toronto" class CountryFactory(factory.Factory): - FACTORY_FOR = Country + class Meta: + target = Country lang = 'fr' capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris") @@ -1260,7 +1356,8 @@ as ``callable(obj, create, extracted, **kwargs)``, where: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User login = 'john' make_mbox = factory.PostGeneration( @@ -1280,7 +1377,8 @@ A decorator is also provided, decorating a single method accepting the same .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User login = 'john' @@ -1316,7 +1414,7 @@ PostGenerationMethodCall .. attribute:: method_name - The name of the method to call on the :attr:`~Factory.FACTORY_FOR` object + The name of the method to call on the :attr:`~FactoryOptions.target` object .. attribute:: args @@ -1340,7 +1438,8 @@ attribute like below: .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User username = 'user' password = factory.PostGenerationMethodCall('set_password', @@ -1390,7 +1489,8 @@ factory during instantiation. .. code-block:: python class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = User + class Meta: + target = User username = 'user' password = factory.PostGenerationMethodCall('set_password', @@ -1404,7 +1504,8 @@ example, if we declared the ``password`` attribute like the following, .. code-block:: python class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User username = 'user' password = factory.PostGenerationMethodCall('set_password', '', 'sha1') @@ -1467,7 +1568,8 @@ Lightweight factory declaration # This is equivalent to: class UserFactory(factory.Factory): - FACTORY_FOR = User + class Meta: + target = User login = 'john' email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) @@ -1486,7 +1588,8 @@ Lightweight factory declaration # This is equivalent to: class UserFactory(factory.django.DjangoModelFactory): - FACTORY_FOR = models.User + class Meta: + target = models.User login = 'john' email = factory.LazyAttribute(lambda u: '%s@example.com' % u.login) From 3a5709527d362a960a1a35769375412e4536839e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 15:10:56 +0200 Subject: [PATCH 041/714] Rename 'target' to 'model'. --- README.rst | 12 +- docs/examples.rst | 4 +- docs/introduction.rst | 24 ++-- docs/orms.rst | 18 +-- docs/recipes.rst | 30 ++--- docs/reference.rst | 106 ++++++++-------- factory/alchemy.py | 6 +- factory/base.py | 76 ++++++------ factory/containers.py | 14 +-- factory/declarations.py | 4 +- factory/django.py | 28 ++--- factory/helpers.py | 2 +- factory/mogo.py | 8 +- factory/mongoengine.py | 8 +- tests/cyclic/bar.py | 2 +- tests/cyclic/foo.py | 2 +- tests/test_alchemy.py | 4 +- tests/test_base.py | 56 ++++----- tests/test_containers.py | 2 +- tests/test_deprecation.py | 2 +- tests/test_django.py | 40 +++---- tests/test_mongoengine.py | 4 +- tests/test_using.py | 246 +++++++++++++++++++------------------- 23 files changed, 349 insertions(+), 349 deletions(-) diff --git a/README.rst b/README.rst index 787d7541..b4ba6895 100644 --- a/README.rst +++ b/README.rst @@ -95,7 +95,7 @@ Defining factories """""""""""""""""" Factories declare a set of attributes used to instantiate an object. -The class of the object must be defined in the ``target`` field of a ``class Meta:`` attribute: +The class of the object must be defined in the ``model`` field of a ``class Meta:`` attribute: .. code-block:: python @@ -104,7 +104,7 @@ The class of the object must be defined in the ``target`` field of a ``class Met class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User first_name = 'John' last_name = 'Doe' @@ -113,7 +113,7 @@ The class of the object must be defined in the ``target`` field of a ``class Met # Another, different, factory for the same object class AdminFactory(factory.Factory): class Meta: - target = models.User + model = models.User first_name = 'Admin' last_name = 'User' @@ -168,7 +168,7 @@ These "lazy" attributes can be added as follows: class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User first_name = 'Joe' last_name = 'Blow' @@ -189,7 +189,7 @@ Unique values in a specific format (for example, e-mail addresses) can be genera class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n)) @@ -209,7 +209,7 @@ This is handled by the ``SubFactory`` helper: class PostFactory(factory.Factory): class Meta: - target = models.Post + model = models.Post author = factory.SubFactory(UserFactory) diff --git a/docs/examples.rst b/docs/examples.rst index 52a5ef64..a57080e0 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -57,7 +57,7 @@ And now, we'll define the related factories: class AccountFactory(factory.Factory): class Meta: - target = objects.Account + model = objects.Account username = factory.Sequence(lambda n: 'john%s' % n) email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username) @@ -65,7 +65,7 @@ And now, we'll define the related factories: class ProfileFactory(factory.Factory): class Meta: - target = objects.Profile + model = objects.Profile account = factory.SubFactory(AccountFactory) gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) diff --git a/docs/introduction.rst b/docs/introduction.rst index 6ea6b5e8..5e3b4d82 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -18,11 +18,11 @@ Basic usage ----------- -Factories declare a set of attributes used to instantiate an object, whose class is defined in the ``class Meta``'s ``target`` attribute: +Factories declare a set of attributes used to instantiate an object, whose class is defined in the ``class Meta``'s ``model`` attribute: - Subclass ``factory.Factory`` (or a more suitable subclass) - Add a ``class Meta:`` block -- Set its ``target`` attribute to the target class +- Set its ``model`` attribute to the target class - Add defaults for keyword args to pass to the associated class' ``__init__`` method @@ -33,7 +33,7 @@ Factories declare a set of attributes used to instantiate an object, whose class class UserFactory(factory.Factory): class Meta: - target = base.User + model = base.User firstname = "John" lastname = "Doe" @@ -59,7 +59,7 @@ A given class may be associated to many :class:`~factory.Factory` subclasses: class EnglishUserFactory(factory.Factory): class Meta: - target = base.User + model = base.User firstname = "John" lastname = "Doe" @@ -68,7 +68,7 @@ A given class may be associated to many :class:`~factory.Factory` subclasses: class FrenchUserFactory(factory.Factory): class Meta: - target = base.User + model = base.User firstname = "Jean" lastname = "Dupont" @@ -93,7 +93,7 @@ This is achieved with the :class:`~factory.Sequence` declaration: class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User username = factory.Sequence(lambda n: 'user%d' % n) @@ -110,7 +110,7 @@ This is achieved with the :class:`~factory.Sequence` declaration: class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User @factory.sequence def username(n): @@ -128,7 +128,7 @@ taking the object being built and returning the value for the field: class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User username = factory.Sequence(lambda n: 'user%d' % n) email = factory.LazyAttribute(lambda obj: '%s@example.com' % obj.username) @@ -154,7 +154,7 @@ taking the object being built and returning the value for the field: class UserFactory(factory.Factory): class Meta: - target = models.User + model = models.User username = factory.Sequence(lambda n: 'user%d' % n) @@ -177,7 +177,7 @@ and update them with its own declarations: class UserFactory(factory.Factory): class Meta: - target = base.User + model = base.User firstname = "John" lastname = "Doe" @@ -224,7 +224,7 @@ This is handled by the :data:`~factory.FactoryOptions.arg_parameters` attribute: class MyFactory(factory.Factory): class Meta: - target = MyClass + model = MyClass arg_parameters = ('x', 'y') x = 1 @@ -262,7 +262,7 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the class MyFactory(factory.Factory): class Meta: - target = MyClass + model = MyClass .. code-block:: pycon diff --git a/docs/orms.rst b/docs/orms.rst index 5ef8568e..d3d98c94 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -32,7 +32,7 @@ All factories for a Django :class:`~django.db.models.Model` should use the This class provides the following features: - * The :attr:`~factory.FactoryOption.target` attribute also supports the ``'app.Model'`` + * The :attr:`~factory.FactoryOptions.model` attribute also supports the ``'app.Model'`` syntax * :func:`~factory.Factory.create()` uses :meth:`Model.objects.create() ` * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value @@ -55,7 +55,7 @@ All factories for a Django :class:`~django.db.models.Model` should use the class UserFactory(factory.django.DjangoModelFactory): class Meta: - target = 'myapp.User' # Equivalent to ``target = myapp.models.User`` + model = 'myapp.User' # Equivalent to ``model = myapp.models.User`` django_get_or_create = ('username',) username = 'john' @@ -87,12 +87,12 @@ All factories for a Django :class:`~django.db.models.Model` should use the class MyAbstractModelFactory(factory.django.DjangoModelFactory): class Meta: - target = models.MyAbstractModel + model = models.MyAbstractModel abstract = True class MyConcreteModelFactory(MyAbstractModelFactory): class Meta: - target = models.MyConcreteModel + model = models.MyConcreteModel Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. @@ -121,7 +121,7 @@ Extra fields class MyFactory(factory.django.DjangoModelFactory): class Meta: - target = models.MyModel + model = models.MyModel the_file = factory.django.FileField(filename='the_file.dat') @@ -159,7 +159,7 @@ Extra fields class MyFactory(factory.django.DjangoModelFactory): class Meta: - target = models.MyModel + model = models.MyModel the_image = factory.django.ImageField(color='blue') @@ -199,7 +199,7 @@ To work around this problem, use the :meth:`mute_signals()` decorator/context ma @factory.django.mute_signals(signals.pre_save, signals.post_save) class FooFactory(factory.django.DjangoModelFactory): class Meta: - target = models.Foo + model = models.Foo # ... @@ -252,7 +252,7 @@ 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`, + .. note:: If the :attr:`associated class ` class UserFactory(factory.DjangoModelFactory): class Meta: - target = UserenaSignup + model = UserenaSignup username = "l7d8s" email = "my_name@example.com" password = "my_password" @classmethod - def _create(cls, target_class, *args, **kwargs): + def _create(cls, model_class, *args, **kwargs): """Override the default ``_create`` with our custom call.""" - manager = cls._get_manager(target_class) + manager = cls._get_manager(model_class) # The default would use ``manager.create(*args, **kwargs)`` return manager.create_user(*args, **kwargs) diff --git a/docs/reference.rst b/docs/reference.rst index f19b44ea..d616d1c4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -23,10 +23,10 @@ The :class:`Factory` class class MyFactory(factory.Factory): class Meta: - target = MyObject + model = MyObject abstract = False - .. attribute:: target + .. attribute:: model This optional attribute describes the class of objects to generate. @@ -40,10 +40,10 @@ The :class:`Factory` class 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:`~FactoryOptions.target` attribute. + subclass nor its parents define the :attr:`~FactoryOptions.model` attribute. .. warning:: This flag is reset to ``False`` When a :class:`Factory` subclasses - another one if a :attr:`~FactoryOptions.target` is set. + another one if a :attr:`~FactoryOptions.model` is set. .. versionadded:: 2.4.0 @@ -57,7 +57,7 @@ The :class:`Factory` class class UserFactory(factory.Factory): class Meta: - target = User + model = User arg_parameters = ('login', 'email') login = 'john' @@ -76,7 +76,7 @@ The :class:`Factory` class While writing a :class:`Factory` for some object, it may be useful to have general fields helping defining others, but that should not be - passed to the target class; for instance, a field named 'now' that would + passed to the model class; for instance, a field named 'now' that would hold a reference time used by other objects. Factory fields whose name are listed in :attr:`hidden_args` will @@ -87,7 +87,7 @@ The :class:`Factory` class class OrderFactory(factory.Factory): class Meta: - target = Order + model = Order hidden_args = ('now',) now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow()) @@ -115,7 +115,7 @@ The :class:`Factory` class .. attribute:: FACTORY_FOR .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.target`. + See :attr:`FactoryOptions.model`. .. attribute:: ABSTRACT_FACTORY @@ -258,19 +258,19 @@ The :class:`Factory` class Subclasses may fetch the next free ID from the database, for instance. - .. classmethod:: _build(cls, target_class, *args, **kwargs) + .. classmethod:: _build(cls, model_class, *args, **kwargs) .. OHAI_VIM* This class method is called whenever a new instance needs to be built. - It receives the target class (provided to :attr:`~FactoryOptions.target`), and + It receives the model class (provided to :attr:`~FactoryOptions.model`), and the positional and keyword arguments to use for the class once all has been computed. Subclasses may override this for custom APIs. - .. classmethod:: _create(cls, target_class, *args, **kwargs) + .. classmethod:: _create(cls, model_class, *args, **kwargs) .. OHAI_VIM* @@ -286,8 +286,8 @@ The :class:`Factory` class class Meta: abstract = True # Optional - def _create(cls, target_class, *args, **kwargs): - obj = target_class(*args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + obj = model_class(*args, **kwargs) obj.save() return obj @@ -363,7 +363,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. but not persisted to any datastore. It is usually a simple call to the :meth:`~object.__init__` method of the - :attr:`~FactoryOptions.target` class. + :attr:`~FactoryOptions.model` class. .. data:: CREATE_STRATEGY @@ -386,7 +386,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. when using the ``create`` strategy. That policy will be used if the - :attr:`associated class ` has an ``objects`` + :attr:`associated class ' % cls.__name__ else: - return '<%s for %s>' % (cls.__name__, cls._meta.target) + return '<%s for %s>' % (cls.__name__, cls._meta.model) class BaseMeta: @@ -189,7 +189,7 @@ def _build_default_options(self): to update() its return value. """ return [ - OptionDefault('target', None, inherit=True), + OptionDefault('model', None, inherit=True), OptionDefault('abstract', False, inherit=False), OptionDefault('strategy', CREATE_STRATEGY, inherit=True), OptionDefault('arg_parameters', (), inherit=True), @@ -225,8 +225,8 @@ def contribute_to_class(self, factory, self._fill_from_meta(meta=meta, base_meta=base_meta) - self.target = self.factory._load_target_class(self.target) - if self.target is None: + self.model = self.factory._load_model_class(self.model) + if self.model is None: self.abstract = True self.counter_reference = self._get_counter_reference() @@ -246,10 +246,10 @@ def contribute_to_class(self, factory, def _get_counter_reference(self): """Identify which factory should be used for a shared counter.""" - if (self.target is not None + if (self.model is not None and self.base_factory is not None - and self.base_factory._meta.target is not None - and issubclass(self.target, self.base_factory._meta.target)): + and self.base_factory._meta.model is not None + and issubclass(self.model, self.base_factory._meta.model)): return self.base_factory else: return self.factory @@ -323,7 +323,7 @@ def __new__(cls, *args, **kwargs): _meta = FactoryOptions() _OLDSTYLE_ATTRIBUTES = { - 'FACTORY_FOR': 'target', + 'FACTORY_FOR': 'model', 'ABSTRACT_FACTORY': 'abstract', 'FACTORY_STRATEGY': 'strategy', 'FACTORY_ARG_PARAMETERS': 'arg_parameters', @@ -444,8 +444,8 @@ def _adjust_kwargs(cls, **kwargs): return kwargs @classmethod - def _load_target_class(cls, class_definition): - """Extension point for loading target classes. + def _load_model_class(cls, class_definition): + """Extension point for loading model classes. This can be overridden in framework-specific subclasses to hook into existing model repositories, for instance. @@ -453,10 +453,10 @@ def _load_target_class(cls, class_definition): return class_definition @classmethod - def _get_target_class(cls): - """Retrieve the actual, associated target class.""" - definition = cls._meta.target - return cls._load_target_class(definition) + def _get_model_class(cls): + """Retrieve the actual, associated model class.""" + definition = cls._meta.model + return cls._load_model_class(definition) @classmethod def _prepare(cls, create, **kwargs): @@ -466,7 +466,7 @@ def _prepare(cls, create, **kwargs): create: bool, whether to create or to build the object **kwargs: arguments to pass to the creation function """ - target_class = cls._get_target_class() + model_class = cls._get_model_class() kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. @@ -482,9 +482,9 @@ def _prepare(cls, create, **kwargs): utils.log_pprint(args, kwargs), ) if create: - return cls._create(target_class, *args, **kwargs) + return cls._create(model_class, *args, **kwargs) else: - return cls._build(target_class, *args, **kwargs) + return cls._build(model_class, *args, **kwargs) @classmethod def _generate(cls, create, attrs): @@ -497,7 +497,7 @@ def _generate(cls, create, attrs): if cls._meta.abstract: raise FactoryError( "Cannot generate instances of abstract factory %(f)s; " - "Ensure %(f)s.Meta.target is set and %(f)s.Meta.abstract " + "Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract " "is either not set or False." % dict(f=cls.__name__)) # Extract declarations used for post-generation @@ -531,34 +531,34 @@ def _after_postgeneration(cls, obj, create, results=None): pass @classmethod - def _build(cls, target_class, *args, **kwargs): - """Actually build an instance of the target_class. + def _build(cls, model_class, *args, **kwargs): + """Actually build an instance of the model_class. Customization point, will be called once the full set of args and kwargs has been computed. Args: - target_class (type): the class for which an instance should be + model_class (type): the class for which an instance should be built args (tuple): arguments to use when building the class kwargs (dict): keyword arguments to use when building the class """ - return target_class(*args, **kwargs) + return model_class(*args, **kwargs) @classmethod - def _create(cls, target_class, *args, **kwargs): - """Actually create an instance of the target_class. + def _create(cls, model_class, *args, **kwargs): + """Actually create an instance of the model_class. Customization point, will be called once the full set of args and kwargs has been computed. Args: - target_class (type): the class for which an instance should be + model_class (type): the class for which an instance should be created args (tuple): arguments to use when creating the class kwargs (dict): keyword arguments to use when creating the class """ - return target_class(*args, **kwargs) + return model_class(*args, **kwargs) @classmethod def build(cls, **kwargs): @@ -705,7 +705,7 @@ class StubFactory(Factory): class Meta: strategy = STUB_STRATEGY - target = containers.StubObject + model = containers.StubObject @classmethod def build(cls, **kwargs): @@ -722,20 +722,20 @@ class Meta: abstract = True @classmethod - def _build(cls, target_class, *args, **kwargs): + def _build(cls, model_class, *args, **kwargs): if args: raise ValueError( "DictFactory %r does not support Meta.arg_parameters.", cls) - return target_class(**kwargs) + return model_class(**kwargs) @classmethod - def _create(cls, target_class, *args, **kwargs): - return cls._build(target_class, *args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + return cls._build(model_class, *args, **kwargs) class DictFactory(BaseDictFactory): class Meta: - target = dict + model = dict class BaseListFactory(Factory): @@ -744,22 +744,22 @@ class Meta: abstract = True @classmethod - def _build(cls, target_class, *args, **kwargs): + def _build(cls, model_class, *args, **kwargs): if args: raise ValueError( "ListFactory %r does not support Meta.arg_parameters.", cls) values = [v for k, v in sorted(kwargs.items())] - return target_class(values) + return model_class(values) @classmethod - def _create(cls, target_class, *args, **kwargs): - return cls._build(target_class, *args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + return cls._build(model_class, *args, **kwargs) class ListFactory(BaseListFactory): class Meta: - target = list + model = list def use_strategy(new_strategy): diff --git a/factory/containers.py b/factory/containers.py index c0c5e246..5116320d 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -47,27 +47,27 @@ class LazyStub(object): __containers (LazyStub list): "parents" of the LazyStub being built. This allows to have the field of a field depend on the value of another field - __target_class (type): the target class to build. + __model_class (type): the model class to build. """ __initialized = False - def __init__(self, attrs, containers=(), target_class=object, log_ctx=None): + def __init__(self, attrs, containers=(), model_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.__model_class = model_class + self.__log_ctx = log_ctx or '%s.%s' % (model_class.__module__, model_class.__name__) self.factory_parent = containers[0] if containers else None self.__initialized = True def __repr__(self): - return '' % (self.__target_class.__module__, self.__target_class.__name__) + return '' % (self.__model_class.__module__, self.__model_class.__name__) def __str__(self): return '' % ( - self.__target_class.__name__, list(self.__attrs.keys())) + self.__model_class.__name__, list(self.__attrs.keys())) def __fill__(self): """Fill this LazyStub, computing values of all defined attributes. @@ -224,7 +224,7 @@ def build(self, create, force_sequence=None): wrapped_attrs[k] = v stub = LazyStub(wrapped_attrs, containers=self._containers, - target_class=self.factory, log_ctx=self._log_ctx) + model_class=self.factory, log_ctx=self._log_ctx) return stub.__fill__() diff --git a/factory/declarations.py b/factory/declarations.py index 037a6797..5e7e734a 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -50,7 +50,7 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): attributes containers (list of containers.LazyStub): The chain of SubFactory which led to building this object. - create (bool): whether the target class should be 'built' or + create (bool): whether the model class should be 'built' or 'created' extra (DeclarationDict or None): extracted key/value extracted from the attribute prefix @@ -434,7 +434,7 @@ def __repr__(self): class PostGenerationDeclaration(object): - """Declarations to be called once the target object has been generated.""" + """Declarations to be called once the model object has been generated.""" def extract(self, name, attrs): """Extract relevant attributes from a dict. diff --git a/factory/django.py b/factory/django.py index 77afd8cc..60901453 100644 --- a/factory/django.py +++ b/factory/django.py @@ -61,10 +61,10 @@ def _build_default_options(self): def _get_counter_reference(self): counter_reference = super(DjangoOptions, self)._get_counter_reference() if (counter_reference == self.base_factory - and self.base_factory._meta.target is not None - and self.base_factory._meta.target._meta.abstract - and self.target is not None - and not self.target._meta.abstract): + and self.base_factory._meta.model is not None + and self.base_factory._meta.model._meta.abstract + and self.model is not None + and not self.model._meta.abstract): # Target factory is for an abstract model, yet we're for another, # concrete subclass => don't reuse the counter. return self.factory @@ -90,7 +90,7 @@ class Meta: }) @classmethod - def _load_target_class(cls, definition): + def _load_model_class(cls, definition): if is_string(definition) and '.' in definition: app, model = definition.split('.', 1) @@ -100,17 +100,17 @@ def _load_target_class(cls, definition): return definition @classmethod - def _get_manager(cls, target_class): + def _get_manager(cls, model_class): try: - return target_class._default_manager # pylint: disable=W0212 + return model_class._default_manager # pylint: disable=W0212 except AttributeError: - return target_class.objects + return model_class.objects @classmethod def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" - model = cls._get_target_class() # pylint: disable=E1101 + model = cls._get_model_class() # pylint: disable=E1101 manager = cls._get_manager(model) try: @@ -122,9 +122,9 @@ def _setup_next_sequence(cls): return 1 @classmethod - def _get_or_create(cls, target_class, *args, **kwargs): + def _get_or_create(cls, model_class, *args, **kwargs): """Create an instance of the model through objects.get_or_create.""" - manager = cls._get_manager(target_class) + manager = cls._get_manager(model_class) assert 'defaults' not in cls._meta.django_get_or_create, ( "'defaults' is a reserved keyword for get_or_create " @@ -140,12 +140,12 @@ def _get_or_create(cls, target_class, *args, **kwargs): return obj @classmethod - def _create(cls, target_class, *args, **kwargs): + def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" - manager = cls._get_manager(target_class) + manager = cls._get_manager(model_class) if cls._meta.django_get_or_create: - return cls._get_or_create(target_class, *args, **kwargs) + return cls._get_or_create(model_class, *args, **kwargs) return manager.create(*args, **kwargs) diff --git a/factory/helpers.py b/factory/helpers.py index 0c387d04..19431df1 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -51,7 +51,7 @@ def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ class Meta: - target = klass + model = klass kwargs['Meta'] = Meta base_class = kwargs.pop('FACTORY_CLASS', base.Factory) diff --git a/factory/mogo.py b/factory/mogo.py index 0214119e..5541043f 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -36,11 +36,11 @@ class Meta: abstract = True @classmethod - def _build(cls, target_class, *args, **kwargs): - return target_class.new(*args, **kwargs) + def _build(cls, model_class, *args, **kwargs): + return model_class.new(*args, **kwargs) @classmethod - def _create(cls, target_class, *args, **kwargs): - instance = target_class.new(*args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + instance = model_class.new(*args, **kwargs) instance.save() return instance diff --git a/factory/mongoengine.py b/factory/mongoengine.py index 729ebb12..e3ab99c8 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -37,12 +37,12 @@ class Meta: abstract = True @classmethod - def _build(cls, target_class, *args, **kwargs): - return target_class(*args, **kwargs) + def _build(cls, model_class, *args, **kwargs): + return model_class(*args, **kwargs) @classmethod - def _create(cls, target_class, *args, **kwargs): - instance = target_class(*args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + instance = model_class(*args, **kwargs) if instance._is_document: instance.save() return instance diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index 8674c4d9..a5e6bf16 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -31,7 +31,7 @@ def __init__(self, foo, y): class BarFactory(factory.Factory): class Meta: - target = Bar + model = Bar y = 13 foo = factory.SubFactory('cyclic.foo.FooFactory') diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index 5310b1e0..18de3624 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -33,7 +33,7 @@ def __init__(self, bar, x): class FooFactory(factory.Factory): class Meta: - target = Foo + model = Foo x = 42 bar = factory.SubFactory(bar_mod.BarFactory) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 4526ad1b..b9222eb3 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -48,7 +48,7 @@ class Meta: class StandardFactory(SQLAlchemyModelFactory): class Meta: - target = models.StandardModel + model = models.StandardModel sqlalchemy_session = models.session id = factory.Sequence(lambda n: n) @@ -57,7 +57,7 @@ class Meta: class NonIntegerPkFactory(SQLAlchemyModelFactory): class Meta: - target = models.NonIntegerPk + model = models.NonIntegerPk sqlalchemy_session = models.session id = factory.Sequence(lambda n: 'foo%d' % n) diff --git a/tests/test_base.py b/tests/test_base.py index c44ebd53..d93bf294 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -53,8 +53,8 @@ class Meta: abstract = True @classmethod - def _create(cls, target_class, *args, **kwargs): - return target_class.create(**kwargs) + def _create(cls, model_class, *args, **kwargs): + return model_class.create(**kwargs) class TestModel(FakeDjangoModel): @@ -68,13 +68,13 @@ def test_base_factory(self): class AbstractFactoryTestCase(unittest.TestCase): def test_factory_for_optional(self): - """Ensure that target= is optional for abstract=True.""" + """Ensure that model= is optional for abstract=True.""" class TestObjectFactory(base.Factory): class Meta: abstract = True self.assertTrue(TestObjectFactory._meta.abstract) - self.assertIsNone(TestObjectFactory._meta.target) + self.assertIsNone(TestObjectFactory._meta.model) def test_factory_for_and_abstract_factory_optional(self): """Ensure that Meta.abstract is optional.""" @@ -82,7 +82,7 @@ class TestObjectFactory(base.Factory): pass self.assertTrue(TestObjectFactory._meta.abstract) - self.assertIsNone(TestObjectFactory._meta.target) + self.assertIsNone(TestObjectFactory._meta.model) def test_abstract_factory_cannot_be_called(self): class TestObjectFactory(base.Factory): @@ -97,18 +97,18 @@ def test_abstract_factory_not_inherited(self): class TestObjectFactory(base.Factory): class Meta: abstract = True - target = TestObject + model = TestObject class TestObjectChildFactory(TestObjectFactory): pass self.assertFalse(TestObjectChildFactory._meta.abstract) - def test_abstract_or_target_is_required(self): + def test_abstract_or_model_is_required(self): class TestObjectFactory(base.Factory): class Meta: abstract = False - target = None + model = None self.assertRaises(base.FactoryError, TestObjectFactory.build) self.assertRaises(base.FactoryError, TestObjectFactory.create) @@ -121,7 +121,7 @@ class AbstractFactory(base.Factory): # Declarative attributes self.assertTrue(AbstractFactory._meta.abstract) - self.assertIsNone(AbstractFactory._meta.target) + self.assertIsNone(AbstractFactory._meta.model) self.assertEqual((), AbstractFactory._meta.arg_parameters) self.assertEqual((), AbstractFactory._meta.hidden_args) self.assertEqual(base.CREATE_STRATEGY, AbstractFactory._meta.strategy) @@ -206,7 +206,7 @@ class DeclarationParsingTests(unittest.TestCase): def test_classmethod(self): class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject @classmethod def some_classmethod(cls): @@ -222,16 +222,16 @@ def test_magic_happens(self): """Calling a FooFactory doesn't yield a FooFactory instance.""" class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject - self.assertEqual(TestObject, TestObjectFactory._meta.target) + self.assertEqual(TestObject, TestObjectFactory._meta.model) obj = TestObjectFactory.build() self.assertFalse(hasattr(obj, '_meta')) def test_display(self): class TestObjectFactory(base.Factory): class Meta: - target = FakeDjangoModel + model = FakeDjangoModel self.assertIn('TestObjectFactory', str(TestObjectFactory)) self.assertIn('FakeDjangoModel', str(TestObjectFactory)) @@ -239,7 +239,7 @@ class Meta: def test_lazy_attribute_non_existent_param(self): class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject one = declarations.LazyAttribute(lambda a: a.does_not_exist ) @@ -249,13 +249,13 @@ def test_inheritance_with_sequence(self): """Tests that sequence IDs are shared between parent and son.""" class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject one = declarations.Sequence(lambda a: a) class TestSubFactory(TestObjectFactory): class Meta: - target = TestObject + model = TestObject pass @@ -273,7 +273,7 @@ def setUp(self): class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject one = declarations.Sequence(lambda n: n) self.TestObjectFactory = TestObjectFactory @@ -358,7 +358,7 @@ def test_build_strategy(self): class TestModelFactory(base.Factory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -371,7 +371,7 @@ def test_create_strategy(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -384,7 +384,7 @@ def test_stub_strategy(self): class TestModelFactory(base.Factory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -397,7 +397,7 @@ def test_unknown_strategy(self): class TestModelFactory(base.Factory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -406,7 +406,7 @@ class Meta: def test_stub_with_non_stub_strategy(self): class TestModelFactory(base.StubFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -421,7 +421,7 @@ def test_change_strategy(self): @base.use_strategy(base.CREATE_STRATEGY) class TestModelFactory(base.StubFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -432,7 +432,7 @@ class FactoryCreationTestCase(unittest.TestCase): def test_factory_for(self): class TestFactory(base.Factory): class Meta: - target = TestObject + model = TestObject self.assertTrue(isinstance(TestFactory.build(), TestObject)) @@ -445,7 +445,7 @@ class TestFactory(base.StubFactory): def test_inheritance_with_stub(self): class TestObjectFactory(base.StubFactory): class Meta: - target = TestObject + model = TestObject pass @@ -457,7 +457,7 @@ class TestFactory(TestObjectFactory): def test_custom_creation(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel @classmethod def _prepare(cls, create, **kwargs): @@ -488,7 +488,7 @@ class PostGenerationParsingTestCase(unittest.TestCase): def test_extraction(self): class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject foo = declarations.PostGenerationDeclaration() @@ -497,7 +497,7 @@ class Meta: def test_classlevel_extraction(self): class TestObjectFactory(base.Factory): class Meta: - target = TestObject + model = TestObject foo = declarations.PostGenerationDeclaration() foo__bar = 42 diff --git a/tests/test_containers.py b/tests/test_containers.py index 8a9e990a..bd7019ef 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -94,7 +94,7 @@ def test_representation(self): class RandomObj(object): pass - stub = containers.LazyStub({'one': 1, 'two': 2}, target_class=RandomObj) + stub = containers.LazyStub({'one': 1, 'two': 2}, model_class=RandomObj) self.assertIn('RandomObj', repr(stub)) self.assertIn('RandomObj', str(stub)) self.assertIn('one', str(stub)) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index bccc3517..bad61049 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -46,4 +46,4 @@ class FooFactory(factory.Factory): # This is to ensure error messages are readable by end users. self.assertEqual(__file__, warning.filename) self.assertIn('FACTORY_FOR', str(warning.message)) - self.assertIn('target', str(warning.message)) + self.assertIn('model', str(warning.message)) diff --git a/tests/test_django.py b/tests/test_django.py index 2bc5fe28..84b0933a 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -100,14 +100,14 @@ def tearDownModule(): class StandardFactory(factory.django.DjangoModelFactory): class Meta: - target = models.StandardModel + model = models.StandardModel foo = factory.Sequence(lambda n: "foo%d" % n) class StandardFactoryWithPKField(factory.django.DjangoModelFactory): class Meta: - target = models.StandardModel + model = models.StandardModel django_get_or_create = ('pk',) foo = factory.Sequence(lambda n: "foo%d" % n) @@ -116,7 +116,7 @@ class Meta: class NonIntegerPkFactory(factory.django.DjangoModelFactory): class Meta: - target = models.NonIntegerPk + model = models.NonIntegerPk foo = factory.Sequence(lambda n: "foo%d" % n) bar = '' @@ -124,7 +124,7 @@ class Meta: class AbstractBaseFactory(factory.django.DjangoModelFactory): class Meta: - target = models.AbstractBase + model = models.AbstractBase abstract = True foo = factory.Sequence(lambda n: "foo%d" % n) @@ -132,22 +132,22 @@ class Meta: class ConcreteSonFactory(AbstractBaseFactory): class Meta: - target = models.ConcreteSon + model = models.ConcreteSon class AbstractSonFactory(AbstractBaseFactory): class Meta: - target = models.AbstractSon + model = models.AbstractSon class ConcreteGrandSonFactory(AbstractBaseFactory): class Meta: - target = models.ConcreteGrandSon + model = models.ConcreteGrandSon class WithFileFactory(factory.django.DjangoModelFactory): class Meta: - target = models.WithFile + model = models.WithFile if django is not None: afile = factory.django.FileField() @@ -155,7 +155,7 @@ class Meta: class WithImageFactory(factory.django.DjangoModelFactory): class Meta: - target = models.WithImage + model = models.WithImage if django is not None: animage = factory.django.ImageField() @@ -163,7 +163,7 @@ class Meta: class WithSignalsFactory(factory.django.DjangoModelFactory): class Meta: - target = models.WithSignals + model = models.WithSignals @unittest.skipIf(django is None, "Django not installed.") @@ -231,19 +231,19 @@ def test_reuse_pk(self): @unittest.skipIf(django is None, "Django not installed.") class DjangoModelLoadingTestCase(django_test.TestCase): """Tests class Meta: - target = 'app.Model' pattern.""" + model = 'app.Model' pattern.""" def test_loading(self): class ExampleFactory(factory.DjangoModelFactory): class Meta: - target = 'djapp.StandardModel' + model = 'djapp.StandardModel' - self.assertEqual(models.StandardModel, ExampleFactory._get_target_class()) + self.assertEqual(models.StandardModel, ExampleFactory._get_model_class()) def test_building(self): class ExampleFactory(factory.DjangoModelFactory): class Meta: - target = 'djapp.StandardModel' + model = 'djapp.StandardModel' e = ExampleFactory.build() self.assertEqual(models.StandardModel, e.__class__) @@ -255,7 +255,7 @@ def test_inherited_loading(self): """ class ExampleFactory(factory.DjangoModelFactory): class Meta: - target = 'djapp.StandardModel' + model = 'djapp.StandardModel' class Example2Factory(ExampleFactory): pass @@ -270,15 +270,15 @@ def test_inherited_loading_and_sequence(self): """ class ExampleFactory(factory.DjangoModelFactory): class Meta: - target = 'djapp.StandardModel' + model = 'djapp.StandardModel' foo = factory.Sequence(lambda n: n) class Example2Factory(ExampleFactory): class Meta: - target = 'djapp.StandardSon' + model = 'djapp.StandardSon' - self.assertEqual(models.StandardSon, Example2Factory._get_target_class()) + self.assertEqual(models.StandardSon, Example2Factory._get_model_class()) e1 = ExampleFactory.build() e2 = Example2Factory.build() @@ -580,7 +580,7 @@ def test_class_decorator(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): class Meta: - target = models.WithSignals + model = models.WithSignals WithSignalsDecoratedFactory() @@ -594,7 +594,7 @@ def test_class_decorator_build(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): class Meta: - target = models.WithSignals + model = models.WithSignals WithSignalsDecoratedFactory.build() diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 39594c01..988c1797 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -43,13 +43,13 @@ class Person(mongoengine.Document): class AddressFactory(MongoEngineFactory): class Meta: - target = Address + model = Address street = factory.Sequence(lambda n: 'street%d' % n) class PersonFactory(MongoEngineFactory): class Meta: - target = Person + model = Person name = factory.Sequence(lambda n: 'name%d' % n) address = factory.SubFactory(AddressFactory) diff --git a/tests/test_using.py b/tests/test_using.py index 6e7ed64a..5486d339 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -82,8 +82,8 @@ class Meta: abstract = True @classmethod - def _create(cls, target_class, *args, **kwargs): - return target_class.create(**kwargs) + def _create(cls, model_class, *args, **kwargs): + return model_class.create(**kwargs) class TestModel(FakeModel): @@ -294,18 +294,18 @@ class UsingFactoryTestCase(unittest.TestCase): def test_attribute(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 'one' test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one') - def test_inheriting_target_class(self): + def test_inheriting_model_class(self): @factory.use_strategy(factory.BUILD_STRATEGY) class TestObjectFactory(factory.Factory, TestObject): class Meta: - target = TestObject + model = TestObject one = 'one' @@ -321,7 +321,7 @@ class Meta: class InheritedFactory(SomeAbstractFactory): class Meta: - target = TestObject + model = TestObject test_object = InheritedFactory.build() self.assertEqual(test_object.one, 'one') @@ -329,7 +329,7 @@ class Meta: def test_sequence(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: 'one%d' % n) two = factory.Sequence(lambda n: 'two%d' % n) @@ -345,7 +345,7 @@ class Meta: def test_sequence_custom_begin(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @classmethod def _setup_next_sequence(cls): @@ -365,7 +365,7 @@ def _setup_next_sequence(cls): def test_sequence_override(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: 'one%d' % n) @@ -382,13 +382,13 @@ class Meta: def test_custom_create(self): class TestModelFactory(factory.Factory): class Meta: - target = TestModel + model = TestModel two = 2 @classmethod - def _create(cls, target_class, *args, **kwargs): - obj = target_class.create(**kwargs) + def _create(cls, model_class, *args, **kwargs): + obj = model_class.create(**kwargs) obj.properly_created = True return obj @@ -406,7 +406,7 @@ def __init__(self, x, y=2): class NonDjangoFactory(factory.Factory): class Meta: - target = NonDjango + model = NonDjango x = 3 @@ -417,7 +417,7 @@ class Meta: def test_sequence_batch(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: 'one%d' % n) two = factory.Sequence(lambda n: 'two%d' % n) @@ -433,7 +433,7 @@ class Meta: def test_lazy_attribute(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.LazyAttribute(lambda a: 'abc' ) two = factory.LazyAttribute(lambda a: a.one + ' xyz') @@ -445,7 +445,7 @@ class Meta: def test_lazy_attribute_sequence(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.LazyAttributeSequence(lambda a, n: 'abc%d' % n) two = factory.LazyAttributeSequence(lambda a, n: a.one + ' xyz%d' % n) @@ -461,7 +461,7 @@ class Meta: def test_lazy_attribute_decorator(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @factory.lazy_attribute def one(a): @@ -476,7 +476,7 @@ class TmpObj(object): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 'xx' two = factory.SelfAttribute('one') @@ -496,13 +496,13 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 3 three = factory.SelfAttribute('..bar') class TestModel2Factory(FakeModelFactory): class Meta: - target = TestModel2 + model = TestModel2 bar = 4 two = factory.SubFactory(TestModelFactory, one=1) @@ -512,7 +512,7 @@ class Meta: def test_sequence_decorator(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @factory.sequence def one(n): @@ -524,7 +524,7 @@ def one(n): def test_lazy_attribute_sequence_decorator(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @factory.lazy_attribute_sequence def one(a, n): @@ -540,7 +540,7 @@ def two(a, n): def test_build_with_parameters(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: 'one%d' % n) two = factory.Sequence(lambda n: 'two%d' % n) @@ -557,7 +557,7 @@ class Meta: def test_create(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -568,7 +568,7 @@ class Meta: def test_create_batch(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -585,7 +585,7 @@ class Meta: def test_generate_build(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -596,7 +596,7 @@ class Meta: def test_generate_create(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -607,7 +607,7 @@ class Meta: def test_generate_stub(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -618,7 +618,7 @@ class Meta: def test_generate_batch_build(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -635,7 +635,7 @@ class Meta: def test_generate_batch_create(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -652,7 +652,7 @@ class Meta: def test_generate_batch_stub(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -669,7 +669,7 @@ class Meta: def test_simple_generate_build(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -680,7 +680,7 @@ class Meta: def test_simple_generate_create(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -691,7 +691,7 @@ class Meta: def test_simple_generate_batch_build(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -708,7 +708,7 @@ class Meta: def test_simple_generate_batch_create(self): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 'one' @@ -725,7 +725,7 @@ class Meta: def test_stub_batch(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 'one' two = factory.LazyAttribute(lambda a: a.one + ' two') @@ -745,14 +745,14 @@ class Meta: def test_inheritance(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 'one' two = factory.LazyAttribute(lambda a: a.one + ' two') class TestObjectFactory2(TestObjectFactory): class Meta: - target = TestObject + model = TestObject three = 'three' four = factory.LazyAttribute(lambda a: a.three + ' four') @@ -770,13 +770,13 @@ def test_inheritance_and_sequences(self): """Sequence counters should be kept within an inheritance chain.""" class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: n) class TestObjectFactory2(TestObjectFactory): class Meta: - target = TestObject + model = TestObject to1a = TestObjectFactory() self.assertEqual(0, to1a.one) @@ -794,13 +794,13 @@ class TestObject2(TestObject): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: n) class TestObjectFactory2(TestObjectFactory): class Meta: - target = TestObject2 + model = TestObject2 to1a = TestObjectFactory() self.assertEqual(0, to1a.one) @@ -826,13 +826,13 @@ def __init__(self, one): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: n) class TestObjectFactory2(TestObjectFactory): class Meta: - target = TestObject2 + model = TestObject2 to1a = TestObjectFactory() self.assertEqual(0, to1a.one) @@ -847,7 +847,7 @@ class Meta: def test_inheritance_with_inherited_class(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 'one' two = factory.LazyAttribute(lambda a: a.one + ' two') @@ -865,13 +865,13 @@ class TestFactory(TestObjectFactory): def test_dual_inheritance(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 'one' class TestOtherFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject two = 'two' four = 'four' @@ -887,7 +887,7 @@ class TestFactory(TestObjectFactory, TestOtherFactory): def test_class_method_accessible(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @classmethod def alt_create(cls, **kwargs): @@ -898,7 +898,7 @@ def alt_create(cls, **kwargs): def test_static_method_accessible(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @staticmethod def alt_create(**kwargs): @@ -914,7 +914,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject arg_parameters = ('x', 'y') x = 1 @@ -934,7 +934,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject hidden_args = ('x', 'z') x = 1 @@ -954,7 +954,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject hidden_args = ('x', 'z') arg_parameters = ('y',) @@ -978,7 +978,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject arg_parameters = ('one', 'two',) one = 1 @@ -1004,7 +1004,7 @@ def create(cls, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject arg_parameters = ('one', 'two') one = 1 @@ -1012,8 +1012,8 @@ class Meta: three = 3 @classmethod - def _create(cls, target_class, *args, **kwargs): - return target_class.create(*args, **kwargs) + def _create(cls, model_class, *args, **kwargs): + return model_class.create(*args, **kwargs) obj = TestObjectFactory.create() self.assertEqual((1, 2), obj.args) @@ -1031,7 +1031,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @classmethod def _adjust_kwargs(cls, **kwargs): @@ -1050,12 +1050,12 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 3 class TestModel2Factory(FakeModelFactory): class Meta: - target = TestModel2 + model = TestModel2 two = factory.SubFactory(TestModelFactory, one=1) test_model = TestModel2Factory(two__one=4) @@ -1069,11 +1069,11 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel class TestModel2Factory(FakeModelFactory): class Meta: - target = TestModel2 + model = TestModel2 two = factory.SubFactory(TestModelFactory, one=factory.Sequence(lambda n: 'x%dx' % n), two=factory.LazyAttribute(lambda o: '%s%s' % (o.one, o.one)), @@ -1091,13 +1091,13 @@ def __init__(self, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Sequence(lambda n: int(n)) class WrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrapped = factory.SubFactory(TestObjectFactory) @@ -1114,7 +1114,7 @@ def __init__(self, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject class OtherTestObject(object): @@ -1124,7 +1124,7 @@ def __init__(self, **kwargs): class WrappingTestObjectFactory(factory.Factory): class Meta: - target = OtherTestObject + model = OtherTestObject wrapped = factory.SubFactory(TestObjectFactory, two=2, four=4) wrapped__two = 4 @@ -1145,18 +1145,18 @@ def __init__(self, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject class WrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrapped = factory.SubFactory(TestObjectFactory) wrapped_bis = factory.SubFactory(TestObjectFactory, one=1) class OuterWrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrap = factory.SubFactory(WrappingTestObjectFactory, wrapped__two=2) @@ -1174,19 +1174,19 @@ def __init__(self, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject two = 'two' class WrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrapped = factory.SubFactory(TestObjectFactory) friend = factory.LazyAttribute(lambda o: o.wrapped.two.four + 1) class OuterWrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrap = factory.SubFactory(WrappingTestObjectFactory, wrapped__two=factory.SubFactory(TestObjectFactory, four=4)) @@ -1207,13 +1207,13 @@ def __init__(self, two='one', wrapped=None): # Innermost factory class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject two = 'two' # Intermediary factory class WrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrapped = factory.SubFactory(TestObjectFactory) wrapped__two = 'three' @@ -1232,12 +1232,12 @@ def __init__(self, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject two = 'two' class WrappingTestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject wrapped = factory.SubFactory(TestObjectFactory) friend = factory.LazyAttribute(lambda o: o.wrapped.two + 1) @@ -1272,23 +1272,23 @@ def __init__(self, foo, side_a, side_b): class InnerMostFactory(factory.Factory): class Meta: - target = InnerMost + model = InnerMost a = 15 b = 20 class SideAFactory(factory.Factory): class Meta: - target = SideA + model = SideA inner_from_a = factory.SubFactory(InnerMostFactory, a=20) class SideBFactory(factory.Factory): class Meta: - target = SideB + model = SideB inner_from_b = factory.SubFactory(InnerMostFactory, b=15) class OuterMostFactory(factory.Factory): class Meta: - target = OuterMost + model = OuterMost foo = 30 side_a = factory.SubFactory(SideAFactory, @@ -1314,13 +1314,13 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 3 two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=False) class TestModel2Factory(FakeModelFactory): class Meta: - target = TestModel2 + model = TestModel2 one = 1 two = factory.SubFactory(TestModelFactory, one=1) @@ -1339,13 +1339,13 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 3 two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=True) class TestModel2Factory(FakeModelFactory): class Meta: - target = TestModel2 + model = TestModel2 one = 1 two = factory.SubFactory(TestModelFactory, one=1) @@ -1362,7 +1362,7 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: - target = TestModel + model = TestModel one = 3 @factory.container_attribute @@ -1373,7 +1373,7 @@ def two(self, containers): class TestModel2Factory(FakeModelFactory): class Meta: - target = TestModel2 + model = TestModel2 one = 1 two = factory.SubFactory(TestModelFactory, one=1) @@ -1392,7 +1392,7 @@ class IteratorTestCase(unittest.TestCase): def test_iterator(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Iterator(range(10, 30)) @@ -1406,7 +1406,7 @@ class Meta: def test_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Iterator([j * 3 for j in range(5)]) @@ -1418,7 +1418,7 @@ class Meta: def test_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Iterator([_j * 3 for _j in range(5)]) @@ -1432,7 +1432,7 @@ class Meta: def test_iterator_decorator(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject @factory.iterator def one(): @@ -1483,7 +1483,7 @@ class DjangoModelFactoryTestCase(unittest.TestCase): def test_simple(self): class FakeModelFactory(factory.django.DjangoModelFactory): class Meta: - target = FakeModel + model = FakeModel obj = FakeModelFactory(one=1) self.assertEqual(1, obj.one) @@ -1498,7 +1498,7 @@ class MyFakeModel(BetterFakeModel): class MyFakeModelFactory(factory.django.DjangoModelFactory): class Meta: - target = MyFakeModel + model = MyFakeModel django_get_or_create = ('x',) x = 1 y = 4 @@ -1520,7 +1520,7 @@ class MyFakeModel(BetterFakeModel): class MyFakeModelFactory(factory.django.DjangoModelFactory): class Meta: - target = MyFakeModel + model = MyFakeModel django_get_or_create = ('x', 'y', 'z') x = 1 y = 4 @@ -1542,7 +1542,7 @@ class MyFakeModel(BetterFakeModel): class MyFakeModelFactory(factory.django.DjangoModelFactory): class Meta: - target = MyFakeModel + model = MyFakeModel django_get_or_create = ('x',) x = 1 y = 4 @@ -1564,7 +1564,7 @@ class MyFakeModel(BetterFakeModel): class MyFakeModelFactory(factory.django.DjangoModelFactory): class Meta: - target = MyFakeModel + model = MyFakeModel django_get_or_create = ('x', 'y', 'z') x = 1 y = 4 @@ -1580,7 +1580,7 @@ class Meta: def test_sequence(self): class TestModelFactory(factory.django.DjangoModelFactory): class Meta: - target = TestModel + model = TestModel a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1599,7 +1599,7 @@ class Meta: def test_no_get_or_create(self): class TestModelFactory(factory.django.DjangoModelFactory): class Meta: - target = TestModel + model = TestModel a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1611,7 +1611,7 @@ class Meta: def test_get_or_create(self): class TestModelFactory(factory.django.DjangoModelFactory): class Meta: - target = TestModel + model = TestModel django_get_or_create = ('a', 'b') a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1631,7 +1631,7 @@ def test_full_get_or_create(self): """Test a DjangoModelFactory with all fields in get_or_create.""" class TestModelFactory(factory.django.DjangoModelFactory): class Meta: - target = TestModel + model = TestModel django_get_or_create = ('a', 'b', 'c', 'd') a = factory.Sequence(lambda n: 'foo_%s' % n) @@ -1652,7 +1652,7 @@ class PostGenerationTestCase(unittest.TestCase): def test_post_generation(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 1 @@ -1671,7 +1671,7 @@ def incr_one(self, _create, _increment): def test_post_generation_hook(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 1 @@ -1693,7 +1693,7 @@ def _after_postgeneration(cls, obj, create, results): def test_post_generation_extraction(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 1 @@ -1719,7 +1719,7 @@ def my_lambda(obj, create, extracted, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject bar = factory.PostGeneration(my_lambda) @@ -1739,7 +1739,7 @@ def call(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 3 two = 2 post_call = factory.PostGenerationMethodCall('call', one=1) @@ -1764,13 +1764,13 @@ def __init__(self, obj=None, one=None, two=None): class TestRelatedObjectFactory(factory.Factory): class Meta: - target = TestRelatedObject + model = TestRelatedObject one = 1 two = factory.LazyAttribute(lambda o: o.one + 1) class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 3 two = 2 three = factory.RelatedFactory(TestRelatedObjectFactory, name='obj') @@ -1811,13 +1811,13 @@ def __init__(self, obj=None, one=None, two=None): class TestRelatedObjectFactory(factory.Factory): class Meta: - target = TestRelatedObject + model = TestRelatedObject one = 1 two = factory.LazyAttribute(lambda o: o.one + 1) class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 3 two = 2 three = factory.RelatedFactory(TestRelatedObjectFactory) @@ -1859,11 +1859,11 @@ def __init__(subself, obj): class TestRelatedObjectFactory(factory.Factory): class Meta: - target = TestRelatedObject + model = TestRelatedObject class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') self.TestRelatedObject = TestRelatedObject @@ -1911,7 +1911,7 @@ class DictTestCase(unittest.TestCase): def test_empty_dict(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Dict({}) o = TestObjectFactory() @@ -1920,7 +1920,7 @@ class Meta: def test_naive_dict(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Dict({'a': 1}) o = TestObjectFactory() @@ -1929,7 +1929,7 @@ class Meta: def test_sequence_dict(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Dict({'a': factory.Sequence(lambda n: n + 2)}) o1 = TestObjectFactory() @@ -1941,7 +1941,7 @@ class Meta: def test_dict_override(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Dict({'a': 1}) o = TestObjectFactory(one__a=2) @@ -1950,7 +1950,7 @@ class Meta: def test_dict_extra_key(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.Dict({'a': 1}) o = TestObjectFactory(one__b=2) @@ -1959,7 +1959,7 @@ class Meta: def test_dict_merged_fields(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject two = 13 one = factory.Dict({ 'one': 1, @@ -1973,7 +1973,7 @@ class Meta: def test_nested_dicts(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 1 two = factory.Dict({ 'one': 3, @@ -2002,7 +2002,7 @@ class ListTestCase(unittest.TestCase): def test_empty_list(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.List([]) o = TestObjectFactory() @@ -2011,7 +2011,7 @@ class Meta: def test_naive_list(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.List([1]) o = TestObjectFactory() @@ -2020,7 +2020,7 @@ class Meta: def test_sequence_list(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.List([factory.Sequence(lambda n: n + 2)]) o1 = TestObjectFactory() @@ -2032,7 +2032,7 @@ class Meta: def test_list_override(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.List([1]) o = TestObjectFactory(one__0=2) @@ -2041,7 +2041,7 @@ class Meta: def test_list_extra_key(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = factory.List([1]) o = TestObjectFactory(one__1=2) @@ -2050,7 +2050,7 @@ class Meta: def test_list_merged_fields(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject two = 13 one = factory.List([ 1, @@ -2064,7 +2064,7 @@ class Meta: def test_nested_lists(self): class TestObjectFactory(factory.Factory): class Meta: - target = TestObject + model = TestObject one = 1 two = factory.List([ From f2d04144167120dc8820401940172d10fdda007b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 15:15:45 +0200 Subject: [PATCH 042/714] Rename hidden/arg_parameters to exclude/inline_args. --- docs/introduction.rst | 4 ++-- docs/reference.rst | 18 +++++++++--------- factory/base.py | 16 ++++++++-------- tests/test_base.py | 4 ++-- tests/test_using.py | 18 +++++++++--------- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index 5e3b4d82..d00154dc 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -218,14 +218,14 @@ Non-kwarg arguments Some classes take a few, non-kwarg arguments first. -This is handled by the :data:`~factory.FactoryOptions.arg_parameters` attribute: +This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute: .. code-block:: python class MyFactory(factory.Factory): class Meta: model = MyClass - arg_parameters = ('x', 'y') + inline_args = ('x', 'y') x = 1 y = 2 diff --git a/docs/reference.rst b/docs/reference.rst index d616d1c4..25fef220 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -47,10 +47,10 @@ The :class:`Factory` class .. versionadded:: 2.4.0 - .. attribute:: arg_parameters + .. attribute:: inline_args Some factories require non-keyword arguments to their :meth:`~object.__init__`. - They should be listed, in order, in the :attr:`arg_parameters` + They should be listed, in order, in the :attr:`inline_args` attribute: .. code-block:: python @@ -58,7 +58,7 @@ The :class:`Factory` class class UserFactory(factory.Factory): class Meta: model = User - arg_parameters = ('login', 'email') + inline_args = ('login', 'email') login = 'john' email = factory.LazyAttribute(lambda o: '%s@example.com' % o.login) @@ -72,14 +72,14 @@ The :class:`Factory` class .. versionadded:: 2.4.0 - .. attribute:: hidden_args + .. attribute:: exclude While writing a :class:`Factory` for some object, it may be useful to have general fields helping defining others, but that should not be passed to the model class; for instance, a field named 'now' that would hold a reference time used by other objects. - Factory fields whose name are listed in :attr:`hidden_args` will + Factory fields whose name are listed in :attr:`exclude` will be removed from the set of args/kwargs passed to the underlying class; they can be any valid factory_boy declaration: @@ -88,7 +88,7 @@ The :class:`Factory` class class OrderFactory(factory.Factory): class Meta: model = Order - hidden_args = ('now',) + exclude = ('now',) now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow()) started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1)) @@ -125,12 +125,12 @@ The :class:`Factory` class .. attribute:: FACTORY_ARG_PARAMETERS .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.arg_parameters`. + See :attr:`FactoryOptions.inline_args`. .. attribute:: FACTORY_HIDDEN_ARGS .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.hidden_args`. + See :attr:`FactoryOptions.exclude`. **Class-level attributes:** @@ -231,7 +231,7 @@ The :class:`Factory` class The :meth:`_adjust_kwargs` extension point allows for late fields tuning. It is called once keyword arguments have been resolved and post-generation - items removed, but before the :attr:`~FactoryOptions.arg_parameters` extraction + items removed, but before the :attr:`~FactoryOptions.inline_args` extraction phase. .. code-block:: python diff --git a/factory/base.py b/factory/base.py index e5c31f73..923c56ba 100644 --- a/factory/base.py +++ b/factory/base.py @@ -192,8 +192,8 @@ def _build_default_options(self): OptionDefault('model', None, inherit=True), OptionDefault('abstract', False, inherit=False), OptionDefault('strategy', CREATE_STRATEGY, inherit=True), - OptionDefault('arg_parameters', (), inherit=True), - OptionDefault('hidden_args', (), inherit=True), + OptionDefault('inline_args', (), inherit=True), + OptionDefault('exclude', (), inherit=True), ] def _fill_from_meta(self, meta, base_meta): @@ -326,8 +326,8 @@ def __new__(cls, *args, **kwargs): 'FACTORY_FOR': 'model', 'ABSTRACT_FACTORY': 'abstract', 'FACTORY_STRATEGY': 'strategy', - 'FACTORY_ARG_PARAMETERS': 'arg_parameters', - 'FACTORY_HIDDEN_ARGS': 'hidden_args', + 'FACTORY_ARG_PARAMETERS': 'inline_args', + 'FACTORY_HIDDEN_ARGS': 'exclude', } # ID to use for the next 'declarations.Sequence' attribute. @@ -470,11 +470,11 @@ def _prepare(cls, create, **kwargs): kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. - for arg in cls._meta.hidden_args: + for arg in cls._meta.exclude: del kwargs[arg] # Extract *args from **kwargs - args = tuple(kwargs.pop(key) for key in cls._meta.arg_parameters) + args = tuple(kwargs.pop(key) for key in cls._meta.inline_args) logger.debug('BaseFactory: Generating %s.%s(%s)', cls.__module__, @@ -725,7 +725,7 @@ class Meta: def _build(cls, model_class, *args, **kwargs): if args: raise ValueError( - "DictFactory %r does not support Meta.arg_parameters.", cls) + "DictFactory %r does not support Meta.inline_args.", cls) return model_class(**kwargs) @classmethod @@ -747,7 +747,7 @@ class Meta: def _build(cls, model_class, *args, **kwargs): if args: raise ValueError( - "ListFactory %r does not support Meta.arg_parameters.", cls) + "ListFactory %r does not support Meta.inline_args.", cls) values = [v for k, v in sorted(kwargs.items())] return model_class(values) diff --git a/tests/test_base.py b/tests/test_base.py index d93bf294..d1df58e1 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -122,8 +122,8 @@ class AbstractFactory(base.Factory): # Declarative attributes self.assertTrue(AbstractFactory._meta.abstract) self.assertIsNone(AbstractFactory._meta.model) - self.assertEqual((), AbstractFactory._meta.arg_parameters) - self.assertEqual((), AbstractFactory._meta.hidden_args) + self.assertEqual((), AbstractFactory._meta.inline_args) + self.assertEqual((), AbstractFactory._meta.exclude) self.assertEqual(base.CREATE_STRATEGY, AbstractFactory._meta.strategy) # Non-declarative attributes diff --git a/tests/test_using.py b/tests/test_using.py index 5486d339..e20f9494 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -906,7 +906,7 @@ def alt_create(**kwargs): self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1}) - def test_arg_parameters(self): + def test_inline_args(self): class TestObject(object): def __init__(self, *args, **kwargs): self.args = args @@ -915,7 +915,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: model = TestObject - arg_parameters = ('x', 'y') + inline_args = ('x', 'y') x = 1 y = 2 @@ -926,7 +926,7 @@ class Meta: self.assertEqual((42, 2), obj.args) self.assertEqual({'z': 5, 't': 4}, obj.kwargs) - def test_hidden_args(self): + def test_exclude(self): class TestObject(object): def __init__(self, *args, **kwargs): self.args = args @@ -935,7 +935,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: model = TestObject - hidden_args = ('x', 'z') + exclude = ('x', 'z') x = 1 y = 2 @@ -946,7 +946,7 @@ class Meta: self.assertEqual((), obj.args) self.assertEqual({'y': 2, 't': 4}, obj.kwargs) - def test_hidden_args_and_arg_parameters(self): + def test_exclude_and_inline_args(self): class TestObject(object): def __init__(self, *args, **kwargs): self.args = args @@ -955,8 +955,8 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: model = TestObject - hidden_args = ('x', 'z') - arg_parameters = ('y',) + exclude = ('x', 'z') + inline_args = ('y',) x = 1 y = 2 @@ -979,7 +979,7 @@ def __init__(self, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: model = TestObject - arg_parameters = ('one', 'two',) + inline_args = ('one', 'two',) one = 1 two = 2 @@ -1005,7 +1005,7 @@ def create(cls, *args, **kwargs): class TestObjectFactory(factory.Factory): class Meta: model = TestObject - arg_parameters = ('one', 'two') + inline_args = ('one', 'two') one = 1 two = 2 From ecde9a5c2f5d7ea230e68e6d0b3586e5956e55c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 15:17:52 +0200 Subject: [PATCH 043/714] Update ideas. --- docs/ideas.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ideas.rst b/docs/ideas.rst index 914e640f..f3c9e623 100644 --- a/docs/ideas.rst +++ b/docs/ideas.rst @@ -4,5 +4,6 @@ Ideas This is a list of future features that may be incorporated into factory_boy: -* **A 'options' attribute**: instead of adding more class-level constants, use a django-style ``class Meta`` Factory attribute with all options there +* When a :class:`Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere +* Define a proper set of rules for the support of third-party ORMs From 3d7a00d1c5cafe37e49a716c8ae075e984b3111a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 15:23:54 +0200 Subject: [PATCH 044/714] Improve docs on create_batch (Closes #139). --- README.rst | 10 ++++++++++ docs/examples.rst | 12 ++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index b4ba6895..0ba4a86e 100644 --- a/README.rst +++ b/README.rst @@ -155,6 +155,16 @@ No matter which strategy is used, it's possible to override the defined attribut "Joe" +It is also possible to create a bunch of objects in a single call: + +.. code-block:: pycon + + >>> users = USerFactory.build(10, first_name="Joe") + >>> len(users) + 10 + >>> [user.first_name for user in users] + ["Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe"] + Lazy Attributes """"""""""""""" diff --git a/docs/examples.rst b/docs/examples.rst index a57080e0..ee521e35 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -114,12 +114,9 @@ We can now use our factories, for tests: def test_get_profile_stats(self): profiles = [] - for _ in xrange(4): - profiles.append(factories.ProfileFactory()) - for _ in xrange(2): - profiles.append(factories.FemaleProfileFactory()) - for _ in xrange(2): - profiles.append(factories.ProfileFactory(planet='Tatooine')) + profiles.extend(factories.ProfileFactory.batch_create(4)) + profiles.extend(factories.FemaleProfileFactory.batch_create(2)) + profiles.extend(factories.ProfileFactory.batch_create(2, planet="Tatooine")) stats = business_logic.profile_stats(profiles) self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets) @@ -133,8 +130,7 @@ Or for fixtures: from . import factories def make_objects(): - for _ in xrange(50): - factories.ProfileFactory() + factories.ProfileFactory.batch_create(size=50) # Let's create a few, known objects. factories.ProfileFactory( From 46ae739823bc9d9c3a176f20058d65097c22cb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 May 2014 15:25:20 +0200 Subject: [PATCH 045/714] Fix test_deprecation test. --- tests/test_deprecation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index bad61049..a07cbf31 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -44,6 +44,6 @@ class FooFactory(factory.Factory): warning = w[0] # Message is indeed related to the current file # This is to ensure error messages are readable by end users. - self.assertEqual(__file__, warning.filename) + self.assertIn(warning.filename, __file__) self.assertIn('FACTORY_FOR', str(warning.message)) self.assertIn('model', str(warning.message)) From 790e8ea2db7a68d7074809124dfd82c3b05b925b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 21 May 2014 14:01:41 +0200 Subject: [PATCH 046/714] Introduce back missing doc about migrating FACTROY_DJANGO_GET_OR_CREATE. --- docs/orms.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index d3d98c94..6a942a6a 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -40,6 +40,11 @@ All factories for a Django :class:`~django.db.models.Model` should use the attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. + .. attribute:: FACTORY_DJANGO_GET_OR_CREATE + + .. deprecated:: 2.4.0 + See :attr:`DjangoOptions.django_get_or_create`. + .. class:: DjangoOptions(factory.base.FactoryOptions) @@ -47,6 +52,8 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. attribute:: django_get_or_create + .. versionadded:: 2.4.0 + Fields whose name are passed in this list will be used to perform a :meth:`Model.objects.get_or_create() ` instead of the usual :meth:`Model.objects.create() `: From ad056787937844809b48dd36311dac0f8bd4c0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 31 May 2014 14:06:45 +0200 Subject: [PATCH 047/714] doc: Document upgrade path for FACTORY_*. --- docs/changelog.rst | 19 +++++++++++++++++++ docs/orms.rst | 10 ++++++++-- docs/reference.rst | 12 ++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e5d76c71..b84ebc3c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,25 @@ ChangeLog *Deprecation:* - Use of ``FACTORY_FOR`` and other ``FACTORY`` class-level attributes is deprecated and will be removed in 2.5. + Those attributes should now declared within the :class:`class Meta ` attribute: + + For :class:`factory.Factory`: + + * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` + + * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` + * Rename :attr:`~factory.Factory.ABSTRACT_FACTORY` to :attr:`~factory.FactoryOptions.abstract` + * Rename :attr:`~factory.Factory.FACTORY_STRATEGY` to :attr:`~factory.FactoryOptions.strategy` + * Rename :attr:`~factory.Factory.FACTORY_ARG_PARAMETERS` to :attr:`~factory.FactoryOptions.inline_args` + * Rename :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` to :attr:`~factory.FactoryOptions.exclude` + + For :class:`factory.django.DjangoModelFactory`: + + * Rename :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` to :attr:`~factory.django.DjangoOptions.django_get_or_create` + + For :class:`factory.alchemy.SQLAlchemyModelFactory`: + + * Rename :attr:`~factory.alchemy.SQLAlchemyModelFactory.FACTORY_SESSION` to :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session` .. _v2.3.1: diff --git a/docs/orms.rst b/docs/orms.rst index 6a942a6a..2aa27b2a 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -273,7 +273,7 @@ SQLAlchemy Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class. -To work, this class needs an `SQLAlchemy`_ session object affected to the ``Meta.sqlalchemy_session`` attribute. +To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:`Meta.sqlalchemy_session ` attribute. .. _SQLAlchemy: http://www.sqlalchemy.org/ @@ -286,6 +286,11 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the ``Meta * :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 + + .. deprecated:: 2.4.0 + See :attr:`~SQLAlchemyOptions.sqlalchemy_session`. + .. class:: SQLAlchemyOptions(factory.base.FactoryOptions) In addition to the usual parameters available in :class:`class Meta `, @@ -293,7 +298,8 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the ``Meta .. attribute:: sqlalchemy_session - Fields whose SQLAlchemy session object are passed will be used to communicate with the database + SQLAlchemy session to use to communicate with the database when creating + an object through this :class:`SQLAlchemyModelFactory`. A (very) simple exemple: diff --git a/docs/reference.rst b/docs/reference.rst index 25fef220..b0dda504 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -106,6 +106,13 @@ The :class:`Factory` class .. versionadded:: 2.4.0 + .. attribute:: strategy + + Use this attribute to change the strategy used by a :class:`Factory`. + The default is :data:`BUILD_STRATEGY`. + + + .. class:: Factory .. note:: In previous versions, the fields of :class:`class Meta ` were @@ -132,6 +139,11 @@ The :class:`Factory` class .. deprecated:: 2.4.0 See :attr:`FactoryOptions.exclude`. + .. attribute:: FACTORY_STRATEGY + + .. deprecated:: 2.4.0 + See :attr:`FactoryOptions.strategy`. + **Class-level attributes:** From d9b49c72395a82d356fc2704c9a66047f20fe983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 Jun 2014 13:32:04 +0200 Subject: [PATCH 048/714] Provide readable errors when Meta.model isn't set (Closes #137). --- factory/__init__.py | 2 ++ factory/django.py | 3 +++ tests/test_django.py | 9 +++++++++ 3 files changed, 14 insertions(+) diff --git a/factory/__init__.py b/factory/__init__.py index aa550e8d..ca1571ab 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -32,6 +32,8 @@ ListFactory, StubFactory, + FactoryError, + BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, diff --git a/factory/django.py b/factory/django.py index 60901453..2b6c463e 100644 --- a/factory/django.py +++ b/factory/django.py @@ -101,6 +101,9 @@ def _load_model_class(cls, definition): @classmethod def _get_manager(cls, model_class): + if model_class is None: + raise base.AssociatedClassError("No model set on %s.%s.Meta" + % (cls.__module__, cls.__name__)) try: return model_class._default_manager # pylint: disable=W0212 except AttributeError: diff --git a/tests/test_django.py b/tests/test_django.py index 84b0933a..41a26cff 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -166,6 +166,15 @@ class Meta: model = models.WithSignals +@unittest.skipIf(django is None, "Django not installed.") +class ModelTests(django_test.TestCase): + def test_unset_model(self): + class UnsetModelFactory(factory.django.DjangoModelFactory): + pass + + self.assertRaises(factory.FactoryError, UnsetModelFactory.create) + + @unittest.skipIf(django is None, "Django not installed.") class DjangoPkSequenceTestCase(django_test.TestCase): def setUp(self): From 52a66af1d9ca679c8c9653e254253b172d8ee1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 Jun 2014 13:35:13 +0200 Subject: [PATCH 049/714] Release v2.4.0 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b84ebc3c..561ce18b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.4.0: -2.4.0 (master) --------------- +2.4.0 (2014-06-21) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index ca1571ab..1f06fba7 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.1' +__version__ = '2.4.0' __author__ = 'Raphaël Barrois ' From 6269fef31787aba4956612febfa3f4944fda947b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 Jun 2014 13:36:52 +0200 Subject: [PATCH 050/714] Fix typo (Closes #144). Thanks to @clouserw for the report. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 0ba4a86e..32b93bdf 100644 --- a/README.rst +++ b/README.rst @@ -159,7 +159,7 @@ It is also possible to create a bunch of objects in a single call: .. code-block:: pycon - >>> users = USerFactory.build(10, first_name="Joe") + >>> users = UserFactory.build(10, first_name="Joe") >>> len(users) 10 >>> [user.first_name for user in users] From e2ef7c96ed74b35b9dec75a7f222b6ffa9214c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 23 Jun 2014 11:09:53 +0200 Subject: [PATCH 051/714] Fix declaration inheritance. --- factory/base.py | 2 +- tests/test_using.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/factory/base.py b/factory/base.py index 923c56ba..9e07899a 100644 --- a/factory/base.py +++ b/factory/base.py @@ -231,7 +231,7 @@ def contribute_to_class(self, factory, self.counter_reference = self._get_counter_reference() - for parent in self.factory.__mro__[1:]: + for parent in reversed(self.factory.__mro__[1:]): if not hasattr(parent, '_meta'): continue self.declarations.update(parent._meta.declarations) diff --git a/tests/test_using.py b/tests/test_using.py index e20f9494..f18df4d6 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -766,6 +766,37 @@ class Meta: test_object_alt = TestObjectFactory.build() self.assertEqual(None, test_object_alt.three) + def test_override_inherited(self): + """Overriding inherited declarations""" + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = 'one' + + class TestObjectFactory2(TestObjectFactory): + one = 'two' + + test_object = TestObjectFactory2.build() + self.assertEqual('two', test_object.one) + + def test_override_inherited_deep(self): + """Overriding inherited declarations""" + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = 'one' + + class TestObjectFactory2(TestObjectFactory): + one = 'two' + + class TestObjectFactory3(TestObjectFactory2): + pass + + test_object = TestObjectFactory3.build() + self.assertEqual('two', test_object.one) + def test_inheritance_and_sequences(self): """Sequence counters should be kept within an inheritance chain.""" class TestObjectFactory(factory.Factory): From 87f8cc0cc0d2f48f489c81b8c93e8ab6de6cff26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 23 Jun 2014 11:11:13 +0200 Subject: [PATCH 052/714] Release v2.4.1 --- docs/changelog.rst | 9 +++++++++ factory/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 561ce18b..7d77f7f5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,15 @@ ChangeLog ========= +.. _v2.4.1: + +2.4.1 (2014-06-23) +------------------ + +*Bugfix:* + + - Fix overriding deeply inherited attributes (set in one factory, overridden in a subclass, used in a sub-sub-class). + .. _v2.4.0: 2.4.0 (2014-06-21) diff --git a/factory/__init__.py b/factory/__init__.py index 1f06fba7..8fc8ef80 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.4.0' +__version__ = '2.4.1' __author__ = 'Raphaël Barrois ' From 82988e154391cc37f81eb398bbfa91f30f524349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 21 Aug 2014 09:28:51 +0200 Subject: [PATCH 053/714] tests: Update to Django new 'duplicate file' mechanism. --- tests/test_django.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 41a26cff..fd9c8766 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -430,7 +430,8 @@ def test_existing_file(self): o2 = WithFileFactory.build(afile=o1.afile) self.assertIsNone(o2.pk) self.assertEqual(b'example_data\n', o2.afile.read()) - self.assertEqual('django/example_1.data', o2.afile.name) + self.assertNotEqual('django/example.data', o2.afile.name) + self.assertRegexpMatches(o2.afile.name, r'django/example_\w+.data') def test_no_file(self): o = WithFileFactory.build(afile=None) @@ -547,7 +548,8 @@ def test_existing_file(self): self.assertIsNone(o2.pk) # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o2.animage.read())) - self.assertEqual('django/example_1.jpeg', o2.animage.name) + self.assertNotEqual('django/example.jpeg', o2.animage.name) + self.assertRegexpMatches(o2.animage.name, r'django/example_\w+.jpeg') def test_no_file(self): o = WithImageFactory.build(animage=None) From 1a00eef263f787a9f3d98fbaa43ec30ad9ac4071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 22:53:02 +0200 Subject: [PATCH 054/714] Fix test running without django (Closes #161). --- tests/test_django.py | 101 +++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index fd9c8766..95e02562 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -57,21 +57,9 @@ from django.test import utils as django_test_utils from django.db.models import signals from .djapp import models -else: # pragma: no cover - django_test = unittest - - class Fake(object): - pass - models = Fake() - models.StandardModel = Fake - models.StandardSon = None - models.AbstractBase = Fake - models.ConcreteSon = Fake - models.NonIntegerPk = Fake - models.WithFile = Fake - models.WithImage = Fake - models.WithSignals = Fake +else: + django_test = unittest test_state = {} @@ -98,72 +86,73 @@ def tearDownModule(): django_test_utils.teardown_test_environment() -class StandardFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.StandardModel +if django is not None: + class StandardFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) -class StandardFactoryWithPKField(factory.django.DjangoModelFactory): - class Meta: - model = models.StandardModel - django_get_or_create = ('pk',) + class StandardFactoryWithPKField(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + django_get_or_create = ('pk',) - foo = factory.Sequence(lambda n: "foo%d" % n) - pk = None + foo = factory.Sequence(lambda n: "foo%d" % n) + pk = None -class NonIntegerPkFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.NonIntegerPk + class NonIntegerPkFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.NonIntegerPk - foo = factory.Sequence(lambda n: "foo%d" % n) - bar = '' + foo = factory.Sequence(lambda n: "foo%d" % n) + bar = '' -class AbstractBaseFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.AbstractBase - abstract = True + class AbstractBaseFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.AbstractBase + abstract = True - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) -class ConcreteSonFactory(AbstractBaseFactory): - class Meta: - model = models.ConcreteSon + class ConcreteSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteSon -class AbstractSonFactory(AbstractBaseFactory): - class Meta: - model = models.AbstractSon + class AbstractSonFactory(AbstractBaseFactory): + class Meta: + model = models.AbstractSon -class ConcreteGrandSonFactory(AbstractBaseFactory): - class Meta: - model = models.ConcreteGrandSon + class ConcreteGrandSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteGrandSon -class WithFileFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithFile + class WithFileFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithFile - if django is not None: - afile = factory.django.FileField() + if django is not None: + afile = factory.django.FileField() -class WithImageFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithImage + class WithImageFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithImage - if django is not None: - animage = factory.django.ImageField() + if django is not None: + animage = factory.django.ImageField() -class WithSignalsFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithSignals + class WithSignalsFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithSignals @unittest.skipIf(django is None, "Django not installed.") From 251c9ef081bdb9233b4885d23501afc3b26324c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 22:54:26 +0200 Subject: [PATCH 055/714] Fix typo in docs (Closes #157). --- docs/examples.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index ee521e35..e7f60577 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -114,9 +114,9 @@ We can now use our factories, for tests: def test_get_profile_stats(self): profiles = [] - profiles.extend(factories.ProfileFactory.batch_create(4)) - profiles.extend(factories.FemaleProfileFactory.batch_create(2)) - profiles.extend(factories.ProfileFactory.batch_create(2, planet="Tatooine")) + profiles.extend(factories.ProfileFactory.create_batch(4)) + profiles.extend(factories.FemaleProfileFactory.create_batch(2)) + profiles.extend(factories.ProfileFactory.create_batch(2, planet="Tatooine")) stats = business_logic.profile_stats(profiles) self.assertEqual({'Earth': 6, 'Mars': 2}, stats.planets) @@ -130,7 +130,7 @@ Or for fixtures: from . import factories def make_objects(): - factories.ProfileFactory.batch_create(size=50) + factories.ProfileFactory.create_batch(size=50) # Let's create a few, known objects. factories.ProfileFactory( From 5e4edb44daf54a4c34703319bdb9467ca7b3de8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 23:47:14 +0200 Subject: [PATCH 056/714] Django1.7 is out, let's not test it on Python2.6 --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2bfb978e..eceada3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,8 @@ script: install: - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - pip install Django sqlalchemy --use-mirrors + - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install Django<1.7 --use-mirrors; else pip install Django; fi + - pip install sqlalchemy --use-mirrors - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi notifications: From 25bd44c30007d5babecefed651827431569ee1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 23:49:47 +0200 Subject: [PATCH 057/714] Fix support for Django 1.7. --- tests/djapp/settings.py | 1 + tests/test_django.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index c1b79b0a..c051faf8 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -41,5 +41,6 @@ 'tests.djapp' ] +MIDDLEWARE_CLASSES = () SECRET_KEY = 'testing.' diff --git a/tests/test_django.py b/tests/test_django.py index 95e02562..874c2729 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -61,6 +61,8 @@ else: django_test = unittest +if django is not None and django.VERSION >= (1, 7, 0): + django.setup() test_state = {} From f81aba229a7492b11a331407f60fba1bd4d6522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Sep 2014 23:50:50 +0200 Subject: [PATCH 058/714] Fix travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eceada3e..3954bf50 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ script: install: - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install Django<1.7 --use-mirrors; else pip install Django; fi + - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install "Django<1.7" --use-mirrors; else pip install Django; fi - pip install sqlalchemy --use-mirrors - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi From 70412551c545e94b27bef468cd248fce7a9cdd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 2 Nov 2014 16:32:16 +0100 Subject: [PATCH 059/714] Add tests for self-referential models (See #173). --- tests/cyclic/self_ref.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_using.py | 17 +++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 tests/cyclic/self_ref.py diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py new file mode 100644 index 00000000..f18c9892 --- /dev/null +++ b/tests/cyclic/self_ref.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +"""Helper to test circular factory dependencies.""" + +import factory + +class TreeElement(object): + def __init__(self, name, parent): + self.parent = parent + self.name = name + + +class TreeElementFactory(factory.Factory): + class Meta: + model = TreeElement + + name = factory.Sequence(lambda n: "tree%s" % n) + parent = factory.SubFactory('tests.cyclic.self_ref.TreeElementFactory') diff --git a/tests/test_using.py b/tests/test_using.py index f18df4d6..8d787895 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1938,6 +1938,23 @@ def test_example(self): self.assertIsNone(b.foo.bar.foo.bar) +class SelfReferentialTests(unittest.TestCase): + def test_no_parent(self): + from .cyclic import self_ref + + obj = self_ref.TreeElementFactory(parent=None) + self.assertIsNone(obj.parent) + + def test_deep(self): + from .cyclic import self_ref + + obj = self_ref.TreeElementFactory(parent__parent__parent__parent=None) + self.assertIsNotNone(obj.parent) + self.assertIsNotNone(obj.parent.parent) + self.assertIsNotNone(obj.parent.parent.parent) + self.assertIsNone(obj.parent.parent.parent.parent) + + class DictTestCase(unittest.TestCase): def test_empty_dict(self): class TestObjectFactory(factory.Factory): From 827af8f13a1b768a75264874c73cc0e620177262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 16 Nov 2014 21:17:11 +0100 Subject: [PATCH 060/714] Add docs for manual sequence counter management --- docs/recipes.rst | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index 72dacefd..70eca46b 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -327,3 +327,97 @@ default :meth:`Model.objects.create() ` manager = cls._get_manager(model_class) # The default would use ``manager.create(*args, **kwargs)`` return manager.create_user(*args, **kwargs) + + +Forcing the sequence counter +---------------------------- + +A common pattern with factory_boy is to use a :class:`factory.Sequence` declaration +to provide varying values to attributes declared as unique. + +However, it is sometimes useful to force a given value to the counter, for instance +to ensure that tests are properly reproductible. + +factory_boy provides a few hooks for this: + + +Forcing the value on a per-call basis + In order to force the counter for a specific :class:`~factory.Factory` instantiation, + just pass the value in the ``__sequence=42`` parameter: + + .. code-block:: python + + class AccountFactory(factory.Factory): + class Meta: + model = Account + uid = factory.Sequence(lambda n: n) + name = "Test" + + .. code-block:: pycon + + >>> obj1 = AccountFactory(name="John Doe", __sequence=10) + >>> obj1.uid # Taken from the __sequence counter + 10 + >>> obj2 = AccountFactory(name="Jane Doe") + >>> obj2.uid # The base sequence counter hasn't changed + 1 + + +Resetting the counter globally + If all calls for a factory must start from a deterministic number, + use :meth:`factory.Factory.reset_sequence`; this will reset the counter + to its initial value (as defined by :meth:`factory.Factory._setup_next_sequence`). + + .. code-block:: pycon + + >>> AccountFactory().uid + 1 + >>> AccountFactory().uid + 2 + >>> AccountFactory.reset_sequence() + >>> AccountFactory().uid # Reset to the initial value + 1 + >>> AccountFactory().uid + 2 + + It is also possible to reset the counter to a specific value: + + .. code-block:: pycon + + >>> AccountFactory.reset_sequence(10) + >>> AccountFactory().uid + 10 + >>> AccountFactory().uid + 11 + + This recipe is most useful in a :class:`~unittest.TestCase`'s + :meth:`~unittest.TestCase.setUp` method. + + +Forcing the initial value for all projects + The sequence counter of a :class:`~factory.Factory` can also be set + automatically upon the first call through the + :meth:`~factory.Factory._setup_next_sequence` method; this helps when the + objects's attributes mustn't conflict with pre-existing data. + + A typical example is to ensure that running a Python script twice will create + non-conflicting objects, by setting up the counter to "max used value plus one": + + .. code-block:: python + + class AccountFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.Account + + @classmethod + def _setup_next_sequence(cls): + try: + return models.Accounts.objects.latest('uid').uid + 1 + except models.Account.DoesNotExist: + return 1 + + .. code-block:: pycon + + >>> Account.objects.create(uid=42, name="Blah") + >>> AccountFactory.create() # Sets up the account number based on the latest uid + From 13d310fa14f4e4b9a559f8b7887f2a2492357013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 16 Nov 2014 22:34:29 +0100 Subject: [PATCH 061/714] Remove automagic pk-based sequence setup Related to issues #78, #92, #103, #111, #153, #170 The default value of all sequences is now 0; the automagic ``_setup_next_sequence`` behavior of Django/SQLAlchemy has been removed. This feature's only goal was to allow the following scenario: 1. Run a Python script that uses MyFactory.create() a couple of times (with a unique field based on the sequence counter) 2. Run the same Python script a second time Without the magical ``_setup_next_sequence``, the Sequence counter would be set to 0 at the beginning of each script run, so both runs would generate objects with the same values for the unique field ; thus conflicting and crashing. The above behavior having only a very limited use and bringing various issues (hitting the database on ``build()``, problems with non-integer or composite primary key columns, ...), it has been removed. It could still be emulated through custom ``_setup_next_sequence`` methods, or by calling ``MyFactory.reset_sequence()``. --- docs/changelog.rst | 13 ++++++++++++- docs/orms.rst | 2 -- factory/alchemy.py | 12 ------------ factory/django.py | 15 --------------- tests/test_alchemy.py | 22 +++++++++++----------- tests/test_django.py | 40 ++++++++++++++++++++-------------------- tests/test_using.py | 20 +++++++------------- 7 files changed, 50 insertions(+), 74 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7d77f7f5..018ec607 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,17 @@ ChangeLog ========= +.. _v2.5.0: + +2.5.0 (master) +-------------- + +*Deprecation:* + + - Remove deprecated features from :ref:`v2.4.0` + - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy; + this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. + .. _v2.4.1: 2.4.1 (2014-06-23) @@ -19,7 +30,7 @@ ChangeLog *New:* - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) - - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) + - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`) - Declare target model and other non-declaration fields in a ``class Meta`` section. diff --git a/docs/orms.rst b/docs/orms.rst index 2aa27b2a..88d49e94 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -35,7 +35,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the * The :attr:`~factory.FactoryOptions.model` attribute also supports the ``'app.Model'`` syntax * :func:`~factory.Factory.create()` uses :meth:`Model.objects.create() ` - * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value * When using :class:`~factory.RelatedFactory` or :class:`~factory.PostGeneration` attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. @@ -284,7 +283,6 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: This class provides the following features: * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add` - * :func:`~factory.Factory._setup_next_sequence()` selects the next unused primary key value .. attribute:: FACTORY_SESSION diff --git a/factory/alchemy.py b/factory/alchemy.py index 3c91411f..2cd28bb7 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -43,18 +43,6 @@ class Meta: 'FACTORY_SESSION': 'sqlalchemy_session', }) - @classmethod - def _setup_next_sequence(cls, *args, **kwargs): - """Compute the next available PK, based on the 'pk' database field.""" - session = cls._meta.sqlalchemy_session - model = cls._meta.model - pk = getattr(model, model.__mapper__.primary_key[0].name) - max_pk = session.query(max(pk)).one()[0] - if isinstance(max_pk, int): - return max_pk + 1 if max_pk else 1 - else: - return 1 - @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" diff --git a/factory/django.py b/factory/django.py index 2b6c463e..c58a6e23 100644 --- a/factory/django.py +++ b/factory/django.py @@ -109,21 +109,6 @@ def _get_manager(cls, model_class): except AttributeError: return model_class.objects - @classmethod - def _setup_next_sequence(cls): - """Compute the next available PK, based on the 'pk' database field.""" - - model = cls._get_model_class() # pylint: disable=E1101 - manager = cls._get_manager(model) - - try: - return 1 + manager.values_list('pk', flat=True - ).order_by('-pk')[0] - except (IndexError, TypeError): - # IndexError: No instance exist yet - # TypeError: pk isn't an integer type - return 1 - @classmethod def _get_or_create(cls, model_class, *args, **kwargs): """Create an instance of the model through objects.get_or_create.""" diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index b9222eb3..2deb418d 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -88,18 +88,18 @@ def test_pk_creation(self): StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo2', std2.foo) - self.assertEqual(2, std2.id) + self.assertEqual('foo0', std2.foo) + self.assertEqual(0, std2.id) def test_pk_force_value(self): std1 = StandardFactory.create(id=10) - self.assertEqual('foo1', std1.foo) # sequence was set before pk + self.assertEqual('foo1', std1.foo) # sequence and pk are unrelated self.assertEqual(10, std1.id) StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo11', std2.foo) - self.assertEqual(11, std2.id) + self.assertEqual('foo0', std2.foo) # Sequence doesn't care about pk + self.assertEqual(0, std2.id) @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") @@ -111,22 +111,22 @@ def setUp(self): def test_first(self): nonint = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint.id) + self.assertEqual('foo0', nonint.id) def test_many(self): nonint1 = NonIntegerPkFactory.build() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint1.id) - self.assertEqual('foo2', nonint2.id) + self.assertEqual('foo0', nonint1.id) + self.assertEqual('foo1', nonint2.id) def test_creation(self): nonint1 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint1.id) + self.assertEqual('foo0', nonint1.id) NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint2.id) + self.assertEqual('foo0', nonint2.id) def test_force_pk(self): nonint1 = NonIntegerPkFactory.create(id='foo10') @@ -134,4 +134,4 @@ def test_force_pk(self): NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint2.id) + self.assertEqual('foo0', nonint2.id) diff --git a/tests/test_django.py b/tests/test_django.py index 874c2729..0cbef194 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -174,32 +174,32 @@ def setUp(self): def test_pk_first(self): std = StandardFactory.build() - self.assertEqual('foo1', std.foo) + self.assertEqual('foo0', std.foo) def test_pk_many(self): std1 = StandardFactory.build() std2 = StandardFactory.build() - self.assertEqual('foo1', std1.foo) - self.assertEqual('foo2', std2.foo) + self.assertEqual('foo0', std1.foo) + self.assertEqual('foo1', std2.foo) def test_pk_creation(self): std1 = StandardFactory.create() - self.assertEqual('foo1', std1.foo) + self.assertEqual('foo0', std1.foo) self.assertEqual(1, std1.pk) StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo2', std2.foo) + self.assertEqual('foo0', std2.foo) self.assertEqual(2, std2.pk) def test_pk_force_value(self): std1 = StandardFactory.create(pk=10) - self.assertEqual('foo1', std1.foo) # sequence was set before pk + self.assertEqual('foo0', std1.foo) # sequence is unrelated to pk self.assertEqual(10, std1.pk) StandardFactory.reset_sequence() std2 = StandardFactory.create() - self.assertEqual('foo11', std2.foo) + self.assertEqual('foo0', std2.foo) self.assertEqual(11, std2.pk) @@ -212,12 +212,12 @@ def setUp(self): def test_no_pk(self): std = StandardFactoryWithPKField() self.assertIsNotNone(std.pk) - self.assertEqual('foo1', std.foo) + self.assertEqual('foo0', std.foo) def test_force_pk(self): std = StandardFactoryWithPKField(pk=42) self.assertIsNotNone(std.pk) - self.assertEqual('foo1', std.foo) + self.assertEqual('foo0', std.foo) def test_reuse_pk(self): std1 = StandardFactoryWithPKField(foo='bar') @@ -286,9 +286,9 @@ class Meta: self.assertEqual(models.StandardModel, e1.__class__) self.assertEqual(models.StandardSon, e2.__class__) self.assertEqual(models.StandardModel, e3.__class__) - self.assertEqual(1, e1.foo) - self.assertEqual(2, e2.foo) - self.assertEqual(3, e3.foo) + self.assertEqual(0, e1.foo) + self.assertEqual(1, e2.foo) + self.assertEqual(2, e3.foo) @unittest.skipIf(django is None, "Django not installed.") @@ -299,23 +299,23 @@ def setUp(self): def test_first(self): nonint = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint.foo) + self.assertEqual('foo0', nonint.foo) def test_many(self): nonint1 = NonIntegerPkFactory.build() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint1.foo) - self.assertEqual('foo2', nonint2.foo) + self.assertEqual('foo0', nonint1.foo) + self.assertEqual('foo1', nonint2.foo) def test_creation(self): nonint1 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint1.foo) - self.assertEqual('foo1', nonint1.pk) + self.assertEqual('foo0', nonint1.foo) + self.assertEqual('foo0', nonint1.pk) NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.build() - self.assertEqual('foo1', nonint2.foo) + self.assertEqual('foo0', nonint2.foo) def test_force_pk(self): nonint1 = NonIntegerPkFactory.create(pk='foo10') @@ -324,8 +324,8 @@ def test_force_pk(self): NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.create() - self.assertEqual('foo1', nonint2.foo) - self.assertEqual('foo1', nonint2.pk) + self.assertEqual('foo0', nonint2.foo) + self.assertEqual('foo0', nonint2.pk) @unittest.skipIf(django is None, "Django not installed.") diff --git a/tests/test_using.py b/tests/test_using.py index 8d787895..8aba8b69 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1490,12 +1490,6 @@ def get_or_create(self, **kwargs): instance.id = 2 return instance, True - def values_list(self, *args, **kwargs): - return self - - def order_by(self, *args, **kwargs): - return [1] - class BetterFakeModel(object): @classmethod @@ -1618,14 +1612,14 @@ class Meta: o1 = TestModelFactory() o2 = TestModelFactory() - self.assertEqual('foo_2', o1.a) - self.assertEqual('foo_3', o2.a) + self.assertEqual('foo_0', o1.a) + self.assertEqual('foo_1', o2.a) o3 = TestModelFactory.build() o4 = TestModelFactory.build() - self.assertEqual('foo_4', o3.a) - self.assertEqual('foo_5', o4.a) + self.assertEqual('foo_2', o3.a) + self.assertEqual('foo_3', o4.a) def test_no_get_or_create(self): class TestModelFactory(factory.django.DjangoModelFactory): @@ -1636,7 +1630,7 @@ class Meta: o = TestModelFactory() self.assertEqual(None, o._defaults) - self.assertEqual('foo_2', o.a) + self.assertEqual('foo_0', o.a) self.assertEqual(2, o.id) def test_get_or_create(self): @@ -1652,7 +1646,7 @@ class Meta: o = TestModelFactory() self.assertEqual({'c': 3, 'd': 4}, o._defaults) - self.assertEqual('foo_2', o.a) + self.assertEqual('foo_0', o.a) self.assertEqual(2, o.b) self.assertEqual(3, o.c) self.assertEqual(4, o.d) @@ -1672,7 +1666,7 @@ class Meta: o = TestModelFactory() self.assertEqual({}, o._defaults) - self.assertEqual('foo_2', o.a) + self.assertEqual('foo_0', o.a) self.assertEqual(2, o.b) self.assertEqual(3, o.c) self.assertEqual(4, o.d) From 336ea5ac8b2d922fb54f99edd55d4773dd126934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 18 Nov 2014 00:35:19 +0100 Subject: [PATCH 062/714] Remove deprecated features. This disables the ``FACTORY_FOR`` syntax and related parameters, that should be declared through ``class Meta``. --- docs/orms.rst | 9 ------- docs/reference.rst | 29 ---------------------- factory/alchemy.py | 5 ---- factory/base.py | 26 -------------------- factory/declarations.py | 9 ------- factory/django.py | 5 ---- factory/helpers.py | 1 - tests/__init__.py | 1 - tests/test_declarations.py | 15 ------------ tests/test_deprecation.py | 49 -------------------------------------- tests/test_using.py | 2 +- 11 files changed, 1 insertion(+), 150 deletions(-) delete mode 100644 tests/test_deprecation.py diff --git a/docs/orms.rst b/docs/orms.rst index 88d49e94..e32eafa2 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -39,11 +39,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the attributes, the base object will be :meth:`saved ` once all post-generation hooks have run. - .. attribute:: FACTORY_DJANGO_GET_OR_CREATE - - .. deprecated:: 2.4.0 - See :attr:`DjangoOptions.django_get_or_create`. - .. class:: DjangoOptions(factory.base.FactoryOptions) @@ -284,10 +279,6 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add` - .. attribute:: FACTORY_SESSION - - .. deprecated:: 2.4.0 - See :attr:`~SQLAlchemyOptions.sqlalchemy_session`. .. class:: SQLAlchemyOptions(factory.base.FactoryOptions) diff --git a/docs/reference.rst b/docs/reference.rst index b0dda504..5eea62c0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -115,35 +115,6 @@ The :class:`Factory` class .. class:: Factory - .. note:: In previous versions, the fields of :class:`class Meta ` were - defined as class attributes on :class:`Factory`. This is now deprecated and will be removed - in 2.5.0. - - .. attribute:: FACTORY_FOR - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.model`. - - .. attribute:: ABSTRACT_FACTORY - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.abstract`. - - .. attribute:: FACTORY_ARG_PARAMETERS - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.inline_args`. - - .. attribute:: FACTORY_HIDDEN_ARGS - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.exclude`. - - .. attribute:: FACTORY_STRATEGY - - .. deprecated:: 2.4.0 - See :attr:`FactoryOptions.strategy`. - **Class-level attributes:** diff --git a/factory/alchemy.py b/factory/alchemy.py index 2cd28bb7..64083934 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -38,11 +38,6 @@ class SQLAlchemyModelFactory(base.Factory): class Meta: abstract = True - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() - _OLDSTYLE_ATTRIBUTES.update({ - 'FACTORY_SESSION': 'sqlalchemy_session', - }) - @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" diff --git a/factory/base.py b/factory/base.py index 9e07899a..efff976f 100644 --- a/factory/base.py +++ b/factory/base.py @@ -21,7 +21,6 @@ # THE SOFTWARE. import logging -import warnings from . import containers from . import declarations @@ -109,23 +108,6 @@ def __new__(mcs, class_name, bases, attrs): attrs_meta = attrs.pop('Meta', None) - oldstyle_attrs = {} - converted_attrs = {} - for old_name, new_name in base_factory._OLDSTYLE_ATTRIBUTES.items(): - if old_name in attrs: - oldstyle_attrs[old_name] = new_name - converted_attrs[new_name] = attrs.pop(old_name) - if oldstyle_attrs: - warnings.warn( - "Declaring any of %s at class-level is deprecated" - " and will be removed in the future. Please set them" - " as %s attributes of a 'class Meta' attribute." % ( - ', '.join(oldstyle_attrs.keys()), - ', '.join(oldstyle_attrs.values()), - ), - PendingDeprecationWarning, 2) - attrs_meta = type('Meta', (object,), converted_attrs) - base_meta = resolve_attribute('_meta', bases) options_class = resolve_attribute('_options_class', bases, FactoryOptions) @@ -322,14 +304,6 @@ def __new__(cls, *args, **kwargs): _meta = FactoryOptions() - _OLDSTYLE_ATTRIBUTES = { - 'FACTORY_FOR': 'model', - 'ABSTRACT_FACTORY': 'abstract', - 'FACTORY_STRATEGY': 'strategy', - 'FACTORY_ARG_PARAMETERS': 'inline_args', - 'FACTORY_HIDDEN_ARGS': 'exclude', - } - # ID to use for the next 'declarations.Sequence' attribute. _counter = None diff --git a/factory/declarations.py b/factory/declarations.py index 5e7e734a..f6f58465 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -22,7 +22,6 @@ import itertools -import warnings import logging from . import compat @@ -502,14 +501,6 @@ class RelatedFactory(PostGenerationDeclaration): def __init__(self, factory, factory_related_name='', **defaults): super(RelatedFactory, self).__init__() - if factory_related_name == '' and defaults.get('name') is not None: - warnings.warn( - "Usage of RelatedFactory(SomeFactory, name='foo') is deprecated" - " and will be removed in the future. Please use the" - " RelatedFactory(SomeFactory, 'foo') or" - " RelatedFactory(SomeFactory, factory_related_name='foo')" - " syntax instead", PendingDeprecationWarning, 2) - factory_related_name = defaults.pop('name') self.name = factory_related_name self.defaults = defaults diff --git a/factory/django.py b/factory/django.py index c58a6e23..70503668 100644 --- a/factory/django.py +++ b/factory/django.py @@ -84,11 +84,6 @@ class DjangoModelFactory(base.Factory): class Meta: abstract = True # Optional, but explicit. - _OLDSTYLE_ATTRIBUTES = base.Factory._OLDSTYLE_ATTRIBUTES.copy() - _OLDSTYLE_ATTRIBUTES.update({ - 'FACTORY_DJANGO_GET_OR_CREATE': 'django_get_or_create', - }) - @classmethod def _load_model_class(cls, definition): diff --git a/factory/helpers.py b/factory/helpers.py index 19431df1..b9cef6e2 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -28,7 +28,6 @@ from . import base from . import declarations -from . import django @contextlib.contextmanager diff --git a/tests/__init__.py b/tests/__init__.py index 855beea4..5b6fc55b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -4,7 +4,6 @@ from .test_base import * from .test_containers import * from .test_declarations import * -from .test_deprecation import * from .test_django import * from .test_fuzzy import * from .test_helpers import * diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 86bc8b50..18a4cd4c 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -22,7 +22,6 @@ import datetime import itertools -import warnings from factory import declarations from factory import helpers @@ -207,20 +206,6 @@ def test_cache(self): datetime.date = orig_date -class RelatedFactoryTestCase(unittest.TestCase): - - def test_deprecate_name(self): - with warnings.catch_warnings(record=True) as w: - - warnings.simplefilter('always') - f = declarations.RelatedFactory('datetime.date', name='blah') - - self.assertEqual('blah', f.name) - self.assertEqual(1, len(w)) - self.assertIn('RelatedFactory', str(w[0].message)) - self.assertIn('factory_related_name', str(w[0].message)) - - class PostGenerationMethodCallTestCase(unittest.TestCase): def setUp(self): self.obj = mock.MagicMock() diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py deleted file mode 100644 index a07cbf31..00000000 --- a/tests/test_deprecation.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -"""Tests for deprecated features.""" - -import warnings - -import factory - -from .compat import mock, unittest -from . import tools - - -class DeprecationTests(unittest.TestCase): - def test_factory_for(self): - class Foo(object): - pass - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always') - class FooFactory(factory.Factory): - FACTORY_FOR = Foo - - self.assertEqual(1, len(w)) - warning = w[0] - # Message is indeed related to the current file - # This is to ensure error messages are readable by end users. - self.assertIn(warning.filename, __file__) - self.assertIn('FACTORY_FOR', str(warning.message)) - self.assertIn('model', str(warning.message)) diff --git a/tests/test_using.py b/tests/test_using.py index 8aba8b69..7318f2e1 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1798,7 +1798,7 @@ class Meta: model = TestObject one = 3 two = 2 - three = factory.RelatedFactory(TestRelatedObjectFactory, name='obj') + three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') obj = TestObjectFactory.build() # Normal fields From 7b31d60af0ab0678d04d7f50abc28ba7c4ccfcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 18 Nov 2014 00:45:07 +0100 Subject: [PATCH 063/714] Fix typo in docs --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 5eea62c0..25169364 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -369,7 +369,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. when using the ``create`` strategy. That policy will be used if the - :attr:`associated class ` has an ``objects`` attribute *and* the :meth:`~Factory._create` classmethod of the :class:`Factory` wasn't overridden. From 392db861e585f12038f18f41e467ecfcab9d39b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 25 Nov 2014 23:46:28 +0100 Subject: [PATCH 064/714] Fix reference docs (Closes #166, #167). Use ``obj`` for ``@post_generation``-decorated methods, instead of ``self``: this makes it clearer that the ``obj`` is an instance of the model, and not of the ``Factory``. Thanks to @jamescooke & @NiklasMM for spotting the typo. --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 25169364..43433e0e 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1192,7 +1192,7 @@ For instance, a :class:`PostGeneration` hook is declared as ``post``: model = SomeObject @post_generation - def post(self, create, extracted, **kwargs): + def post(obj, create, extracted, **kwargs): obj.set_origin(create) .. OHAI_VIM** From f83c602874698427bdc141accd8fc14a9749d6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 6 Feb 2015 23:19:06 +0100 Subject: [PATCH 065/714] docs: Add explanations about SQLAlchemy's scoped_session. --- docs/conf.py | 4 +- docs/orms.rst | 102 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 4f76d459..ce6730b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -247,7 +247,7 @@ def get_version(*module_dir_components): 'http://docs.djangoproject.com/en/dev/_objects/', ), 'sqlalchemy': ( - 'http://docs.sqlalchemy.org/en/rel_0_8/', - 'http://docs.sqlalchemy.org/en/rel_0_8/objects.inv', + 'http://docs.sqlalchemy.org/en/rel_0_9/', + 'http://docs.sqlalchemy.org/en/rel_0_9/objects.inv', ), } diff --git a/docs/orms.rst b/docs/orms.rst index e32eafa2..ab813a21 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -298,9 +298,8 @@ A (very) simple exemple: from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker - session = scoped_session(sessionmaker()) engine = create_engine('sqlite://') - session.configure(bind=engine) + session = scoped_session(sessionmaker(bind=engine)) Base = declarative_base() @@ -330,3 +329,102 @@ A (very) simple exemple: >>> session.query(User).all() [] + + +Managing sessions +""""""""""""""""" + +Since `SQLAlchemy`_ is a general purpose library, +there is no "global" session management system. + +The most common pattern when working with unit tests and ``factory_boy`` +is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: + +* The test runner configures some project-wide :class:`~sqlalchemy.orm.scoping.scoped_session` +* Each :class:`~SQLAlchemyModelFactory` subclass uses this + :class:`~sqlalchemy.orm.scoping.scoped_session` as its :attr:`~SQLAlchemyOptions.sqlalchemy_session` +* The :meth:`~unittest.TestCase.tearDown` method of tests calls + :meth:`Session.remove ` + to reset the session. + + +Here is an example layout: + +- A global (test-only?) file holds the :class:`~sqlalchemy.orm.scoping.scoped_session`: + +.. code-block:: python + + # myprojet/test/common.py + + from sqlalchemy import orm + Session = orm.scoped_session(orm.sessionmaker()) + + +- All factory access it: + +.. code-block:: python + + # myproject/factories.py + + import factory + import factory.alchemy + + from . import models + from .test import common + + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = models.User + + # Use the not-so-global scoped_session + # Warning: DO NOT USE common.Session()! + sqlalchemy_session = common.Session + + name = factory.Sequence(lambda n: "User %d" % n) + + +- The test runner configures the :class:`~sqlalchemy.orm.scoping.scoped_session` when it starts: + +.. code-block:: python + + # myproject/test/runtests.py + + import sqlalchemy + + from . import common + + def runtests(): + engine = sqlalchemy.create_engine('sqlite://') + + # It's a scoped_session, we can configure it later + common.Session.configure(engine=engine) + + run_the_tests + + +- :class:`test cases ` use this ``scoped_session``, + and clear it after each test: + +.. code-block:: python + + # myproject/test/test_stuff.py + + import unittest + + from . import common + + class MyTest(unittest.TestCase): + + def setUp(self): + # Prepare a new, clean session + self.session = common.Session() + + def test_something(self): + u = factories.UserFactory() + self.assertEqual([u], self.session.query(User).all()) + + def tearDown(self): + # Rollback the session => no changes to the database + self.session.rollback() + # Remove it, so that the next test gets a new Session() + common.Session.remove() From d95bc982cd8480aa44e5282ab1284a9278049066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 6 Feb 2015 23:29:52 +0100 Subject: [PATCH 066/714] docs: Improve explanation of SQLAlchemy's scoped_session. --- docs/orms.rst | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index ab813a21..9e4d106e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -347,6 +347,16 @@ is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: :meth:`Session.remove ` to reset the session. +.. note:: See the excellent :ref:`SQLAlchemy guide on scoped_session ` + for details of :class:`~sqlalchemy.orm.scoping.scoped_session`'s usage. + + The basic idea is that declarative parts of the code (including factories) + need a simple way to access the "current session", + but that session will only be created and configured at a later point. + + The :class:`~sqlalchemy.orm.scoping.scoped_session` handles this, + by virtue of only creating the session when a query is sent to the database. + Here is an example layout: @@ -396,14 +406,14 @@ Here is an example layout: def runtests(): engine = sqlalchemy.create_engine('sqlite://') - # It's a scoped_session, we can configure it later - common.Session.configure(engine=engine) + # It's a scoped_session, and now is the time to configure it. + common.Session.configure(bind=engine) run_the_tests - :class:`test cases ` use this ``scoped_session``, - and clear it after each test: + and clear it after each test (for isolation): .. code-block:: python From 97a88905b7f0f513bd480fe630e43798aba22c74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 18 Feb 2015 22:00:01 +0100 Subject: [PATCH 067/714] Enable resetting factory.fuzzy's random generator (Closes #175, #185). Users may now call ``factory.fuzzy.get_random_state()`` to retrieve the current random generator's state (isolated from the one used in Python's ``random``). That state can then be reinjected with ``factory.fuzzy.set_random_state(state)``. --- docs/changelog.rst | 4 ++++ docs/fuzzy.rst | 30 +++++++++++++++++++++++++++ factory/fuzzy.py | 35 +++++++++++++++++++++++-------- tests/test_fuzzy.py | 50 ++++++++++++++++++++++++++++++++------------- 4 files changed, 97 insertions(+), 22 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 018ec607..ebe99300 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,10 @@ ChangeLog 2.5.0 (master) -------------- +*New:* + + - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). + *Deprecation:* - Remove deprecated features from :ref:`v2.4.0` diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 14804192..06586525 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -338,3 +338,33 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its The method responsible for generating random values. *Must* be overridden in subclasses. + + +Managing randomness +------------------- + +Using :mod:`random` in factories allows to "fuzz" a program efficiently. +However, it's sometimes required to *reproduce* a failing test. + +:mod:`factory.fuzzy` uses a separate instance of :class:`random.Random`, +and provides a few helpers for this: + +.. method:: get_random_state() + + Call :meth:`get_random_state` to retrieve the random generator's current + state. + +.. method:: set_random_state(state) + + Use :meth:`set_random_state` to set a custom state into the random generator + (fetched from :meth:`get_random_state` in a previous run, for instance) + +.. method:: reseed_random(seed) + + The :meth:`reseed_random` function allows to load a chosen seed into the random generator. + + +Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD** +use :obj:`factory.fuzzy._random` foras a randomness source; this ensures that +data they generate can be regenerated using the simple state from +:meth:`get_random_state`. diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 94599b72..0137ba99 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -34,6 +34,25 @@ from . import declarations +_random = random.Random() + + +def get_random_state(): + """Retrieve the state of factory.fuzzy's random generator.""" + return _random.getstate() + + +def set_random_state(state): + """Force-set the state of factory.fuzzy's random generator.""" + return _random.setstate(state) + + +def reseed_random(seed): + """Reseed factory.fuzzy's random generator.""" + r = random.Random(seed) + set_random_state(r.getstate()) + + class BaseFuzzyAttribute(declarations.OrderedDeclaration): """Base class for fuzzy attributes. @@ -81,7 +100,7 @@ class FuzzyText(BaseFuzzyAttribute): """ def __init__(self, prefix='', length=12, suffix='', - chars=string.ascii_letters, **kwargs): + chars=string.ascii_letters, **kwargs): super(FuzzyText, self).__init__(**kwargs) self.prefix = prefix self.suffix = suffix @@ -89,7 +108,7 @@ def __init__(self, prefix='', length=12, suffix='', self.chars = tuple(chars) # Unroll iterators def fuzz(self): - chars = [random.choice(self.chars) for _i in range(self.length)] + chars = [_random.choice(self.chars) for _i in range(self.length)] return self.prefix + ''.join(chars) + self.suffix @@ -101,7 +120,7 @@ def __init__(self, choices, **kwargs): super(FuzzyChoice, self).__init__(**kwargs) def fuzz(self): - return random.choice(self.choices) + return _random.choice(self.choices) class FuzzyInteger(BaseFuzzyAttribute): @@ -119,7 +138,7 @@ def __init__(self, low, high=None, step=1, **kwargs): super(FuzzyInteger, self).__init__(**kwargs) def fuzz(self): - return random.randrange(self.low, self.high + 1, self.step) + return _random.randrange(self.low, self.high + 1, self.step) class FuzzyDecimal(BaseFuzzyAttribute): @@ -137,7 +156,7 @@ def __init__(self, low, high=None, precision=2, **kwargs): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - base = compat.float_to_decimal(random.uniform(self.low, self.high)) + base = compat.float_to_decimal(_random.uniform(self.low, self.high)) return base.quantize(decimal.Decimal(10) ** -self.precision) @@ -155,7 +174,7 @@ def __init__(self, low, high=None, **kwargs): super(FuzzyFloat, self).__init__(**kwargs) def fuzz(self): - return random.uniform(self.low, self.high) + return _random.uniform(self.low, self.high) class FuzzyDate(BaseFuzzyAttribute): @@ -175,7 +194,7 @@ def __init__(self, start_date, end_date=None, **kwargs): self.end_date = end_date.toordinal() def fuzz(self): - return datetime.date.fromordinal(random.randint(self.start_date, self.end_date)) + return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date)) class BaseFuzzyDateTime(BaseFuzzyAttribute): @@ -215,7 +234,7 @@ def fuzz(self): delta = self.end_dt - self.start_dt microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400)) - offset = random.randint(0, microseconds) + offset = _random.randint(0, microseconds) result = self.start_dt + datetime.timedelta(microseconds=offset) if self.force_year is not None: diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 1caeb0a2..fd32705d 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -55,7 +55,7 @@ def test_mock(self): d = fuzzy.FuzzyChoice(options) - with mock.patch('random.choice', fake_choice): + with mock.patch('factory.fuzzy._random.choice', fake_choice): res = d.evaluate(2, None, False) self.assertEqual(6, res) @@ -93,7 +93,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyInteger(2, 8) - with mock.patch('random.randrange', fake_randrange): + with mock.patch('factory.fuzzy._random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((2 + 8 + 1) * 1, res) @@ -103,7 +103,7 @@ def test_biased_high_only(self): fuzz = fuzzy.FuzzyInteger(8) - with mock.patch('random.randrange', fake_randrange): + with mock.patch('factory.fuzzy._random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((0 + 8 + 1) * 1, res) @@ -113,7 +113,7 @@ def test_biased_with_step(self): fuzz = fuzzy.FuzzyInteger(5, 8, 3) - with mock.patch('random.randrange', fake_randrange): + with mock.patch('factory.fuzzy._random.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((5 + 8 + 1) * 3, res) @@ -146,7 +146,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyDecimal(2.0, 8.0) - with mock.patch('random.uniform', fake_uniform): + with mock.patch('factory.fuzzy._random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('10.0'), res) @@ -156,7 +156,7 @@ def test_biased_high_only(self): fuzz = fuzzy.FuzzyDecimal(8.0) - with mock.patch('random.uniform', fake_uniform): + with mock.patch('factory.fuzzy._random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('8.0'), res) @@ -166,7 +166,7 @@ def test_precision(self): fuzz = fuzzy.FuzzyDecimal(8.0, precision=3) - with mock.patch('random.uniform', fake_uniform): + with mock.patch('factory.fuzzy._random.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) @@ -214,7 +214,7 @@ def test_biased(self): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31) - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.date(2013, 1, 16), res) @@ -225,7 +225,7 @@ def test_biased_partial(self): fuzz = fuzzy.FuzzyDate(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.date(2013, 1, 2), res) @@ -332,7 +332,7 @@ def test_biased(self): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31) - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 16), res) @@ -343,7 +343,7 @@ def test_biased_partial(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2), res) @@ -450,7 +450,7 @@ def test_biased(self): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31) - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res) @@ -461,7 +461,7 @@ def test_biased_partial(self): fuzz = fuzzy.FuzzyDateTime(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('random.randint', fake_randint): + with mock.patch('factory.fuzzy._random.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res) @@ -486,7 +486,7 @@ def test_mock(self): chars = ['a', 'b', 'c'] fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4) - with mock.patch('random.choice', fake_choice): + with mock.patch('factory.fuzzy._random.choice', fake_choice): res = fuzz.evaluate(2, None, False) self.assertEqual('preaaaapost', res) @@ -504,3 +504,25 @@ def options(): for char in res: self.assertIn(char, ['a', 'b', 'c']) + + +class FuzzyRandomTestCase(unittest.TestCase): + def test_seeding(self): + fuzz = fuzzy.FuzzyInteger(1, 1000) + + fuzzy.reseed_random(42) + value = fuzz.evaluate(sequence=1, obj=None, create=False) + + fuzzy.reseed_random(42) + value2 = fuzz.evaluate(sequence=1, obj=None, create=False) + self.assertEqual(value, value2) + + def test_reset_state(self): + fuzz = fuzzy.FuzzyInteger(1, 1000) + + state = fuzzy.get_random_state() + value = fuzz.evaluate(sequence=1, obj=None, create=False) + + fuzzy.set_random_state(state) + value2 = fuzz.evaluate(sequence=1, obj=None, create=False) + self.assertEqual(value, value2) From 93e37c2016b72e7ee66b02bfae329753ccfbe322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 18 Feb 2015 22:05:17 +0100 Subject: [PATCH 068/714] Remove sphinx markup from README.rst (Closes #180). That file should be readable by PyPI's RST parser, which doesn't support Sphinx constructs. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 32b93bdf..cb38a9a7 100644 --- a/README.rst +++ b/README.rst @@ -250,7 +250,7 @@ Debugging factory_boy Debugging factory_boy can be rather complex due to the long chains of calls. Detailed logging is available through the ``factory`` logger. -A helper, :meth:`factory.debug()`, is available to ease debugging: +A helper, `factory.debug()`, is available to ease debugging: .. code-block:: python From efa9d3c0d165a4c49def26b423711ed28eb2d264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:45:45 +0100 Subject: [PATCH 069/714] Fix typos in docs (Closes #159, closes #178, closes #188). --- README.rst | 2 +- docs/orms.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index cb38a9a7..c787ca8b 100644 --- a/README.rst +++ b/README.rst @@ -159,7 +159,7 @@ It is also possible to create a bunch of objects in a single call: .. code-block:: pycon - >>> users = UserFactory.build(10, first_name="Joe") + >>> users = UserFactory.build_batch(10, first_name="Joe") >>> len(users) 10 >>> [user.first_name for user in users] diff --git a/docs/orms.rst b/docs/orms.rst index 9e4d106e..a0afc408 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -290,7 +290,7 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. -A (very) simple exemple: +A (very) simple example: .. code-block:: python From b6f6ae48c796d722f2a0209963d525b2f8b8fe0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:47:42 +0100 Subject: [PATCH 070/714] Fix bad default value for Factory.declarations (Closes #162). --- factory/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/base.py b/factory/base.py index efff976f..7215f00d 100644 --- a/factory/base.py +++ b/factory/base.py @@ -409,7 +409,7 @@ def declarations(cls, extra_defs=None): retrieved DeclarationDict. """ decls = cls._meta.declarations.copy() - decls.update(extra_defs) + decls.update(extra_defs or {}) return decls @classmethod From c666411153ea9840b492f7abecf0cfa51e21dc27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:48:43 +0100 Subject: [PATCH 071/714] Docs: fix default strategy (Closes #158). --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 43433e0e..9fd2576c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -109,7 +109,7 @@ The :class:`Factory` class .. attribute:: strategy Use this attribute to change the strategy used by a :class:`Factory`. - The default is :data:`BUILD_STRATEGY`. + The default is :data:`CREATE_STRATEGY`. From 6b9a2b5d9aaa1f4fb06819240a7b243fcfd79943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 3 Mar 2015 22:50:49 +0100 Subject: [PATCH 072/714] Logs: Allow non-integer sequences (Closes #148). As pointed by @glinmac. --- factory/declarations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index f6f58465..b3833ee7 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -194,7 +194,7 @@ def __init__(self, function, type=int): # pylint: disable=W0622 self.type = type def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("Sequence: Computing next value of %r for seq=%d", self.function, sequence) + logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, sequence) return self.function(self.type(sequence)) @@ -208,7 +208,7 @@ class LazyAttributeSequence(Sequence): of counter for the 'function' attribute. """ def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%d, obj=%r", + logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%r", self.function, sequence, obj) return self.function(obj, self.type(sequence)) From 40d4a4b13d4ca959879d1798f24d510fd7abf4dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:07:19 +0100 Subject: [PATCH 073/714] Fix typo in FuzzyDateTime (Closes #189). Thanks to @shinuza for spotting this! --- factory/fuzzy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 0137ba99..564264ef 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -289,10 +289,10 @@ def _now(self): def _check_bounds(self, start_dt, end_dt): if start_dt.tzinfo is None: raise ValueError( - "FuzzyDateTime only handles aware datetimes, got start=%r" + "FuzzyDateTime requires timezone-aware datetimes, got start=%r" % start_dt) if end_dt.tzinfo is None: raise ValueError( - "FuzzyDateTime only handles aware datetimes, got end=%r" + "FuzzyDateTime requires timezone-aware datetimes, got end=%r" % end_dt) super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt) From 69befae5fde1897cf68c4d44a146db5ba642c814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:22:03 +0100 Subject: [PATCH 074/714] Allow lazy evaluation of FuzzyChoice's iterators (Closes #184). This allows the following idiom: ``user = factory.fuzzy.FuzzyChoice(User.objects.all())`` Previously, the ``User.objects.all()`` queryset would have been evaluated *at import time*; it is now evaluated with the first use of the ``FuzzyChoice``. --- docs/changelog.rst | 1 + docs/fuzzy.rst | 7 +++++-- factory/fuzzy.py | 12 ++++++++++-- tests/test_fuzzy.py | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ebe99300..13fdd68b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog *New:* - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). + - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). *Deprecation:* diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 06586525..18978e42 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -62,8 +62,11 @@ FuzzyChoice The :class:`FuzzyChoice` fuzzer yields random choices from the given iterable. - .. note:: The passed in :attr:`choices` will be converted into a list at - declaration time. + .. note:: The passed in :attr:`choices` will be converted into a list upon + first use, not at declaration time. + + This allows passing in, for instance, a Django queryset that will + only hit the database during the database, not at import time. .. attribute:: choices diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 564264ef..4e6a03dc 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -113,13 +113,21 @@ def fuzz(self): class FuzzyChoice(BaseFuzzyAttribute): - """Handles fuzzy choice of an attribute.""" + """Handles fuzzy choice of an attribute. + + Args: + choices (iterable): An iterable yielding options; will only be unrolled + on the first call. + """ def __init__(self, choices, **kwargs): - self.choices = list(choices) + self.choices = None + self.choices_generator = choices super(FuzzyChoice, self).__init__(**kwargs) def fuzz(self): + if self.choices is None: + self.choices = list(self.choices_generator) return _random.choice(self.choices) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index fd32705d..c7e1106c 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -74,6 +74,24 @@ def options(): res = d.evaluate(2, None, False) self.assertIn(res, [0, 1, 2]) + def test_lazy_generator(self): + class Gen(object): + def __init__(self, options): + self.options = options + self.unrolled = False + + def __iter__(self): + self.unrolled = True + return iter(self.options) + + opts = Gen([1, 2, 3]) + d = fuzzy.FuzzyChoice(opts) + self.assertFalse(opts.unrolled) + + res = d.evaluate(2, None, False) + self.assertIn(res, [1, 2, 3]) + self.assertTrue(opts.unrolled) + class FuzzyIntegerTestCase(unittest.TestCase): def test_definition(self): From 72fd943513b0e516f06c53b13ff35ca814b0a4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:37:54 +0100 Subject: [PATCH 075/714] Fix issues between mute_signals() and factory inheritance (Closes #183). Previously, if a factory was decorated with ``@mute_signals`` and one of its descendant called another one of its descendant, signals weren't unmuted properly. --- docs/changelog.rst | 4 ++++ factory/django.py | 9 +++++++-- tests/test_django.py | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 13fdd68b..0cf83680 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,10 @@ ChangeLog - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). +*Bugfix:* + + - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). + *Deprecation:* - Remove deprecated features from :ref:`v2.4.0` diff --git a/factory/django.py b/factory/django.py index 70503668..e823ee9d 100644 --- a/factory/django.py +++ b/factory/django.py @@ -269,6 +269,9 @@ def __exit__(self, exc_type, exc_value, traceback): signal.receivers = receivers self.paused = {} + def copy(self): + return mute_signals(*self.signals) + def __call__(self, callable_obj): if isinstance(callable_obj, base.FactoryMetaClass): # Retrieve __func__, the *actual* callable object. @@ -277,7 +280,8 @@ def __call__(self, callable_obj): @classmethod @functools.wraps(generate_method) def wrapped_generate(*args, **kwargs): - with self: + # A mute_signals() object is not reentrant; use a copy everytime. + with self.copy(): return generate_method(*args, **kwargs) callable_obj._generate = wrapped_generate @@ -286,7 +290,8 @@ def wrapped_generate(*args, **kwargs): else: @functools.wraps(callable_obj) def wrapper(*args, **kwargs): - with self: + # A mute_signals() object is not reentrant; use a copy everytime. + with self.copy(): return callable_obj(*args, **kwargs) return wrapper diff --git a/tests/test_django.py b/tests/test_django.py index 0cbef194..46533053 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -592,6 +592,27 @@ class Meta: self.assertSignalsReactivated() + def test_class_decorator_with_subfactory(self): + @factory.django.mute_signals(signals.pre_save, signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithSignals + + @factory.post_generation + def post(obj, create, extracted, **kwargs): + if not extracted: + WithSignalsDecoratedFactory.create(post=42) + + # This will disable the signals (twice), create two objects, + # and reactivate the signals. + WithSignalsDecoratedFactory() + + self.assertEqual(self.handlers.pre_init.call_count, 2) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.assertSignalsReactivated() + def test_class_decorator_build(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): From 636ca46951d710a4b9d9fd61ec1da02294806d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 22:53:15 +0100 Subject: [PATCH 076/714] Add support for multidb with Django (Closes #171). The ``factory.django.DjangoModelFactory`` now takes an extra option: ``` class MyFactory(factory.django.DjangoModelFactory): class Meta: model = models.MyModel database = 'replica' ``` This will create all instances of ``models.Model`` in the ``'replica'`` database. --- docs/changelog.rst | 1 + docs/orms.rst | 9 ++++++++- factory/django.py | 8 ++++++-- tests/djapp/settings.py | 3 +++ tests/test_django.py | 10 ++++++++++ tests/test_using.py | 6 ++++++ 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0cf83680..c2da698d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). + - Support non-default databases at the factory level (see :issue:`171`) *Bugfix:* diff --git a/docs/orms.rst b/docs/orms.rst index a0afc408..5105e66a 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -42,7 +42,14 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. class:: DjangoOptions(factory.base.FactoryOptions) - The ``class Meta`` on a :class:`~DjangoModelFactory` supports an extra parameter: + The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters: + + .. attribute:: database + + .. versionadded:: 2.5.0 + + All queries to the related model will be routed to the given database. + It defaults to ``'default'``. .. attribute:: django_get_or_create diff --git a/factory/django.py b/factory/django.py index e823ee9d..ee5749a9 100644 --- a/factory/django.py +++ b/factory/django.py @@ -56,6 +56,7 @@ class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), + base.OptionDefault('database', 'default', inherit=True), ] def _get_counter_reference(self): @@ -100,9 +101,12 @@ def _get_manager(cls, model_class): raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) try: - return model_class._default_manager # pylint: disable=W0212 + manager = model_class._default_manager # pylint: disable=W0212 except AttributeError: - return model_class.objects + manager = model_class.objects + + manager = manager.using(cls._meta.database) + return manager @classmethod def _get_or_create(cls, model_class, *args, **kwargs): diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index c051faf8..18e43dd1 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -34,6 +34,9 @@ 'default': { 'ENGINE': 'django.db.backends.sqlite3', }, + 'replica': { + 'ENGINE': 'django.db.backends.sqlite3', + }, } diff --git a/tests/test_django.py b/tests/test_django.py index 46533053..a8f1f771 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -165,6 +165,16 @@ class UnsetModelFactory(factory.django.DjangoModelFactory): self.assertRaises(factory.FactoryError, UnsetModelFactory.create) + def test_cross_database(self): + class OtherDBFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + database = 'replica' + + obj = OtherDBFactory() + self.assertFalse(models.StandardModel.objects.exists()) + self.assertEqual(obj, models.StandardModel.objects.using('replica').get()) + @unittest.skipIf(django is None, "Django not installed.") class DjangoPkSequenceTestCase(django_test.TestCase): diff --git a/tests/test_using.py b/tests/test_using.py index 7318f2e1..1d7977f0 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -69,6 +69,9 @@ def values_list(self, *args, **kwargs): def order_by(self, *args, **kwargs): return [1] + def using(self, db): + return self + objects = FakeModelManager() def __init__(self, **kwargs): @@ -1490,6 +1493,9 @@ def get_or_create(self, **kwargs): instance.id = 2 return instance, True + def using(self, db): + return self + class BetterFakeModel(object): @classmethod From a456a9e3f440e5f61497e97d75dd0a15efe71a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:04:41 +0100 Subject: [PATCH 077/714] Remove limitations of factory.StubFactory (Closes #131). ``StubFactory.build()`` is now supported, and maps to ``StubFactory.stub()``. --- docs/changelog.rst | 1 + factory/base.py | 2 +- tests/test_base.py | 31 +++++++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c2da698d..a6ca79eb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,7 @@ ChangeLog *Bugfix:* - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). + - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`). *Deprecation:* diff --git a/factory/base.py b/factory/base.py index 7215f00d..4c3e0add 100644 --- a/factory/base.py +++ b/factory/base.py @@ -683,7 +683,7 @@ class Meta: @classmethod def build(cls, **kwargs): - raise UnsupportedStrategy() + return cls.stub(**kwargs) @classmethod def create(cls, **kwargs): diff --git a/tests/test_base.py b/tests/test_base.py index d1df58e1..12031b90 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -403,7 +403,7 @@ class Meta: self.assertRaises(base.Factory.UnknownStrategy, TestModelFactory) - def test_stub_with_non_stub_strategy(self): + def test_stub_with_create_strategy(self): class TestModelFactory(base.StubFactory): class Meta: model = TestModel @@ -414,8 +414,18 @@ class Meta: self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) + def test_stub_with_build_strategy(self): + class TestModelFactory(base.StubFactory): + class Meta: + model = TestModel + + one = 'one' + TestModelFactory._meta.strategy = base.BUILD_STRATEGY - self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) + obj = TestModelFactory() + + # For stubs, build() is an alias of stub(). + self.assertFalse(isinstance(obj, TestModel)) def test_change_strategy(self): @base.use_strategy(base.CREATE_STRATEGY) @@ -454,6 +464,23 @@ class TestFactory(TestObjectFactory): self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY) + def test_stub_and_subfactory(self): + class StubA(base.StubFactory): + class Meta: + model = TestObject + + one = 'blah' + + class StubB(base.StubFactory): + class Meta: + model = TestObject + + stubbed = declarations.SubFactory(StubA, two='two') + + b = StubB() + self.assertEqual('blah', b.stubbed.one) + self.assertEqual('two', b.stubbed.two) + def test_custom_creation(self): class TestModelFactory(FakeModelFactory): class Meta: From 4e0e563c1c0d823d2869d340e2fa31ca8630d854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:31:56 +0100 Subject: [PATCH 078/714] Turn FileField/ImageField into normal fields (Closes #141). Previously, they ran as post_generation hooks, meaning that they couldn't be checked in a model's ``save()`` method, for instance. --- docs/changelog.rst | 1 + factory/django.py | 31 ++++++++------------------ tests/test_django.py | 53 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a6ca79eb..c2731efc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,7 @@ ChangeLog - Add support for getting/setting :mod:`factory.fuzzy`'s random state (see :issue:`175`, :issue:`185`). - Support lazy evaluation of iterables in :class:`factory.fuzzy.FuzzyChoice` (see :issue:`184`). - Support non-default databases at the factory level (see :issue:`171`) + - Make :class:`factory.django.FileField` and :class:`factory.django.ImageField` non-post_generation, i.e normal fields also available in ``save()`` (see :issue:`141`). *Bugfix:* diff --git a/factory/django.py b/factory/django.py index ee5749a9..9d4cde98 100644 --- a/factory/django.py +++ b/factory/django.py @@ -144,24 +144,23 @@ def _after_postgeneration(cls, obj, create, results=None): obj.save() -class FileField(declarations.PostGenerationDeclaration): +class FileField(declarations.ParameteredAttribute): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' def __init__(self, **defaults): require_django() - self.defaults = defaults - super(FileField, self).__init__() + super(FileField, self).__init__(**defaults) def _make_data(self, params): """Create data for the field.""" return params.get('data', b'') - def _make_content(self, extraction_context): + def _make_content(self, extra): path = '' params = dict(self.defaults) - params.update(extraction_context.extra) + params.update(extra) if params.get('from_path') and params.get('from_file'): raise ValueError( @@ -169,12 +168,7 @@ def _make_content(self, extraction_context): "be non-empty when calling factory.django.FileField." ) - if extraction_context.did_extract: - # Should be a django.core.files.File - content = extraction_context.value - path = content.name - - elif params.get('from_path'): + if params.get('from_path'): path = params['from_path'] f = open(path, 'rb') content = django_files.File(f, name=path) @@ -196,19 +190,12 @@ def _make_content(self, extraction_context): filename = params.get('filename', default_filename) return filename, content - def call(self, obj, create, extraction_context): + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Fill in the field.""" - if extraction_context.did_extract and extraction_context.value is None: - # User passed an empty value, don't fill - return - filename, content = self._make_content(extraction_context) - field_file = getattr(obj, extraction_context.for_field) - try: - field_file.save(filename, content, save=create) - finally: - content.file.close() - return field_file + filename, content = self._make_content(extra) + print("Returning file with filename=%r, contents=%r" % (filename, content)) + return django_files.File(content.file, filename) class ImageField(FileField): diff --git a/tests/test_django.py b/tests/test_django.py index a8f1f771..cf80edb0 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -364,6 +364,9 @@ def test_default_build(self): o = WithFileFactory.build() self.assertIsNone(o.pk) self.assertEqual(b'', o.afile.read()) + self.assertEqual('example.dat', o.afile.name) + + o.save() self.assertEqual('django/example.dat', o.afile.name) def test_default_create(self): @@ -375,19 +378,26 @@ def test_default_create(self): def test_with_content(self): o = WithFileFactory.build(afile__data='foo') self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'foo', o.afile.read()) self.assertEqual('django/example.dat', o.afile.name) def test_with_file(self): with open(testdata.TESTFILE_PATH, 'rb') as f: o = WithFileFactory.build(afile__from_file=f) - self.assertIsNone(o.pk) + o.save() + self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) def test_with_path(self): o = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH) self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) @@ -397,7 +407,9 @@ def test_with_file_empty_path(self): afile__from_file=f, afile__from_path='' ) - self.assertIsNone(o.pk) + # Django only allocates the full path on save() + o.save() + self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) @@ -407,6 +419,9 @@ def test_with_path_empty_file(self): afile__from_file=None, ) self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.data', o.afile.name) @@ -422,14 +437,21 @@ def test_override_filename_with_path(self): afile__filename='example.foo', ) self.assertIsNone(o.pk) + + # Django only allocates the full path on save() + o.save() self.assertEqual(b'example_data\n', o.afile.read()) self.assertEqual('django/example.foo', o.afile.name) def test_existing_file(self): o1 = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH) + o1.save() + self.assertEqual('django/example.data', o1.afile.name) - o2 = WithFileFactory.build(afile=o1.afile) + o2 = WithFileFactory.build(afile__from_file=o1.afile) self.assertIsNone(o2.pk) + o2.save() + self.assertEqual(b'example_data\n', o2.afile.read()) self.assertNotEqual('django/example.data', o2.afile.name) self.assertRegexpMatches(o2.afile.name, r'django/example_\w+.data') @@ -453,6 +475,8 @@ def tearDown(self): def test_default_build(self): o = WithImageFactory.build() self.assertIsNone(o.pk) + o.save() + self.assertEqual(100, o.animage.width) self.assertEqual(100, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -460,6 +484,8 @@ def test_default_build(self): def test_default_create(self): o = WithImageFactory.create() self.assertIsNotNone(o.pk) + o.save() + self.assertEqual(100, o.animage.width) self.assertEqual(100, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -467,6 +493,8 @@ def test_default_create(self): def test_with_content(self): o = WithImageFactory.build(animage__width=13, animage__color='red') self.assertIsNone(o.pk) + o.save() + self.assertEqual(13, o.animage.width) self.assertEqual(13, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -480,6 +508,8 @@ def test_with_content(self): def test_gif(self): o = WithImageFactory.build(animage__width=13, animage__color='blue', animage__format='GIF') self.assertIsNone(o.pk) + o.save() + self.assertEqual(13, o.animage.width) self.assertEqual(13, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) @@ -493,7 +523,8 @@ def test_gif(self): def test_with_file(self): with open(testdata.TESTIMAGE_PATH, 'rb') as f: o = WithImageFactory.build(animage__from_file=f) - self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -501,6 +532,8 @@ def test_with_file(self): def test_with_path(self): o = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH) self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -511,7 +544,8 @@ def test_with_file_empty_path(self): animage__from_file=f, animage__from_path='' ) - self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -522,6 +556,8 @@ def test_with_path_empty_file(self): animage__from_file=None, ) self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.jpeg', o.animage.name) @@ -538,15 +574,20 @@ def test_override_filename_with_path(self): animage__filename='example.foo', ) self.assertIsNone(o.pk) + o.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o.animage.read())) self.assertEqual('django/example.foo', o.animage.name) def test_existing_file(self): o1 = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH) + o1.save() - o2 = WithImageFactory.build(animage=o1.animage) + o2 = WithImageFactory.build(animage__from_file=o1.animage) self.assertIsNone(o2.pk) + o2.save() + # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o2.animage.read())) self.assertNotEqual('django/example.jpeg', o2.animage.name) From 35f9ee112f5b3dfb799e24635d548fd228c98db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:40:07 +0100 Subject: [PATCH 079/714] Release v2.5.0 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c2731efc..4018e32f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.5.0: -2.5.0 (master) --------------- +2.5.0 (2015-03-26) +------------------ *New:* diff --git a/factory/__init__.py b/factory/__init__.py index 8fc8ef80..20b4d0fa 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.4.1' +__version__ = '2.5.0' __author__ = 'Raphaël Barrois ' From 8a3127f394283b367f15f43328a1c8751982898f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:41:11 +0100 Subject: [PATCH 080/714] Get ready for next release. --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4018e32f..0554eb71 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,12 @@ ChangeLog ========= +.. _v2.5.1: + +2.5.1 (master) +-------------- + + .. _v2.5.0: 2.5.0 (2015-03-26) From a1e5ff13c0573feb95c810e7e27cd30de97b8f21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:45:29 +0100 Subject: [PATCH 081/714] Update header years. --- LICENSE | 2 +- docs/conf.py | 2 +- factory/__init__.py | 2 +- factory/base.py | 2 +- factory/compat.py | 2 +- factory/containers.py | 2 +- factory/declarations.py | 2 +- factory/django.py | 2 +- factory/fuzzy.py | 2 +- factory/helpers.py | 2 +- factory/mogo.py | 2 +- factory/mongoengine.py | 2 +- factory/utils.py | 2 +- tests/__init__.py | 2 +- tests/compat.py | 2 +- tests/cyclic/bar.py | 2 +- tests/cyclic/foo.py | 2 +- tests/cyclic/self_ref.py | 2 +- tests/djapp/models.py | 2 +- tests/djapp/settings.py | 2 +- tests/test_alchemy.py | 2 +- tests/test_base.py | 2 +- tests/test_containers.py | 2 +- tests/test_declarations.py | 2 +- tests/test_django.py | 2 +- tests/test_fuzzy.py | 2 +- tests/test_helpers.py | 2 +- tests/test_using.py | 2 +- tests/test_utils.py | 2 +- tests/testdata/__init__.py | 2 +- tests/tools.py | 2 +- tests/utils.py | 2 +- 32 files changed, 32 insertions(+), 32 deletions(-) diff --git a/LICENSE b/LICENSE index 620dc61f..d009218a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2010 Mark Sandstrom -Copyright (c) 2011-2013 Raphaël Barrois +Copyright (c) 2011-2015 Raphaël Barrois Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/conf.py b/docs/conf.py index ce6730b5..c3512e0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ # General information about the project. project = u'Factory Boy' -copyright = u'2011-2013, Raphaël Barrois, Mark Sandstrom' +copyright = u'2011-2015, Raphaël Barrois, Mark Sandstrom' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/factory/__init__.py b/factory/__init__.py index 20b4d0fa..ad313b34 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/base.py b/factory/base.py index 4c3e0add..d48edd50 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/compat.py b/factory/compat.py index 7747b1a6..785d174e 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/containers.py b/factory/containers.py index 5116320d..0ae354b8 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/declarations.py b/factory/declarations.py index b3833ee7..8f2314ac 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/django.py b/factory/django.py index 9d4cde98..7862d75f 100644 --- a/factory/django.py +++ b/factory/django.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 4e6a03dc..923d8b74 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/helpers.py b/factory/helpers.py index b9cef6e2..60a4d75c 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/mogo.py b/factory/mogo.py index 5541043f..c6c3c197 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/mongoengine.py b/factory/mongoengine.py index e3ab99c8..f50b727e 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/utils.py b/factory/utils.py index 276977a1..6ecf9a74 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/__init__.py b/tests/__init__.py index 5b6fc55b..c73165f2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois from .test_base import * from .test_containers import * diff --git a/tests/compat.py b/tests/compat.py index ff96f13e..167c1851 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index a5e6bf16..b4f8e0c8 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index 18de3624..62e58c04 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py index f18c9892..d98b3abc 100644 --- a/tests/cyclic/self_ref.py +++ b/tests/cyclic/self_ref.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 9b211814..35c765ff 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index 18e43dd1..1ef16d5e 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 2deb418d..9d7288a2 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2013 Romain Command& +# Copyright (c) 2015 Romain Command& # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_base.py b/tests/test_base.py index 12031b90..24f64e58 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_containers.py b/tests/test_containers.py index bd7019ef..083b306f 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 18a4cd4c..2601a388 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_django.py b/tests/test_django.py index cf80edb0..27440320 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index c7e1106c..3f9c434d 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f5a66e5e..bee66caf 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_using.py b/tests/test_using.py index 1d7977f0..b7fea81e 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_utils.py b/tests/test_utils.py index d321c2ab..eed7a571 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py index 99566104..b534998a 100644 --- a/tests/testdata/__init__.py +++ b/tests/testdata/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/tools.py b/tests/tools.py index 571899bc..47f705c1 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/utils.py b/tests/utils.py index 215fc83a..7a31ed2a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2013 Raphaël Barrois +# Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal From 140956f854b34164cce90bbaaa49255383a440c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 26 Mar 2015 23:52:41 +0100 Subject: [PATCH 082/714] Clarify impacts of 2.5.0. --- docs/changelog.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0554eb71..326e8f1e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,11 +25,16 @@ ChangeLog - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`). + *Deprecation:* - Remove deprecated features from :ref:`v2.4.0` - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy; - this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. + this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. See https://github.com/rbarrois/factory_boy/commit/13d310f for technical details. + +.. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature. + This could trigger some bugs when tests expected a non-zero sequence reference. + .. _v2.4.1: From d6f351c5af74ac659b4d3add916546d286ff4fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 13:44:15 +0100 Subject: [PATCH 083/714] Add upgrade instructions for 2.5.0 --- README.rst | 12 +++++++----- docs/changelog.rst | 30 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index c787ca8b..8bfbc24e 100644 --- a/README.rst +++ b/README.rst @@ -4,6 +4,8 @@ factory_boy .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ +Latest release: `2.5.0 `_ (includes breaking changes, see the `ChangeLog `_) + factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures @@ -281,12 +283,12 @@ This will yield messages similar to those (artificial indentation): ORM Support """"""""""" -factory_boy has specific support for a few ORMs, through specific :class:`~factory.Factory` subclasses: +factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses: -* Django, with :class:`~factory.django.DjangoModelFactory` -* Mogo, with :class:`~factory.mogo.MogoFactory` -* MongoEngine, with :class:`~factory.mongoengine.MongoEngineFactory` -* SQLAlchemy, with :class:`~factory.alchemy.SQLAlchemyModelFactory` +* Django, with ``factory.django.DjangoModelFactory`` +* Mogo, with ``factory.mogo.MogoFactory`` +* MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` +* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` Contributing ------------ diff --git a/docs/changelog.rst b/docs/changelog.rst index 326e8f1e..a7ff0506 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -35,6 +35,36 @@ ChangeLog .. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature. This could trigger some bugs when tests expected a non-zero sequence reference. +Upgrading +""""""""" + +.. warning:: Version 2.5.0 removes features that were marked as deprecated in :ref:`v2.4.0 `. + +All ``FACTORY_*``-style attributes are now declared in a ``class Meta:`` section: + +.. code-block:: python + + # Old-style, deprecated + class MyFactory(factory.Factory): + FACTORY_FOR = models.MyModel + FACTORY_HIDDEN_ARGS = ['a', 'b', 'c'] + + # New-style + class MyFactory(factory.Factory): + class Meta: + model = models.MyModel + exclude = ['a', 'b', 'c'] + +A simple shell command to upgrade the code would be: + +.. code-block:: sh + + # sed -i: inplace update + # grep -l: only file names, not matching lines + sed -i 's/FACTORY_FOR =/class Meta:\n model =/' $(grep -l FACTORY_FOR $(find . -name '*.py')) + +This takes care of all ``FACTORY_FOR`` occurences; the files containing other attributes to rename can be found with ``grep -R FACTORY .`` + .. _v2.4.1: From 03ca4ecebc914f2e120e902b8fcbe2b526460ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 13:50:07 +0100 Subject: [PATCH 084/714] Remove debug prints --- factory/django.py | 1 - 1 file changed, 1 deletion(-) diff --git a/factory/django.py b/factory/django.py index 7862d75f..eb07bfb8 100644 --- a/factory/django.py +++ b/factory/django.py @@ -194,7 +194,6 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): """Fill in the field.""" filename, content = self._make_content(extra) - print("Returning file with filename=%r, contents=%r" % (filename, content)) return django_files.File(content.file, filename) From bdc1b815cfdf3028379c6c3f18c9c47ee8298a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 16:27:05 +0100 Subject: [PATCH 085/714] Respect default manager in DjangoModelFactory (Closes #192). The previous version tries to use ``cls._default_manager`` all the time, which breaks with ``manager.using(db_name)``. --- docs/changelog.rst | 3 +++ factory/django.py | 13 ++++++------- tests/djapp/models.py | 17 +++++++++++++++++ tests/test_django.py | 12 ++++++++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a7ff0506..cc4a1dc1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,9 @@ ChangeLog 2.5.1 (master) -------------- +*Bugfix:* + + - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`) .. _v2.5.0: diff --git a/factory/django.py b/factory/django.py index eb07bfb8..ba81f135 100644 --- a/factory/django.py +++ b/factory/django.py @@ -45,6 +45,8 @@ logger = logging.getLogger('factory.generate') +DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS + def require_django(): """Simple helper to ensure Django is available.""" @@ -56,7 +58,7 @@ class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), - base.OptionDefault('database', 'default', inherit=True), + base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True), ] def _get_counter_reference(self): @@ -100,12 +102,9 @@ def _get_manager(cls, model_class): if model_class is None: raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) - try: - manager = model_class._default_manager # pylint: disable=W0212 - except AttributeError: - manager = model_class.objects - - manager = manager.using(cls._meta.database) + manager = model_class.objects + if cls._meta.database != DEFAULT_DB_ALIAS: + manager = manager.using(cls._meta.database) return manager @classmethod diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 35c765ff..96ee5cf3 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -87,3 +87,20 @@ class WithImage(models.Model): class WithSignals(models.Model): foo = models.CharField(max_length=20) + + +class CustomQuerySet(models.QuerySet): + pass + + +class CustomManager(models.Manager): + + def create(self, arg=None, **kwargs): + return super(CustomManager, self).create(**kwargs) + + +class WithCustomManager(models.Model): + + foo = models.CharField(max_length=20) + + objects = CustomManager.from_queryset(CustomQuerySet)() diff --git a/tests/test_django.py b/tests/test_django.py index 27440320..9ac8f5ca 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -157,6 +157,13 @@ class Meta: model = models.WithSignals + class WithCustomManagerFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithCustomManager + + foo = factory.Sequence(lambda n: "foo%d" % n) + + @unittest.skipIf(django is None, "Django not installed.") class ModelTests(django_test.TestCase): def test_unset_model(self): @@ -706,5 +713,10 @@ def generate(cls): self.assertSignalsReactivated() +class DjangoCustomManagerTestCase(django_test.TestCase): + + def test_extra_args(self): + model = WithCustomManagerFactory(arg='foo') + if __name__ == '__main__': # pragma: no cover unittest.main() From 5363951bb62ca90d971bf036851dea564204ed2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 17:00:32 +0100 Subject: [PATCH 086/714] Support declarations in FileField/ImageField. Previously, the declarations (``factory.Sequence`` & co) weren't properly computed. --- docs/changelog.rst | 2 ++ factory/django.py | 11 ++++++----- tests/djapp/models.py | 1 + tests/test_django.py | 11 +++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cc4a1dc1..de9778b0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ ChangeLog *Bugfix:* - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`) + - Allow passing declarations (e.g :class:`~factory.Sequence`) as parameters to :class:`~factory.django.FileField` + and :class:`~factory.django.ImageField`. .. _v2.5.0: diff --git a/factory/django.py b/factory/django.py index ba81f135..cbf7c107 100644 --- a/factory/django.py +++ b/factory/django.py @@ -147,6 +147,7 @@ class FileField(declarations.ParameteredAttribute): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' + EXTEND_CONTAINERS = True def __init__(self, **defaults): require_django() @@ -156,10 +157,8 @@ def _make_data(self, params): """Create data for the field.""" return params.get('data', b'') - def _make_content(self, extra): + def _make_content(self, params): path = '' - params = dict(self.defaults) - params.update(extra) if params.get('from_path') and params.get('from_file'): raise ValueError( @@ -189,10 +188,12 @@ def _make_content(self, extra): filename = params.get('filename', default_filename) return filename, content - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def generate(self, sequence, obj, create, params): """Fill in the field.""" - filename, content = self._make_content(extra) + params.setdefault('__sequence', sequence) + params = base.DictFactory.simple_generate(create, **params) + filename, content = self._make_content(params) return django_files.File(content.file, filename) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 96ee5cf3..1c1fd8ec 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -79,6 +79,7 @@ class WithFile(models.Model): class WithImage(models.Model): animage = models.ImageField(upload_to=WITHFILE_UPLOAD_TO) + size = models.IntegerField(default=0) else: class WithImage(models.Model): diff --git a/tests/test_django.py b/tests/test_django.py index 9ac8f5ca..ac527692 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -497,6 +497,17 @@ def test_default_create(self): self.assertEqual(100, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) + def test_complex_create(self): + o = WithImageFactory.create( + size=10, + animage__filename=factory.Sequence(lambda n: 'img%d.jpg' % n), + __sequence=42, + animage__width=factory.SelfAttribute('..size'), + animage__height=factory.SelfAttribute('width'), + ) + self.assertIsNotNone(o.pk) + self.assertEqual('django/img42.jpg', o.animage.name) + def test_with_content(self): o = WithImageFactory.build(animage__width=13, animage__color='red') self.assertIsNone(o.pk) From 48a1e4a65968a911d530c87cf0bcb9f312927641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 17:48:38 +0100 Subject: [PATCH 087/714] Release v2.5.1 --- docs/changelog.rst | 4 ++-- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index de9778b0..0d12cb3b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,8 @@ ChangeLog .. _v2.5.1: -2.5.1 (master) --------------- +2.5.1 (2015-03-27) +------------------ *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index ad313b34..378035f9 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.5.0' +__version__ = '2.5.1' __author__ = 'Raphaël Barrois ' From 71f5d76c5ecb2b88cd734c9f2611d7dfad1dd923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 27 Mar 2015 17:57:10 +0100 Subject: [PATCH 088/714] Fix custom queryset tests for Django<1.7 --- tests/djapp/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 1c1fd8ec..513c47c8 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -90,10 +90,6 @@ class WithSignals(models.Model): foo = models.CharField(max_length=20) -class CustomQuerySet(models.QuerySet): - pass - - class CustomManager(models.Manager): def create(self, arg=None, **kwargs): @@ -104,4 +100,4 @@ class WithCustomManager(models.Model): foo = models.CharField(max_length=20) - objects = CustomManager.from_queryset(CustomQuerySet)() + objects = CustomManager() From c77d97d950bcf6fab0061519922c4800e06ff711 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 2 Apr 2015 09:48:53 +0200 Subject: [PATCH 089/714] Fix imports for Django 1.8 --- tests/test_django.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index ac527692..9da99cc7 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -53,7 +53,10 @@ from django import test as django_test from django.conf import settings from django.db import models as django_models - from django.test import simple as django_test_simple + if django.VERSION <= (1, 8, 0): + from django.test.simple import DjangoTestSuiteRunner + else: + from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner from django.test import utils as django_test_utils from django.db.models import signals from .djapp import models @@ -71,7 +74,7 @@ def setUpModule(): if django is None: # pragma: no cover raise unittest.SkipTest("Django not installed") django_test_utils.setup_test_environment() - runner = django_test_simple.DjangoTestSuiteRunner() + runner = DjangoTestSuiteRunner() runner_state = runner.setup_databases() test_state.update({ 'runner': runner, From ae5d46af448fc33ef74eee99c5a3d686c8d26e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 12 Apr 2015 12:14:29 +0200 Subject: [PATCH 090/714] Fix tests with latest pymongo/mongoengine. mongoengine>=0.9.0 and pymongo>=2.1 require extra parameters: - The server connection timeout was set too high - We have to define a ``read_preference``. --- tests/test_mongoengine.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 988c1797..c0a019ca 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -61,10 +61,20 @@ class MongoEngineTestCase(unittest.TestCase): db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') db_host = os.environ.get('MONGO_HOST', 'localhost') db_port = int(os.environ.get('MONGO_PORT', '27017')) + MONGOD_TIMEOUT_MS = 100 @classmethod def setUpClass(cls): - cls.db = mongoengine.connect(cls.db_name, host=cls.db_host, port=cls.db_port) + from pymongo import read_preferences as mongo_rp + cls.db = mongoengine.connect( + db=cls.db_name, + host=cls.db_host, + port=cls.db_port, + # PyMongo>=2.1 requires an explicit read_preference. + read_preference=mongo_rp.ReadPreference.PRIMARY, + # PyMongo>=2.1 has a 20s timeout, use 100ms instead + serverselectiontimeoutms=cls.MONGOD_TIMEOUT_MS, + ) @classmethod def tearDownClass(cls): From c58a190b12535bdcfc984b1be8b72a6a2c84a2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 12 Apr 2015 12:20:53 +0200 Subject: [PATCH 091/714] mongoengine: allow tuning the server timeout. So that it doesn't fail on ci... --- tests/test_mongoengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index c0a019ca..1cf0cb50 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -61,7 +61,7 @@ class MongoEngineTestCase(unittest.TestCase): db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') db_host = os.environ.get('MONGO_HOST', 'localhost') db_port = int(os.environ.get('MONGO_PORT', '27017')) - MONGOD_TIMEOUT_MS = 100 + server_timeout_ms = int(os.environ.get('MONGO_TIMEOUT', '300')) @classmethod def setUpClass(cls): From 16b414d27d638fd76701f10fe338c67d7d9dfde0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 12 Apr 2015 12:23:49 +0200 Subject: [PATCH 092/714] test_mongoengine: fix typo --- tests/test_mongoengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 1cf0cb50..6fa41259 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -73,7 +73,7 @@ def setUpClass(cls): # PyMongo>=2.1 requires an explicit read_preference. read_preference=mongo_rp.ReadPreference.PRIMARY, # PyMongo>=2.1 has a 20s timeout, use 100ms instead - serverselectiontimeoutms=cls.MONGOD_TIMEOUT_MS, + serverselectiontimeoutms=cls.server_timeout_ms, ) @classmethod From e357919cdb52af96eb67148fd38dced34981821a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 14 Apr 2015 00:20:33 +0200 Subject: [PATCH 093/714] Remove warnings with Django 1.7 (Closes #195). Builds upon pull request by @shinuza: - Properly import ``get_model`` - Run ``django.setup()`` before importing any models. --- factory/django.py | 17 +++++++++++++++-- tests/test_django.py | 4 ++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/factory/django.py b/factory/django.py index cbf7c107..74e4fdb8 100644 --- a/factory/django.py +++ b/factory/django.py @@ -32,8 +32,10 @@ """factory_boy extensions for use with the Django framework.""" try: + import django from django.core import files as django_files except ImportError as e: # pragma: no cover + django = None django_files = None import_failure = e @@ -54,6 +56,18 @@ def require_django(): raise import_failure +if django is None: + def get_model(app, model): + raise import_failure + +elif django.VERSION[:2] < (1, 7): + from django.db.models.loading import get_model + +else: + from django import apps as django_apps + get_model = django_apps.apps.get_model + + class DjangoOptions(base.FactoryOptions): def _build_default_options(self): return super(DjangoOptions, self)._build_default_options() + [ @@ -92,8 +106,7 @@ def _load_model_class(cls, definition): if is_string(definition) and '.' in definition: app, model = definition.split('.', 1) - from django.db.models import loading as django_loading - return django_loading.get_model(app, model) + return get_model(app, model) return definition diff --git a/tests/test_django.py b/tests/test_django.py index 9da99cc7..113caeb0 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -50,6 +50,8 @@ if django is not None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') + if django.VERSION >= (1, 7, 0): + django.setup() from django import test as django_test from django.conf import settings from django.db import models as django_models @@ -64,8 +66,6 @@ else: django_test = unittest -if django is not None and django.VERSION >= (1, 7, 0): - django.setup() test_state = {} From 52c984d3f1c6c440a832e53331d7f95f25c8b046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 14 Apr 2015 00:22:08 +0200 Subject: [PATCH 094/714] Fix minor typo (Closes #194). Thanks to @DasAllFolks for spotting it! --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 9fd2576c..44f78b69 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -42,7 +42,7 @@ The :class:`Factory` class It will be automatically set to ``True`` if neither the :class:`Factory` subclass nor its parents define the :attr:`~FactoryOptions.model` attribute. - .. warning:: This flag is reset to ``False`` When a :class:`Factory` subclasses + .. warning:: This flag is reset to ``False`` when a :class:`Factory` subclasses another one if a :attr:`~FactoryOptions.model` is set. .. versionadded:: 2.4.0 From 23616f4a376f79aaf0f9088dc15dc87a668e1ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 19 Apr 2015 17:00:53 +0200 Subject: [PATCH 095/714] Update travis config: focus on Py2.7/Py3.4 --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3954bf50..a1d14f6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,15 @@ language: python python: - - "2.6" - "2.7" - - "3.2" - - "3.3" + - "3.4" - "pypy" script: - python setup.py test install: - - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install unittest2 --use-mirrors; fi - - if [[ $TRAVIS_PYTHON_VERSION = 2.6 ]]; then pip install "Django<1.7" --use-mirrors; else pip install Django; fi + - pip install Django - pip install sqlalchemy --use-mirrors - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi From 3bb4a0eb6170a588434fbb6438ec1a063eb115e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:31:15 +0200 Subject: [PATCH 096/714] Add wheel support --- dev_requirements.txt | 1 + setup.cfg | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 setup.cfg diff --git a/dev_requirements.txt b/dev_requirements.txt index bdc23d05..a8dd896b 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -4,3 +4,4 @@ Pillow sqlalchemy mongoengine mock +wheel diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..2a9acf13 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 From 86d1b29ea5e508c37320ad66949d2d8d33b1db02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:31:43 +0200 Subject: [PATCH 097/714] Add badges to README. --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 8bfbc24e..9ffa8095 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,18 @@ factory_boy =========== +.. image:: https://pypip.in/version/factory_boy/badge.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: Latest Version + +.. image:: https://pypip.in/py_versions/factory_boy/badge.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: Supported Python versions + +.. image:: https://pypip.in/wheel/factory_boy/badge.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: Wheel status + .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ From 0e73c401d3a0aa0780bf428cc2f4cea142118ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:33:08 +0200 Subject: [PATCH 098/714] Declare Python3.4 support --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f637a481..9c8f8d24 100755 --- a/setup.py +++ b/setup.py @@ -63,6 +63,7 @@ def get_version(package_name): "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules" From f73be3fc0d8c51aef3fcd890e61bb5ba74c55909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:33:50 +0200 Subject: [PATCH 099/714] Fix typo in setup.py (Closes #197). Thanks to @nikolas for spotting it! --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9c8f8d24..daee69b2 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def get_version(package_name): setup( name='factory_boy', version=get_version(PACKAGE), - description="A verstile test fixtures replacement based on thoughtbot's factory_girl for Ruby.", + description="A versatile test fixtures replacement based on thoughtbot's factory_girl for Ruby.", author='Mark Sandstrom', author_email='mark@deliciouslynerdy.com', maintainer='Raphaël Barrois', From 24269d9265ee2e3f53ca9f4bdbb01c79470988df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:38:19 +0200 Subject: [PATCH 100/714] Release v2.5.2 --- docs/changelog.rst | 9 +++++++++ factory/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0d12cb3b..8f635674 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.5.2: + +2.5.2 (2015-04-21) +------------------ + +*Bugfix:* + + - Add support for Django 1.7/1.8 + - Add support for mongoengine>=0.9.0 / pymongo>=2.1 .. _v2.5.1: diff --git a/factory/__init__.py b/factory/__init__.py index 378035f9..ea8c4598 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.5.1' +__version__ = '2.5.2' __author__ = 'Raphaël Barrois ' From 9b8ad9be6f3b033e1e3673e4329ac63ba9fa07d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 21 Apr 2015 11:43:56 +0200 Subject: [PATCH 101/714] README: Remove duplicate "latest release" block. --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 9ffa8095..9492c67b 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ factory_boy =========== .. image:: https://pypip.in/version/factory_boy/badge.svg - :target: https://pypi.python.org/pypi/factory_boy/ + :target: http://factoryboy.readthedocs.org/en/latest/changelog.html :alt: Latest Version .. image:: https://pypip.in/py_versions/factory_boy/badge.svg @@ -16,8 +16,6 @@ factory_boy .. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master :target: http://travis-ci.org/rbarrois/factory_boy/ -Latest release: `2.5.0 `_ (includes breaking changes, see the `ChangeLog `_) - factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures From 0e3cdffac41250cddfe93388b1c9fc1547e77a67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 25 Apr 2015 17:50:50 +0200 Subject: [PATCH 102/714] Clarify .build() issue with Django>1.8 (Ref #198). From 1.8 onwards, this crashes: >>> a = MyModel() # Don't save >>> b = MyOtherModel(fkey_to_mymodel=a) In turn, it breaks: class MyModelFactory(factory.django.DjangoModelFactory): class Meta: model = MyModel class MyOtherModelFactory(factory.django.DjangoModelFactory): class Meta: model = MyOtherModel fkey_to_mymodel = factory.SubFactory(MyModelFactory) MyOtherModelFactory.build() # Breaks The error message is: Cannot assign "MyModel()": "MyModel" instance isn't saved in the database. See https://code.djangoproject.com/ticket/10811 for details. --- docs/orms.rst | 8 ++++++++ tests/djapp/models.py | 9 +++++++++ tests/test_django.py | 26 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index 5105e66a..bbe91e6e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -40,6 +40,14 @@ All factories for a Django :class:`~django.db.models.Model` should use the once all post-generation hooks have run. +.. note:: Starting with Django 1.8, it is no longer possible to call ``.build()`` + on a factory if this factory uses a :class:`~factory.SubFactory` pointing + to another model: Django refuses to set a :class:`~djang.db.models.ForeignKey` + to an unsaved :class:`~django.db.models.Model` instance. + + See https://code.djangoproject.com/ticket/10811 for details. + + .. class:: DjangoOptions(factory.base.FactoryOptions) The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters: diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 513c47c8..68b9709b 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -68,6 +68,15 @@ class StandardSon(StandardModel): pass +class PointedModel(models.Model): + foo = models.CharField(max_length=20) + + +class PointingModel(models.Model): + foo = models.CharField(max_length=20) + pointed = models.OneToOneField(PointedModel, related_name='pointer', null=True) + + WITHFILE_UPLOAD_TO = 'django' WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO) diff --git a/tests/test_django.py b/tests/test_django.py index 113caeb0..33d159d3 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -361,6 +361,32 @@ def test_auto_sequence(self): self.assertEqual(1, obj.pk) +@unittest.skipIf(django is None, "Django not installed.") +class DjangoRelatedFieldTestCase(django_test.TestCase): + + @classmethod + def setUpClass(cls): + super(DjangoRelatedFieldTestCase, cls).setUpClass() + class PointedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointedModel + foo = 'ahah' + + class PointerFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointingModel + pointed = factory.SubFactory(PointedFactory, foo='hihi') + foo = 'bar' + + cls.PointedFactory = PointedFactory + cls.PointerFactory = PointerFactory + + def test_direct_related_create(self): + ptr = self.PointerFactory() + self.assertEqual('hihi', ptr.pointed.foo) + self.assertEqual(ptr.pointed, models.PointedModel.objects.get()) + + @unittest.skipIf(django is None, "Django not installed.") class DjangoFileFieldTestCase(unittest.TestCase): From bb7939b061f468f977caba8e5fdaaff62096e7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 15:29:18 +0200 Subject: [PATCH 103/714] Simplify dependencies installation for multi-version. You may now use: ``make DJANGO_VERSION=1.7 test``. Valid options: * ``DJANGO_VERSION`` * ``MONGOENGINE_VERSION`` * ``ALCHEMY_VERSION`` --- .gitignore | 1 + .travis.yml | 4 +--- Makefile | 31 ++++++++++++++++++++++++++++--- dev_requirements.txt | 2 +- requirements.txt | 0 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index b4d25fca..5437c432 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # Build-related files docs/_build/ +auto_dev_requirements*.txt .coverage .tox *.egg-info diff --git a/.travis.yml b/.travis.yml index a1d14f6d..ed331d48 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,9 +9,7 @@ script: - python setup.py test install: - - pip install Django - - pip install sqlalchemy --use-mirrors - - if ! python --version 2>&1 | grep -q -i pypy ; then pip install Pillow --use-mirrors; fi + - make install-deps notifications: email: false diff --git a/Makefile b/Makefile index bb0428b6..9841b312 100644 --- a/Makefile +++ b/Makefile @@ -5,26 +5,51 @@ DOC_DIR=docs # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) +# Dependencies +DJANGO_VERSION ?= 1.8 +NEXT_DJANGO_VERSION = $(shell python -c "v='$(DJANGO_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") + +ALCHEMY_VERSION ?= 1.0 +NEXT_ALCHEMY_VERSION = $(shell python -c "v='$(ALCHEMY_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") + +MONGOENGINE_VERSION ?= 0.9 +NEXT_MONGOENGINE_VERSION = $(shell python -c "v='$(MONGOENGINE_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") + +REQ_FILE = auto_dev_requirements_django$(DJANGO_VERSION)_alchemy$(ALCHEMY_VERSION)_mongoengine$(MONGOENGINE_VERSION).txt + all: default default: +install-deps: $(REQ_FILE) + pip install --upgrade pip setuptools + pip install --upgrade -r $< + pip freeze + +$(REQ_FILE): dev_requirements.txt requirements.txt + grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@ + echo "Django>=$(DJANGO_VERSION),<$(NEXT_DJANGO_VERSION)" >> $@ + echo "SQLAlchemy>=$(ALCHEMY_VERSION),<$(NEXT_ALCHEMY_VERSION)" >> $@ + echo "mongoengine>=$(MONGOENGINE_VERSION),<$(NEXT_MONGOENGINE_VERSION)" >> $@ + + clean: find . -type f -name '*.pyc' -delete find . -type f -path '*/__pycache__/*' -delete find . -type d -empty -delete + @rm -f auto_dev_requirements_* @rm -rf tmp_test/ -test: +test: install-deps python -W default setup.py test pylint: pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ -coverage: +coverage: install-deps $(COVERAGE) erase $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" @@ -34,4 +59,4 @@ doc: $(MAKE) -C $(DOC_DIR) html -.PHONY: all default clean coverage doc pylint test +.PHONY: all default clean coverage doc install-deps pylint test diff --git a/dev_requirements.txt b/dev_requirements.txt index a8dd896b..7c291859 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,7 +1,7 @@ coverage Django Pillow -sqlalchemy +SQLAlchemy mongoengine mock wheel diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e69de29b From 526293fccdc2661d6b0d68e524dc32aa858a3435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 15:30:14 +0200 Subject: [PATCH 104/714] Fix test startup for Django==1.6 --- tests/__init__.py | 4 +++- tests/test_django.py | 40 +++++++++++++++++++++------------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index c73165f2..dc1a119c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # Copyright (c) 2011-2015 Raphaël Barrois +# factory.django needs a configured Django. +from .test_django import * + from .test_base import * from .test_containers import * from .test_declarations import * -from .test_django import * from .test_fuzzy import * from .test_helpers import * from .test_using import * diff --git a/tests/test_django.py b/tests/test_django.py index 33d159d3..2cfb55c9 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -22,31 +22,13 @@ import os -import factory -import factory.django - try: import django except ImportError: # pragma: no cover django = None -try: - from PIL import Image -except ImportError: # pragma: no cover - # Try PIL alternate name - try: - import Image - except ImportError: - # OK, not installed - Image = None - - -from .compat import is_python2, unittest, mock -from . import testdata -from . import tools - - +# Setup Django as soon as possible if django is not None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') @@ -67,6 +49,26 @@ django_test = unittest + +try: + from PIL import Image +except ImportError: # pragma: no cover + # Try PIL alternate name + try: + import Image + except ImportError: + # OK, not installed + Image = None + + +import factory +import factory.django + +from .compat import is_python2, unittest, mock +from . import testdata +from . import tools + + test_state = {} From d0de4c4bbc8d495f0dc6d4023f096e00118b3d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 15:43:19 +0200 Subject: [PATCH 105/714] Update testing instructions. --- README.rst | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 9492c67b..ac1986c1 100644 --- a/README.rst +++ b/README.rst @@ -311,20 +311,28 @@ All pull request should pass the test suite, which can be launched simply with: .. code-block:: sh - $ python setup.py test + $ make test -.. note:: - Running test requires the unittest2 (standard in Python 2.7+) and mock libraries. +In order to test coverage, please use: + +.. code-block:: sh + $ make coverage -In order to test coverage, please use: + +To test with a specific framework version, you may use: .. code-block:: sh - $ pip install coverage - $ coverage erase; coverage run --branch setup.py test; coverage report + $ make DJANGO_VERSION=1.7 test + +Valid options are: + +* ``DJANGO_VERSION`` for ``Django`` +* ``MONGOENGINE_VERSION`` for ``mongoengine`` +* ``ALCHEMY_VERSION`` for ``SQLAlchemy`` Contents, indices and tables From e95475d492ea4e08ebc9b99e1851861df1eb83c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 16:15:54 +0200 Subject: [PATCH 106/714] Simpler way to define version names. Avoid hitting bugs with max shebang line length in jenkins. --- Makefile | 20 ++++++++++---------- README.rst | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index 9841b312..88830150 100644 --- a/Makefile +++ b/Makefile @@ -6,16 +6,16 @@ DOC_DIR=docs COVERAGE = python $(shell which coverage) # Dependencies -DJANGO_VERSION ?= 1.8 -NEXT_DJANGO_VERSION = $(shell python -c "v='$(DJANGO_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") +DJANGO ?= 1.8 +NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -ALCHEMY_VERSION ?= 1.0 -NEXT_ALCHEMY_VERSION = $(shell python -c "v='$(ALCHEMY_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") +ALCHEMY ?= 1.0 +NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -MONGOENGINE_VERSION ?= 0.9 -NEXT_MONGOENGINE_VERSION = $(shell python -c "v='$(MONGOENGINE_VERSION)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") +MONGOENGINE ?= 0.9 +NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -REQ_FILE = auto_dev_requirements_django$(DJANGO_VERSION)_alchemy$(ALCHEMY_VERSION)_mongoengine$(MONGOENGINE_VERSION).txt +REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt all: default @@ -30,9 +30,9 @@ install-deps: $(REQ_FILE) $(REQ_FILE): dev_requirements.txt requirements.txt grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@ - echo "Django>=$(DJANGO_VERSION),<$(NEXT_DJANGO_VERSION)" >> $@ - echo "SQLAlchemy>=$(ALCHEMY_VERSION),<$(NEXT_ALCHEMY_VERSION)" >> $@ - echo "mongoengine>=$(MONGOENGINE_VERSION),<$(NEXT_MONGOENGINE_VERSION)" >> $@ + echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@ + echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@ + echo "mongoengine>=$(MONGOENGINE),<$(NEXT_MONGOENGINE)" >> $@ clean: diff --git a/README.rst b/README.rst index ac1986c1..59ea0cfc 100644 --- a/README.rst +++ b/README.rst @@ -326,13 +326,13 @@ To test with a specific framework version, you may use: .. code-block:: sh - $ make DJANGO_VERSION=1.7 test + $ make DJANGO=1.7 test Valid options are: -* ``DJANGO_VERSION`` for ``Django`` -* ``MONGOENGINE_VERSION`` for ``mongoengine`` -* ``ALCHEMY_VERSION`` for ``SQLAlchemy`` +* ``DJANGO`` for ``Django`` +* ``MONGOENGINE`` for ``mongoengine`` +* ``ALCHEMY`` for ``SQLAlchemy`` Contents, indices and tables From 29de94f46b356bef181e8cf02d6cb3ae4ac52075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 16:39:10 +0200 Subject: [PATCH 107/714] Allow skipping Mongo tests. --- .travis.yml | 2 +- tests/test_mongoengine.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index ed331d48..e1600bdf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - "pypy" script: - - python setup.py test + - SKIP_MONGOENGINE=1 python setup.py test install: - make install-deps diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 6fa41259..7badd43b 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -31,6 +31,9 @@ except ImportError: mongoengine = None +if os.environ.get('SKIP_MONGOENGINE') == 1: + mongoengine = None + if mongoengine: from factory.mongoengine import MongoEngineFactory From 536ac1b0fe7c4a04ad144022d6394b994feccdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 27 Apr 2015 16:48:30 +0200 Subject: [PATCH 108/714] Fix typo. --- tests/test_mongoengine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 7badd43b..148d2744 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -31,7 +31,7 @@ except ImportError: mongoengine = None -if os.environ.get('SKIP_MONGOENGINE') == 1: +if os.environ.get('SKIP_MONGOENGINE') == '1': mongoengine = None if mongoengine: From fa6d60d17ddb7b70c6bc2337d901ef8cc924e67b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 20 May 2015 23:24:45 +0200 Subject: [PATCH 109/714] Add Meta.rename to handle name conflicts (See #206). Define ``Meta.rename = {'attrs': 'attributes'}`` if your model expects a ``attributes`` kwarg but you can't define it since it's already reserved by the ``Factory`` class. --- docs/changelog.rst | 9 +++++++++ docs/reference.rst | 23 ++++++++++++++++++++++- factory/base.py | 8 ++++++++ tests/test_using.py | 15 +++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8f635674..cd5d2812 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.6.0: + +2.6.0 (XXXX-XX-XX) +------------------ + +*New:* + + - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) + .. _v2.5.2: 2.5.2 (2015-04-21) diff --git a/docs/reference.rst b/docs/reference.rst index 44f78b69..0705ca24 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -106,6 +106,28 @@ The :class:`Factory` class .. versionadded:: 2.4.0 + .. attribute:: rename + + Sometimes, a model expect a field with a name already used by one + of :class:`Factory`'s methods. + + In this case, the :attr:`rename` attributes allows to define renaming + rules: the keys of the :attr:`rename` dict are those used in the + :class:`Factory` declarations, and their values the new name: + + .. code-block:: python + + class ImageFactory(factory.Factory): + # The model expects "attributes" + form_attributes = ['thumbnail', 'black-and-white'] + + class Meta: + model = Image + rename = {'form_attributes': 'attributes'} + + .. versionadded: 2.6.0 + + .. attribute:: strategy Use this attribute to change the strategy used by a :class:`Factory`. @@ -229,7 +251,6 @@ The :class:`Factory` class .. OHAI_VIM** - .. classmethod:: _setup_next_sequence(cls) This method will compute the first value to use for the sequence counter diff --git a/factory/base.py b/factory/base.py index d48edd50..0f2af597 100644 --- a/factory/base.py +++ b/factory/base.py @@ -176,6 +176,7 @@ def _build_default_options(self): OptionDefault('strategy', CREATE_STRATEGY, inherit=True), OptionDefault('inline_args', (), inherit=True), OptionDefault('exclude', (), inherit=True), + OptionDefault('rename', {}, inherit=True), ] def _fill_from_meta(self, meta, base_meta): @@ -412,6 +413,12 @@ def declarations(cls, extra_defs=None): decls.update(extra_defs or {}) return decls + @classmethod + def _rename_fields(cls, **kwargs): + for old_name, new_name in cls._meta.rename.items(): + kwargs[new_name] = kwargs.pop(old_name) + return kwargs + @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" @@ -441,6 +448,7 @@ def _prepare(cls, create, **kwargs): **kwargs: arguments to pass to the creation function """ model_class = cls._get_model_class() + kwargs = cls._rename_fields(**kwargs) kwargs = cls._adjust_kwargs(**kwargs) # Remove 'hidden' arguments. diff --git a/tests/test_using.py b/tests/test_using.py index b7fea81e..6d755316 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1076,6 +1076,21 @@ def _adjust_kwargs(cls, **kwargs): self.assertEqual({'x': 1, 'y': 2, 'z': 3, 'foo': 3}, obj.kwargs) self.assertEqual((), obj.args) + def test_rename(self): + class TestObject(object): + def __init__(self, attributes=None): + self.attributes = attributes + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + rename = {'attributes_': 'attributes'} + + attributes_ = 42 + + obj = TestObjectFactory.build() + self.assertEqual(42, obj.attributes) + class SubFactoryTestCase(unittest.TestCase): def test_sub_factory(self): From 939796a915d66722b0c3a286a12c88757d4eb137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 20 May 2015 23:32:33 +0200 Subject: [PATCH 110/714] Fix typo in docs/fuzzy (Closes #207). Thanks to @nikolas for spotting it! --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 18978e42..af5c490e 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -368,6 +368,6 @@ and provides a few helpers for this: Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD** -use :obj:`factory.fuzzy._random` foras a randomness source; this ensures that +use :obj:`factory.fuzzy._random` as a randomness source; this ensures that data they generate can be regenerated using the simple state from :meth:`get_random_state`. From da8d2e6323014be6065a9a490754173859fb95b4 Mon Sep 17 00:00:00 2001 From: Pauly Fenwar Date: Wed, 6 May 2015 13:54:31 +0100 Subject: [PATCH 111/714] Update README.rst - "attributes" is not a strategy (Closes #204). The wording of the readme suggested that "attributes" is a strategy just like "build" and "create", but this is not the case in the implementation (for example keyword arguments do not work, SubFactory fields don't behave as expected), so I have removed the mention of this and replaced the attributes example to mention the "stub" strategy. --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 59ea0cfc..991180f5 100644 --- a/README.rst +++ b/README.rst @@ -64,7 +64,7 @@ Its main features include: - Straightforward declarative syntax - Chaining factory calls while retaining the global context -- Support for multiple build strategies (saved/unsaved instances, attribute dicts, stubbed objects) +- Support for multiple build strategies (saved/unsaved instances, stubbed objects) - Multiple factories per class support, including inheritance @@ -135,7 +135,7 @@ The class of the object must be defined in the ``model`` field of a ``class Meta Using factories """"""""""""""" -factory_boy supports several different build strategies: build, create, attributes and stub: +factory_boy supports several different build strategies: build, create, and stub: .. code-block:: python @@ -145,8 +145,8 @@ factory_boy supports several different build strategies: build, create, attribut # Returns a saved User instance user = UserFactory.create() - # Returns a dict of attributes that can be used to build a User instance - attributes = UserFactory.attributes() + # Returns a stub object (just a bunch of attributes) + obj = UserFactory.stub() You can use the Factory class as a shortcut for the default build strategy: From 6f37f9be2d2e1bc75340068911db18b2bbcbe722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 22 May 2015 20:24:13 +0200 Subject: [PATCH 112/714] Add factory.Faker() This relies on the ``fake-factory`` library, and provides realistic random values for most field types. --- README.rst | 22 +++++++++ dev_requirements.txt | 2 + docs/changelog.rst | 2 + docs/reference.rst | 60 +++++++++++++++++++++++++ factory/__init__.py | 1 + factory/faker.py | 96 +++++++++++++++++++++++++++++++++++++++ requirement.txt | 1 + setup.py | 3 ++ tests/__init__.py | 1 + tests/test_faker.py | 105 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 293 insertions(+) create mode 100644 factory/faker.py create mode 100644 requirement.txt create mode 100644 tests/test_faker.py diff --git a/README.rst b/README.rst index 991180f5..9b824069 100644 --- a/README.rst +++ b/README.rst @@ -177,6 +177,28 @@ It is also possible to create a bunch of objects in a single call: >>> [user.first_name for user in users] ["Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe", "Joe"] + +Realistic, random values +"""""""""""""""""""""""" + +Tests look better with random yet realistic values. +For this, factory_boy relies on the excellent `fake-factory `_ library: + +.. code-block:: python + + class RandomUserFactory(factory.Factory): + class Meta: + model = models.User + + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + +.. code-block:: pycon + + >>> UserFactory() + + + Lazy Attributes """"""""""""""" diff --git a/dev_requirements.txt b/dev_requirements.txt index 7c291859..d55129a9 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,3 +1,5 @@ +-r requirements.txt + coverage Django Pillow diff --git a/docs/changelog.rst b/docs/changelog.rst index cd5d2812..886db0bd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,8 @@ ChangeLog *New:* - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) + - Add support for random-yet-realistic values through `fake-factory `_, + through the :class:`factory.Faker` class. .. _v2.5.2: diff --git a/docs/reference.rst b/docs/reference.rst index 0705ca24..a168de5c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -474,6 +474,66 @@ factory_boy supports two main strategies for generating instances, plus stubs. Declarations ------------ + +Faker +""""" + +.. class:: Faker(provider, locale=None, **kwargs) + + .. OHAIVIM** + + In order to easily define realistic-looking factories, + use the :class:`Faker` attribute declaration. + + This is a wrapper around `fake-factory `_; + its argument is the name of a ``fake-factory`` provider: + + .. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + first_name = factory.Faker('name') + + .. code-block:: pycon + + >>> user = UserFactory() + >>> user.name + 'Lucy Cechtelar' + + + .. attribute:: locale + + If a custom locale is required for one specific field, + use the ``locale`` parameter: + + .. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + first_name = factory.Faker('name', locale='fr_FR') + + .. code-block:: pycon + + >>> user = UserFactory() + >>> user.name + 'Jean Valjean' + + + .. classmethod:: override_default_locale(cls, locale) + + If the locale needs to be overridden for a whole test, + use :meth:`~factory.Faker.override_default_locale`: + + .. code-block:: pycon + + >>> with factory.Faker.override_default_locale('de_DE'): + ... UserFactory() + + LazyAttribute """"""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index ea8c4598..80eb6a8b 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -43,6 +43,7 @@ # Backward compatibility; this should be removed soon. from .mogo import MogoFactory from .django import DjangoModelFactory +from .faker import Faker from .declarations import ( LazyAttribute, diff --git a/factory/faker.py b/factory/faker.py new file mode 100644 index 00000000..10a0cba1 --- /dev/null +++ b/factory/faker.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2015 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Additional declarations for "faker" attributes. + +Usage: + + class MyFactory(factory.Factory): + class Meta: + model = MyProfile + + first_name = factory.Faker('name') +""" + + +from __future__ import absolute_import +from __future__ import unicode_literals + +import contextlib + +import faker +import faker.config + +from . import declarations + +class Faker(declarations.OrderedDeclaration): + """Wrapper for 'faker' values. + + Args: + provider (str): the name of the Faker field + locale (str): the locale to use for the faker + + All other kwargs will be passed to the underlying provider + (e.g ``factory.Faker('ean', length=10)`` + calls ``faker.Faker.ean(length=10)``) + + Usage: + >>> foo = factory.Faker('name') + """ + def __init__(self, provider, locale=None, **kwargs): + self.provider = provider + self.provider_kwargs = kwargs + self.locale = locale + + def generate(self, extra_kwargs): + kwargs = {} + kwargs.update(self.provider_kwargs) + kwargs.update(extra_kwargs) + faker = self._get_faker(self.locale) + return faker.format(self.provider, **kwargs) + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + return self.generate(extra or {}) + + _FAKER_REGISTRY = {} + _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE + + @classmethod + @contextlib.contextmanager + def override_default_locale(cls, locale): + old_locale = cls._DEFAULT_LOCALE + cls._DEFAULT_LOCALE = locale + try: + yield + finally: + cls._DEFAULT_LOCALE = old_locale + + @classmethod + def _get_faker(cls, locale=None): + if locale is None: + locale = cls._DEFAULT_LOCALE + + if locale not in cls._FAKER_REGISTRY: + cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale) + + return cls._FAKER_REGISTRY[locale] diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 00000000..bd2a4a63 --- /dev/null +++ b/requirement.txt @@ -0,0 +1 @@ +fake-factory>=0.5.0 diff --git a/setup.py b/setup.py index daee69b2..942fa2c6 100755 --- a/setup.py +++ b/setup.py @@ -47,6 +47,9 @@ def get_version(package_name): setup_requires=[ 'setuptools>=0.8', ], + install_requires=[ + 'fake-factory>=0.5.0', + ], tests_require=[ #'mock', ], diff --git a/tests/__init__.py b/tests/__init__.py index dc1a119c..b2c772d4 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,7 @@ from .test_base import * from .test_containers import * from .test_declarations import * +from .test_faker import * from .test_fuzzy import * from .test_helpers import * from .test_using import * diff --git a/tests/test_faker.py b/tests/test_faker.py new file mode 100644 index 00000000..41f8e191 --- /dev/null +++ b/tests/test_faker.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2015 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +import factory +import unittest + + +class MockFaker(object): + def __init__(self, expected): + self.expected = expected + + def format(self, provider, **kwargs): + return self.expected[provider] + + +class FakerTests(unittest.TestCase): + def setUp(self): + self._real_fakers = factory.Faker._FAKER_REGISTRY + factory.Faker._FAKER_REGISTRY = {} + + def tearDown(self): + factory.Faker._FAKER_REGISTRY = self._real_fakers + + def _setup_mock_faker(self, locale=None, **definitions): + if locale is None: + locale = factory.Faker._DEFAULT_LOCALE + factory.Faker._FAKER_REGISTRY[locale] = MockFaker(definitions) + + def test_simple_biased(self): + self._setup_mock_faker(name="John Doe") + faker_field = factory.Faker('name') + self.assertEqual("John Doe", faker_field.generate({})) + + def test_full_factory(self): + class Profile(object): + def __init__(self, first_name, last_name, email): + self.first_name = first_name + self.last_name = last_name + self.email = email + + class ProfileFactory(factory.Factory): + class Meta: + model = Profile + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name', locale='fr_FR') + email = factory.Faker('email') + + self._setup_mock_faker(first_name="John", last_name="Doe", email="john.doe@example.org") + self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@exemple.fr", locale='fr_FR') + + profile = ProfileFactory() + self.assertEqual("John", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + self.assertEqual('john.doe@example.org', profile.email) + + def test_override_locale(self): + class Profile(object): + def __init__(self, first_name, last_name): + self.first_name = first_name + self.last_name = last_name + + class ProfileFactory(factory.Factory): + class Meta: + model = Profile + + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name', locale='fr_FR') + + self._setup_mock_faker(first_name="John", last_name="Doe") + self._setup_mock_faker(first_name="Jean", last_name="Valjean", locale='fr_FR') + self._setup_mock_faker(first_name="Johannes", last_name="Brahms", locale='de_DE') + + profile = ProfileFactory() + self.assertEqual("John", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + + with factory.Faker.override_default_locale('de_DE'): + profile = ProfileFactory() + self.assertEqual("Johannes", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + + profile = ProfileFactory() + self.assertEqual("John", profile.first_name) + self.assertEqual("Valjean", profile.last_name) + From ebc89520d3f7589da35d4e7b78637fbe7d4d664a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 24 May 2015 18:21:04 +0200 Subject: [PATCH 113/714] Add lazy loading to factory.Iterator. factory.Iterator no longers begins iteration of its argument on declaration, since this behavior may trigger database query when that argument is, for instance, a Django queryset. The ``factory.Iterator``'s argument will only be called when the containing ``Factory`` is first evaluated; this means that factories using ``factory.Iterator(models.MyThingy.objects.all())`` will no longer call the database at import time. --- docs/changelog.rst | 3 +++ docs/recipes.rst | 23 +++++++++++++++++++++++ factory/declarations.py | 11 +++++++++-- setup.py | 8 ++++---- tests/test_django.py | 2 +- tests/test_using.py | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 886db0bd..0cbd4af0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,9 @@ ChangeLog - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) - Add support for random-yet-realistic values through `fake-factory `_, through the :class:`factory.Faker` class. + - :class:`factory.Iterator` no longer begins iteration of its argument at import time, + thus allowing to pass in a lazy iterator such as a Django queryset + (i.e ``factory.Iterator(models.MyThingy.objects.all())``). .. _v2.5.2: diff --git a/docs/recipes.rst b/docs/recipes.rst index 70eca46b..3cbe6d2a 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -33,6 +33,29 @@ use the :class:`~factory.SubFactory` declaration: group = factory.SubFactory(GroupFactory) +Choosing from a populated table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the target of the :class:`~django.db.models.ForeignKey` should be +chosen from a pre-populated table +(e.g :class:`django.contrib.contenttypes.models.ContentType`), +simply use a :class:`factory.Iterator` on the chosen queryset: + +.. code-block:: python + + import factory, factory.django + from . import models + + class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.User + + language = factory.Iterator(models.Language.objects.all()) + +Here, ``models.Language.objects.all()`` won't be evaluated until the +first call to ``UserFactory``; thus avoiding DB queries at import time. + + Reverse dependencies (reverse ForeignKey) ----------------------------------------- diff --git a/factory/declarations.py b/factory/declarations.py index 8f2314ac..f0dbfe54 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -160,12 +160,19 @@ class Iterator(OrderedDeclaration): def __init__(self, iterator, cycle=True, getter=None): super(Iterator, self).__init__() self.getter = getter + self.iterator = None if cycle: - iterator = itertools.cycle(iterator) - self.iterator = utils.ResetableIterator(iterator) + self.iterator_builder = lambda: utils.ResetableIterator(itertools.cycle(iterator)) + else: + self.iterator_builder = lambda: utils.ResetableIterator(iterator) def evaluate(self, sequence, obj, create, extra=None, containers=()): + # Begin unrolling as late as possible. + # This helps with ResetableIterator(MyModel.objects.all()) + if self.iterator is None: + self.iterator = self.iterator_builder() + logger.debug("Iterator: Fetching next value from %r", self.iterator) value = next(iter(self.iterator)) if self.getter is None: diff --git a/setup.py b/setup.py index 942fa2c6..8ca7e4b2 100755 --- a/setup.py +++ b/setup.py @@ -44,12 +44,12 @@ def get_version(package_name): keywords=['factory_boy', 'factory', 'fixtures'], packages=['factory'], license='MIT', - setup_requires=[ - 'setuptools>=0.8', - ], install_requires=[ 'fake-factory>=0.5.0', ], + setup_requires=[ + 'setuptools>=0.8', + ], tests_require=[ #'mock', ], @@ -69,7 +69,7 @@ def get_version(package_name): "Programming Language :: Python :: 3.4", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Libraries :: Python Modules" + "Topic :: Software Development :: Libraries :: Python Modules", ], test_suite='tests', test_loader=test_loader, diff --git a/tests/test_django.py b/tests/test_django.py index 2cfb55c9..bde8efe2 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -21,6 +21,7 @@ """Tests for factory_boy/Django interactions.""" import os +from .compat import is_python2, unittest, mock try: @@ -64,7 +65,6 @@ import factory import factory.django -from .compat import is_python2, unittest, mock from . import testdata from . import tools diff --git a/tests/test_using.py b/tests/test_using.py index 6d755316..c7d2b857 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1493,6 +1493,38 @@ def one(): for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) + def test_iterator_late_loading(self): + """Ensure that Iterator doesn't unroll on class creation. + + This allows, for Django objects, to call: + foo = factory.Iterator(models.MyThingy.objects.all()) + """ + class DBRequest(object): + def __init__(self): + self.ready = False + + def __iter__(self): + if not self.ready: + raise ValueError("Not ready!!") + return iter([1, 2, 3]) + + # calling __iter__() should crash + req1 = DBRequest() + with self.assertRaises(ValueError): + iter(req1) + + req2 = DBRequest() + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = factory.Iterator(req2) + + req2.ready = True + obj = TestObjectFactory() + self.assertEqual(1, obj.one) + class BetterFakeModelManager(object): def __init__(self, keys, instance): From e9851a7d51afffea2a5679934ad6284c0835cfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 31 May 2015 10:15:27 +0100 Subject: [PATCH 114/714] Docs: fix minor typo. As spotted by @proofit404 --- docs/reference.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index a168de5c..4c7f8f78 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -494,7 +494,7 @@ Faker class Meta: model = User - first_name = factory.Faker('name') + name = factory.Faker('name') .. code-block:: pycon @@ -514,7 +514,7 @@ Faker class Meta: model = User - first_name = factory.Faker('name', locale='fr_FR') + name = factory.Faker('name', locale='fr_FR') .. code-block:: pycon From 0b5270eab393fad20faa7a6a9720af18c97b1773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 31 May 2015 10:47:28 +0100 Subject: [PATCH 115/714] Properly handle custom Django managers (Closes #201). The actual behavior of Django with custom managers and inherited abstract models is rather complex, so this had to be adapted to the actual Django source code. --- docs/changelog.rst | 4 ++++ factory/django.py | 9 ++++++++- tests/djapp/models.py | 11 +++++++++++ tests/test_django.py | 13 ++++++++++++- 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0cbd4af0..c871ce8d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,10 @@ ChangeLog thus allowing to pass in a lazy iterator such as a Django queryset (i.e ``factory.Iterator(models.MyThingy.objects.all())``). +*Bugfix:* + + - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. + .. _v2.5.2: 2.5.2 (2015-04-21) diff --git a/factory/django.py b/factory/django.py index 74e4fdb8..e4a3ea7e 100644 --- a/factory/django.py +++ b/factory/django.py @@ -115,7 +115,14 @@ def _get_manager(cls, model_class): if model_class is None: raise base.AssociatedClassError("No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) - manager = model_class.objects + + try: + manager = model_class.objects + except AttributeError: + # When inheriting from an abstract model with a custom + # manager, the class has no 'objects' field. + manager = model_class._default_manager + if cls._meta.database != DEFAULT_DB_ALIAS: manager = manager.using(cls._meta.database) return manager diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 68b9709b..cadefbc9 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -110,3 +110,14 @@ class WithCustomManager(models.Model): foo = models.CharField(max_length=20) objects = CustomManager() + + +class AbstractWithCustomManager(models.Model): + custom_objects = CustomManager() + + class Meta: + abstract = True + + +class FromAbstractWithCustomManager(AbstractWithCustomManager): + pass diff --git a/tests/test_django.py b/tests/test_django.py index bde8efe2..b8e7ccbe 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -755,10 +755,21 @@ def generate(cls): self.assertSignalsReactivated() -class DjangoCustomManagerTestCase(django_test.TestCase): +@unittest.skipIf(django is None, "Django not installed.") +class DjangoCustomManagerTestCase(unittest.TestCase): def test_extra_args(self): + # Our CustomManager will remove the 'arg=' argument. model = WithCustomManagerFactory(arg='foo') + def test_with_manager_on_abstract(self): + class ObjFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.FromAbstractWithCustomManager + + # Our CustomManager will remove the 'arg=' argument, + # invalid for the actual model. + ObjFactory.create(arg='invalid') + if __name__ == '__main__': # pragma: no cover unittest.main() From 9246fa6d26ca655c02ae37bbfc389d9f34dfba16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 31 May 2015 10:57:53 +0100 Subject: [PATCH 116/714] Improve ORM layer import paths (Closes #186). You may now use the following code: import factory factory.alchemy.SQLAlchemyModelFactory factory.django.DjangoModelFactory factory.mongoengine.MongoEngineFactory --- docs/changelog.rst | 2 ++ docs/orms.rst | 31 ++++++++++++++++++++++++++++++- factory/__init__.py | 12 +++++++++--- factory/alchemy.py | 1 - 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c871ce8d..eea38c5e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,8 @@ ChangeLog - :class:`factory.Iterator` no longer begins iteration of its argument at import time, thus allowing to pass in a lazy iterator such as a Django queryset (i.e ``factory.Iterator(models.MyThingy.objects.all())``). + - Simplify imports for ORM layers, now available through a simple ``factory`` import, + at ``factory.alchemy.SQLAlchemyModelFactory`` / ``factory.django.DjangoModelFactory`` / ``factory.mongoengine.MongoEngineFactory``. *Bugfix:* diff --git a/docs/orms.rst b/docs/orms.rst index bbe91e6e..26390b5c 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -273,6 +273,34 @@ factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngin This feature makes it possible to use :class:`~factory.SubFactory` to create embedded document. +A minimalist example: + +.. code-block:: python + + import mongoengine + + class Address(mongoengine.EmbeddedDocument): + street = mongoengine.StringField() + + class Person(mongoengine.Document): + name = mongoengine.StringField() + address = mongoengine.EmbeddedDocumentField(Address) + + import factory + + class AddressFactory(factory.mongoengine.MongoEngineFactory): + class Meta: + model = Address + + street = factory.Sequence(lambda n: 'street%d' % n) + + class PersonFactory(factory.mongoengine.MongoEngineFactory): + class Meta: + model = Person + + name = factory.Sequence(lambda n: 'name%d' % n) + address = factory.SubFactory(AddressFactory) + SQLAlchemy ---------- @@ -327,8 +355,9 @@ A (very) simple example: Base.metadata.create_all(engine) + import factory - class UserFactory(SQLAlchemyModelFactory): + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: model = User sqlalchemy_session = session # the SQLAlchemy session object diff --git a/factory/__init__.py b/factory/__init__.py index 80eb6a8b..843cf99f 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -40,9 +40,6 @@ use_strategy, ) -# Backward compatibility; this should be removed soon. -from .mogo import MogoFactory -from .django import DjangoModelFactory from .faker import Faker from .declarations import ( @@ -84,3 +81,12 @@ post_generation, ) +# Backward compatibility; this should be removed soon. +from . import alchemy +from . import django +from . import mogo +from . import mongoengine + +MogoFactory = mogo.MogoFactory +DjangoModelFactory = django.DjangoModelFactory + diff --git a/factory/alchemy.py b/factory/alchemy.py index 64083934..20da6cf5 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -19,7 +19,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. from __future__ import unicode_literals -from sqlalchemy.sql.functions import max from . import base From 5114549520e95c658716b7cbe9a7ab333d8ca524 Mon Sep 17 00:00:00 2001 From: Peter Marsh Date: Tue, 30 Jun 2015 17:32:41 +0100 Subject: [PATCH 117/714] Remove requirement.txt, move content into requirements.txt requirement.txt was introduced in 6f37f9b, after requirements.txt had already put in place. dev_requirements.txt installs the contents of requirements.txt (which is empty) while a single dependency is specified in requirement.txt. It looks like requirement.txt was added accidently and it's content should always have been in requirements.txt. This removes requirement.txt and puts the dependency delcared in there in requirements.txt. --- requirement.txt | 1 - requirements.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 requirement.txt diff --git a/requirement.txt b/requirement.txt deleted file mode 100644 index bd2a4a63..00000000 --- a/requirement.txt +++ /dev/null @@ -1 +0,0 @@ -fake-factory>=0.5.0 diff --git a/requirements.txt b/requirements.txt index e69de29b..bd2a4a63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +fake-factory>=0.5.0 From d471c1b4b0d4b06d557b5b6a9349a7dc55515d69 Mon Sep 17 00:00:00 2001 From: Ilya Baryshev Date: Thu, 2 Jul 2015 23:44:37 +0300 Subject: [PATCH 118/714] Fix mute_signals behavior for signals with caching Connecting signals (with use_caching=True) inside mute_signals was breaking unmute on exit. Paused receivers were not running. This was caused by signal cache not being restored after unpatching. Workaround is to clear signal cache on exit. Fixes #212 --- docs/changelog.rst | 1 + factory/django.py | 2 ++ tests/test_django.py | 13 +++++++++++++ 3 files changed, 16 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index eea38c5e..01d5775f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,7 @@ ChangeLog *Bugfix:* - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. + - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching .. _v2.5.2: diff --git a/factory/django.py b/factory/django.py index e4a3ea7e..5cc2b31a 100644 --- a/factory/django.py +++ b/factory/django.py @@ -277,6 +277,8 @@ def __exit__(self, exc_type, exc_value, traceback): receivers) signal.receivers = receivers + with signal.lock: + signal.sender_receivers_cache.clear() self.paused = {} def copy(self): diff --git a/tests/test_django.py b/tests/test_django.py index b8e7ccbe..103df911 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -678,6 +678,19 @@ def test_context_manager(self): self.assertSignalsReactivated() + def test_signal_cache(self): + with factory.django.mute_signals(signals.pre_save, signals.post_save): + signals.post_save.connect(self.handlers.mute_block_receiver) + WithSignalsFactory() + + self.assertTrue(self.handlers.mute_block_receiver.call_count, 1) + self.assertEqual(self.handlers.pre_init.call_count, 1) + self.assertFalse(self.handlers.pre_save.called) + self.assertFalse(self.handlers.post_save.called) + + self.assertSignalsReactivated() + self.assertTrue(self.handlers.mute_block_receiver.call_count, 1) + def test_class_decorator(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): From 6d190fa33f8a0cd625d3ce13d6de29bd5b72e742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 5 Jul 2015 17:00:21 +0200 Subject: [PATCH 119/714] Improve @coagulant's fixes to django signals (Closes #212). Signal caching didn't exist until Django 1.6. --- factory/django.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/factory/django.py b/factory/django.py index 5cc2b31a..b2af12c0 100644 --- a/factory/django.py +++ b/factory/django.py @@ -268,6 +268,9 @@ def __enter__(self): logger.debug('mute_signals: Disabling signal handlers %r', signal.receivers) + # Note that we're using implementation details of + # django.signals, since arguments to signal.connect() + # are lost in signal.receivers self.paused[signal] = signal.receivers signal.receivers = [] @@ -277,8 +280,12 @@ def __exit__(self, exc_type, exc_value, traceback): receivers) signal.receivers = receivers - with signal.lock: - signal.sender_receivers_cache.clear() + if django.VERSION[:2] >= (1, 6): + with signal.lock: + # Django uses some caching for its signals. + # Since we're bypassing signal.connect and signal.disconnect, + # we have to keep messing with django's internals. + signal.sender_receivers_cache.clear() self.paused = {} def copy(self): From 63edb526bc4efd8cf7abe260f2787f55d2953e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 11 Jul 2015 19:52:45 +0200 Subject: [PATCH 120/714] Improve debug logging efficiency (Closes #155). As suggested by @adamchainz, use lazy computation of args/kwargs pprint to only perform complex computation when running with debug. --- factory/utils.py | 32 +++++++++++++++++++++++--------- tests/test_utils.py | 14 +++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/factory/utils.py b/factory/utils.py index 6ecf9a74..806b1ec5 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -110,15 +110,29 @@ def _safe_repr(obj): return obj_repr.decode('utf-8') -def log_pprint(args=(), kwargs=None): - kwargs = kwargs or {} - return ', '.join( - [_safe_repr(arg) for arg in args] + - [ - '%s=%s' % (key, _safe_repr(value)) - for key, value in kwargs.items() - ] - ) +class log_pprint(object): + """Helper for properly printing args / kwargs passed to an object. + + Since it is only used with factory.debug(), the computation is + performed lazily. + """ + __slots__ = ['args', 'kwargs'] + + def __init__(self, args=(), kwargs=None): + self.args = args + self.kwargs = kwargs or {} + + def __repr__(self): + return repr(str(self)) + + def __str__(self): + return ', '.join( + [_safe_repr(arg) for arg in self.args] + + [ + '%s=%s' % (key, _safe_repr(value)) + for key, value in self.kwargs.items() + ] + ) class ResetableIterator(object): diff --git a/tests/test_utils.py b/tests/test_utils.py index eed7a571..77598e16 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -238,33 +238,33 @@ def test_invalid_module(self): class LogPPrintTestCase(unittest.TestCase): def test_nothing(self): - txt = utils.log_pprint() + txt = str(utils.log_pprint()) self.assertEqual('', txt) def test_only_args(self): - txt = utils.log_pprint((1, 2, 3)) + txt = str(utils.log_pprint((1, 2, 3))) self.assertEqual('1, 2, 3', txt) def test_only_kwargs(self): - txt = utils.log_pprint(kwargs={'a': 1, 'b': 2}) + txt = str(utils.log_pprint(kwargs={'a': 1, 'b': 2})) self.assertIn(txt, ['a=1, b=2', 'b=2, a=1']) def test_bytes_args(self): - txt = utils.log_pprint((b'\xe1\xe2',)) + txt = str(utils.log_pprint((b'\xe1\xe2',))) expected = "b'\\xe1\\xe2'" if is_python2: expected = expected.lstrip('b') self.assertEqual(expected, txt) def test_text_args(self): - txt = utils.log_pprint(('ŧêßŧ',)) + txt = str(utils.log_pprint(('ŧêßŧ',))) expected = "'ŧêßŧ'" if is_python2: expected = "u'\\u0167\\xea\\xdf\\u0167'" self.assertEqual(expected, txt) def test_bytes_kwargs(self): - txt = utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'}) + txt = str(utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'})) expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'" expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'" if is_python2: @@ -273,7 +273,7 @@ def test_bytes_kwargs(self): self.assertIn(txt, (expected1, expected2)) def test_text_kwargs(self): - txt = utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'}) + txt = str(utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'})) expected1 = "x='ŧêßŧ', y='ŧßêŧ'" expected2 = "y='ŧßêŧ', x='ŧêßŧ'" if is_python2: From b0fbd24c69a155c4f9d58f5e4dab8209afeb3660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 15 Jul 2015 23:15:13 +0200 Subject: [PATCH 121/714] Add examples folder. This should contain examples of "using factory_boy with third-party frameworks". --- Makefile | 6 ++- dev_requirements.txt | 1 + examples/Makefile | 9 ++++ examples/flask_alchemy/demoapp.py | 55 +++++++++++++++++++++ examples/flask_alchemy/demoapp_factories.py | 27 ++++++++++ examples/flask_alchemy/requirements.txt | 3 ++ examples/flask_alchemy/test_demoapp.py | 35 +++++++++++++ examples/requirements.txt | 1 + 8 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 examples/Makefile create mode 100644 examples/flask_alchemy/demoapp.py create mode 100644 examples/flask_alchemy/demoapp_factories.py create mode 100644 examples/flask_alchemy/requirements.txt create mode 100644 examples/flask_alchemy/test_demoapp.py create mode 100644 examples/requirements.txt diff --git a/Makefile b/Makefile index 88830150..79b5e826 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ PACKAGE=factory TESTS_DIR=tests DOC_DIR=docs +EXAMPLES_DIR=examples # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) @@ -43,9 +44,12 @@ clean: @rm -rf tmp_test/ -test: install-deps +test: install-deps example-test python -W default setup.py test +example-test: + $(MAKE) -C $(EXAMPLES_DIR) test + pylint: pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ diff --git a/dev_requirements.txt b/dev_requirements.txt index d55129a9..22261a17 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,4 +1,5 @@ -r requirements.txt +-r examples/requirements.txt coverage Django diff --git a/examples/Makefile b/examples/Makefile new file mode 100644 index 00000000..6064a9bc --- /dev/null +++ b/examples/Makefile @@ -0,0 +1,9 @@ +EXAMPLES = flask_alchemy + +TEST_TARGETS = $(addprefix runtest-,$(EXAMPLES)) + +test: $(TEST_TARGETS) + + +$(TEST_TARGETS): runtest-%: + cd $* && PYTHONPATH=../.. python -m unittest diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py new file mode 100644 index 00000000..4ab42b0c --- /dev/null +++ b/examples/flask_alchemy/demoapp.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +from flask import Flask +from flask.ext.sqlalchemy import SQLAlchemy + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' +db = SQLAlchemy(app) + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True) + email = db.Column(db.String(120), unique=True) + + def __init__(self, username, email): + self.username = username + self.email = email + + def __repr__(self): + return '' % self.username + + +class UserLog(db.Model): + id = db.Column(db.Integer, primary_key=True) + message = db.Column(db.String(1000)) + + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', backref=db.backref('logs', lazy='dynamic')) + + def __init__(self, message, user): + self.message = message + self.user = user + + def __repr__(self): + return '' % (self.user, self.message) diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py new file mode 100644 index 00000000..6b71d04f --- /dev/null +++ b/examples/flask_alchemy/demoapp_factories.py @@ -0,0 +1,27 @@ +import factory +import factory.alchemy +import factory.fuzzy + +import demoapp + + +class BaseFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + abstract = True + sqlalchemy_session = demoapp.db.session + + +class UserFactory(BaseFactory): + class Meta: + model = demoapp.User + + username = factory.fuzzy.FuzzyText() + email = factory.fuzzy.FuzzyText() + + +class UserLogFactory(BaseFactory): + class Meta: + model = demoapp.UserLog + + message = factory.fuzzy.FuzzyText() + user = factory.SubFactory(UserFactory) diff --git a/examples/flask_alchemy/requirements.txt b/examples/flask_alchemy/requirements.txt new file mode 100644 index 00000000..3ee3e5eb --- /dev/null +++ b/examples/flask_alchemy/requirements.txt @@ -0,0 +1,3 @@ +-r ../../requirements.txt +Flask +Flask-SQLAlchemy diff --git a/examples/flask_alchemy/test_demoapp.py b/examples/flask_alchemy/test_demoapp.py new file mode 100644 index 00000000..b485a924 --- /dev/null +++ b/examples/flask_alchemy/test_demoapp.py @@ -0,0 +1,35 @@ +import os +import unittest +import tempfile + +import demoapp +import demoapp_factories + +class DemoAppTestCase(unittest.TestCase): + + def setUp(self): + demoapp.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + demoapp.app.config['TESTING'] = True + self.app = demoapp.app.test_client() + self.db = demoapp.db + self.db.create_all() + + def tearDown(self): + self.db.drop_all() + + def test_user_factory(self): + user = demoapp_factories.UserFactory() + self.db.session.commit() + self.assertIsNotNone(user.id) + self.assertEqual(1, len(demoapp.User.query.all())) + + def test_userlog_factory(self): + userlog = demoapp_factories.UserLogFactory() + self.db.session.commit() + self.assertIsNotNone(userlog.id) + self.assertIsNotNone(userlog.user.id) + self.assertEqual(1, len(demoapp.User.query.all())) + self.assertEqual(1, len(demoapp.UserLog.query.all())) + +if __name__ == '__main__': + unittest.main() diff --git a/examples/requirements.txt b/examples/requirements.txt new file mode 100644 index 00000000..5e11ca53 --- /dev/null +++ b/examples/requirements.txt @@ -0,0 +1 @@ +-r flask_alchemy/requirements.txt From 197555d3d0de4759ca9ad45d5986fdcc4aa4c15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 25 Jul 2015 14:19:12 +0200 Subject: [PATCH 122/714] Docs: 'import factory.fuzzy' as required (See #138). --- docs/fuzzy.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index af5c490e..6b066080 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -8,6 +8,8 @@ Some tests may be interested in testing with fuzzy, random values. This is handled by the :mod:`factory.fuzzy` module, which provides a few random declarations. +.. note:: Use ``import factory.fuzzy`` to load this module. + FuzzyAttribute -------------- From b9347efae7e2f04687863f54e8db7d9e10f9dc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 22:51:35 +0200 Subject: [PATCH 123/714] examples: Fix make test (Closes #238) Properly install dependencies from examples folders. --- Makefile | 3 ++- examples/flask_alchemy/requirements.txt | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 79b5e826..35f635cb 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ MONGOENGINE ?= 0.9 NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt +EXAMPLES_REQ_FILES = $(shell find $(EXAMPLES_DIR) -name requirements.txt) all: default @@ -29,7 +30,7 @@ install-deps: $(REQ_FILE) pip install --upgrade -r $< pip freeze -$(REQ_FILE): dev_requirements.txt requirements.txt +$(REQ_FILE): dev_requirements.txt requirements.txt $(EXAMPLES_REQ_FILES) grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@ echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@ echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@ diff --git a/examples/flask_alchemy/requirements.txt b/examples/flask_alchemy/requirements.txt index 3ee3e5eb..fb675a95 100644 --- a/examples/flask_alchemy/requirements.txt +++ b/examples/flask_alchemy/requirements.txt @@ -1,3 +1,2 @@ --r ../../requirements.txt Flask Flask-SQLAlchemy From 57be4ac78b1213928a83079d298bafcc93e69483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionu=C8=9B=20Ar=C8=9B=C4=83ri=C8=99i?= Date: Sat, 18 Jul 2015 14:30:50 +0100 Subject: [PATCH 124/714] add a way to add custom providers to Faker factory_boy wraps faker and it stores Faker generators in a 'private' _FAKER_REGISTRY class attribute dict. There needs to be a way to extend the Faker generators with additional custom providers (without having to access _FAKER_REGISTRY directly). This commit adds a (factory_boy) Faker.add_provider class method which calls Faker's own `add_provider` method on internally stored (via _FAKER_REGISTRY) Faker generators. --- factory/faker.py | 5 +++++ tests/test_faker.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/factory/faker.py b/factory/faker.py index 10a0cba1..5411985d 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -94,3 +94,8 @@ def _get_faker(cls, locale=None): cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale) return cls._FAKER_REGISTRY[locale] + + @classmethod + def add_provider(cls, provider, locale=None): + """Add a new Faker provider for the specified locale""" + cls._get_faker(locale).add_provider(provider) diff --git a/tests/test_faker.py b/tests/test_faker.py index 41f8e191..99e54afd 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -21,9 +21,12 @@ # THE SOFTWARE. -import factory import unittest +import faker.providers + +import factory + class MockFaker(object): def __init__(self, expected): @@ -103,3 +106,30 @@ class Meta: self.assertEqual("John", profile.first_name) self.assertEqual("Valjean", profile.last_name) + def test_add_provider(self): + class Face(object): + def __init__(self, smiley, french_smiley): + self.smiley = smiley + self.french_smiley = french_smiley + + class FaceFactory(factory.Factory): + class Meta: + model = Face + + smiley = factory.Faker('smiley') + french_smiley = factory.Faker('smiley', locale='fr_FR') + + class SmileyProvider(faker.providers.BaseProvider): + def smiley(self): + return ':)' + + class FrenchSmileyProvider(faker.providers.BaseProvider): + def smiley(self): + return '(:' + + factory.Faker.add_provider(SmileyProvider) + factory.Faker.add_provider(FrenchSmileyProvider, 'fr_FR') + + face = FaceFactory() + self.assertEqual(":)", face.smiley) + self.assertEqual("(:", face.french_smiley) From 15f328350311ee46f84c628310e58e4ed8b49e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:08:29 +0200 Subject: [PATCH 125/714] Docs: Document Faker.add_provider (Closes #218) --- docs/reference.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 4c7f8f78..3a57c666 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -534,6 +534,23 @@ Faker ... UserFactory() + .. classmethod:: add_provider(cls, locale=None) + + Some projects may need to fake fields beyond those provided by ``fake-factory``; + in such cases, use :meth:`factory.Faker.add_provider` to declare additional providers + for those fields: + + .. code-block:: python + + factory.Faker.add_provider(SmileyProvider) + + class FaceFactory(factory.Factory): + class Meta: + model = Face + + smiley = factory.Faker('smiley') + + LazyAttribute """"""""""""" From f30c7b243a112eb07af0bcddbd9a211596ed80d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:18:22 +0200 Subject: [PATCH 126/714] Lazy load django's get_model (Closes #228). Loading this function will, on pre-1.8 versions, load Django settings. We'll lazy-load it to avoid crashes when Django hasn't been configured yet (e.g in auto-discovery test setups). --- docs/changelog.rst | 1 + factory/django.py | 34 ++++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01d5775f..24c01aa5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,7 @@ ChangeLog - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching + - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required .. _v2.5.2: diff --git a/factory/django.py b/factory/django.py index b2af12c0..b3c508c4 100644 --- a/factory/django.py +++ b/factory/django.py @@ -56,16 +56,34 @@ def require_django(): raise import_failure -if django is None: - def get_model(app, model): - raise import_failure +_LAZY_LOADS = {} + +def get_model(app, model): + """Wrapper around django's get_model.""" + if 'get_model' not in _LAZY_LOADS: + _lazy_load_get_model() + + _get_model = _LAZY_LOADS['get_model'] + return _get_model(app, model) + + +def _lazy_load_get_model(): + """Lazy loading of get_model. + + get_model loads django.conf.settings, which may fail if + the settings haven't been configured yet. + """ + if django is None: + def get_model(app, model): + raise import_failure -elif django.VERSION[:2] < (1, 7): - from django.db.models.loading import get_model + elif django.VERSION[:2] < (1, 7): + from django.db.models.loading import get_model -else: - from django import apps as django_apps - get_model = django_apps.apps.get_model + else: + from django import apps as django_apps + get_model = django_apps.apps.get_model + _LAZY_LOADS['get_model'] = get_model class DjangoOptions(base.FactoryOptions): From 1e1adebe92397b405563dc141c853f62feca6c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:21:33 +0200 Subject: [PATCH 127/714] Docs: Fix typo in M2M recipes (Closes #226) As spotted by @stephane, thanks! --- docs/recipes.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 3cbe6d2a..df86bac4 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -148,8 +148,8 @@ factory_boy related factories. method. -Simple ManyToMany ------------------ +Simple Many-to-many relationship +-------------------------------- Building the adequate link between two models depends heavily on the use case; factory_boy doesn't provide a "all in one tools" as for :class:`~factory.SubFactory` @@ -167,7 +167,7 @@ hook: class User(models.Model): name = models.CharField() - groups = models.ManyToMany(Group) + groups = models.ManyToManyField(Group) # factories.py @@ -204,8 +204,8 @@ the ``groups`` declaration will add passed in groups to the set of groups for th user. -ManyToMany with a 'through' ---------------------------- +Many-to-many relation with a 'through' +-------------------------------------- If only one link is required, this can be simply performed with a :class:`RelatedFactory`. @@ -219,7 +219,7 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: class Group(models.Model): name = models.CharField() - members = models.ManyToMany(User, through='GroupLevel') + members = models.ManyToManyField(User, through='GroupLevel') class GroupLevel(models.Model): user = models.ForeignKey(User) From dc7d02095fff8124aaeccf8f08958fa6797b6ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:26:52 +0200 Subject: [PATCH 128/714] mogo: Stop using deprecated .new (Closes #219) This method has been deprecated in `mogo.model.Model` since 2012. Thanks to @federicobond for spotting this! --- docs/changelog.rst | 1 + factory/mogo.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 24c01aa5..d38c06ac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ ChangeLog - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required + - :issue:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago. .. _v2.5.2: diff --git a/factory/mogo.py b/factory/mogo.py index c6c3c197..b5841b1f 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -37,7 +37,7 @@ class Meta: @classmethod def _build(cls, model_class, *args, **kwargs): - return model_class.new(*args, **kwargs) + return model_class(*args, **kwargs) @classmethod def _create(cls, model_class, *args, **kwargs): From 41bbff4701ac857bf6c468a4dc53836ee85baa11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:30:18 +0200 Subject: [PATCH 129/714] Update note on django's unsaved instance checks This note was added to document a regression in Django 1.8.0; the regression has been fixed in 1.8.4. Closes #232 --- docs/orms.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 26390b5c..9b209bc0 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -40,12 +40,12 @@ All factories for a Django :class:`~django.db.models.Model` should use the once all post-generation hooks have run. -.. note:: Starting with Django 1.8, it is no longer possible to call ``.build()`` - on a factory if this factory uses a :class:`~factory.SubFactory` pointing - to another model: Django refuses to set a :class:`~djang.db.models.ForeignKey` +.. note:: With Django versions 1.8.0 to 1.8.3, it was no longer possible to call ``.build()`` + on a factory if this factory used a :class:`~factory.SubFactory` pointing + to another model: Django refused to set a :class:`~djang.db.models.ForeignKey` to an unsaved :class:`~django.db.models.Model` instance. - See https://code.djangoproject.com/ticket/10811 for details. + See https://code.djangoproject.com/ticket/10811 and https://code.djangoproject.com/ticket/25160 for details. .. class:: DjangoOptions(factory.base.FactoryOptions) From b827b1a06d5d06e97120f4fa582ebbe79cb59d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:34:42 +0200 Subject: [PATCH 130/714] Tox isn't used, remove its config file. --- tox.ini | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 2200f56a..00000000 --- a/tox.ini +++ /dev/null @@ -1,17 +0,0 @@ -[tox] -envlist = py26,py27,pypy - -[testenv] -commands= - python -W default setup.py test - -[testenv:py26] - -deps= - mock - unittest2 - -[textenv:py27] - -deps= - mock From 72751aef7b4ba519575bbd8bd4b40864fdf5158e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:41:02 +0200 Subject: [PATCH 131/714] Ideas: I want to be able to nest declarations Closes #140, as this won't be implemented in the next few weeks. --- docs/ideas.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ideas.rst b/docs/ideas.rst index f3c9e623..6e3962d4 100644 --- a/docs/ideas.rst +++ b/docs/ideas.rst @@ -6,4 +6,4 @@ This is a list of future features that may be incorporated into factory_boy: * When a :class:`Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere * Define a proper set of rules for the support of third-party ORMs - +* Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) From 3b3fed10ba95ef55cf057994922af55defd007ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 20 Oct 2015 23:58:48 +0200 Subject: [PATCH 132/714] Release v2.6.0 --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/__init__.py b/factory/__init__.py index 843cf99f..4a4a09f0 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.5.2' +__version__ = '2.6.0' __author__ = 'Raphaël Barrois ' From 5ae2055fe474fbd2a5c5b5a92515bc0affcf9e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 21 Oct 2015 00:12:26 +0200 Subject: [PATCH 133/714] docs: Note 2.6.0 release date. --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d38c06ac..32d8da61 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. _v2.6.0: -2.6.0 (XXXX-XX-XX) +2.6.0 (2015-10-20) ------------------ *New:* From e1bf839f80398d5bd5465cf5fa9463915f887c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 21 Oct 2015 00:16:21 +0200 Subject: [PATCH 134/714] mogo: Stop using .new, continued. From dc7d02095fff, spotted by @federicobond too. See #219. --- factory/mogo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/mogo.py b/factory/mogo.py index b5841b1f..aa9f28b0 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -41,6 +41,6 @@ def _build(cls, model_class, *args, **kwargs): @classmethod def _create(cls, model_class, *args, **kwargs): - instance = model_class.new(*args, **kwargs) + instance = model_class(*args, **kwargs) instance.save() return instance From be85908f5205810083c524a25c7da565788f2c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 7 Nov 2015 10:08:47 +0100 Subject: [PATCH 135/714] Fix obsolete text in docs (Closes #245, #248, #249). Thanks a lot to Jeff Widman for spotting them! --- docs/changelog.rst | 2 -- docs/reference.rst | 2 +- examples/flask_alchemy/demoapp_factories.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 32d8da61..fa542f4c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -131,8 +131,6 @@ This takes care of all ``FACTORY_FOR`` occurences; the files containing other at For :class:`factory.Factory`: - * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` - * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` * Rename :attr:`~factory.Factory.ABSTRACT_FACTORY` to :attr:`~factory.FactoryOptions.abstract` * Rename :attr:`~factory.Factory.FACTORY_STRATEGY` to :attr:`~factory.FactoryOptions.strategy` diff --git a/docs/reference.rst b/docs/reference.rst index 3a57c666..6398d9a2 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -634,7 +634,7 @@ This declaration takes a single argument, a function accepting a single paramete .. note:: An extra kwarg argument, ``type``, may be provided. - This feature is deprecated in 1.3.0 and will be removed in 2.0.0. + This feature was deprecated in 1.3.0 and will be removed in 2.0.0. .. code-block:: python diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py index 6b71d04f..f32f8c39 100644 --- a/examples/flask_alchemy/demoapp_factories.py +++ b/examples/flask_alchemy/demoapp_factories.py @@ -1,5 +1,4 @@ import factory -import factory.alchemy import factory.fuzzy import demoapp From a98ad593280fb7ddef0b8e019c6106da09b931dc Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Mon, 23 Nov 2015 23:53:04 -0500 Subject: [PATCH 136/714] Clarify sequence behavior on inheritance Sequences are only shared via inheritance if the model of the subclass is the same as or a subclass of the model of the parent class. Clarify the docs on this point. --- docs/reference.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 6398d9a2..44300a59 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -711,8 +711,9 @@ The sequence counter is shared across all :class:`Sequence` attributes of the Inheritance ~~~~~~~~~~~ -When a :class:`Factory` inherits from another :class:`Factory`, their -sequence counter is shared: +When a :class:`Factory` inherits from another :class:`Factory` and the `model` +of the subclass inherits from the `model` of the parent, the sequence counter +is shared across the :class:`Factory` classes: .. code-block:: python From 819ffaf9efe0d5a3eee85afc847ceb6969242833 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 8 Dec 2015 12:56:00 +1100 Subject: [PATCH 137/714] Test LazyValues handling CyclicDefinitionError --- tests/test_containers.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/test_containers.py b/tests/test_containers.py index 083b306f..9107b0d8 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -78,18 +78,40 @@ def test_access_parent(self): 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): - self.attrname = attrname + class LazyAttr(containers.LazyValue): + def __init__(self, attrname): + self.attrname = attrname - def evaluate(self, obj, container=None): - return 1 + getattr(obj, self.attrname) + def evaluate(self, obj, container=None): + return 1 + getattr(obj, self.attrname) - stub = containers.LazyStub({'one': LazyAttr('two'), 'two': LazyAttr('one')}) + def test_cyclic_definition(self): + stub = containers.LazyStub({ + 'one': self.LazyAttr('two'), + 'two': self.LazyAttr('one'), + }) self.assertRaises(containers.CyclicDefinitionError, getattr, stub, 'one') + def test_cyclic_definition_rescue(self): + class LazyAttrDefault(self.LazyAttr): + def __init__(self, attname, defvalue): + super(LazyAttrDefault, self).__init__(attname) + self.defvalue = defvalue + def evaluate(self, obj, container=None): + try: + return super(LazyAttrDefault, self).evaluate(obj, container) + except containers.CyclicDefinitionError: + return self.defvalue + + stub = containers.LazyStub({ + 'one': LazyAttrDefault('two', 10), + 'two': self.LazyAttr('one'), + }) + + self.assertEqual(10, stub.one) + self.assertEqual(11, stub.two) + def test_representation(self): class RandomObj(object): pass From f023e5a477668b8374a75c78e87d946b21a27f15 Mon Sep 17 00:00:00 2001 From: Alexey Kotlyarov Date: Tue, 8 Dec 2015 12:57:24 +1100 Subject: [PATCH 138/714] Don't leave AttributeBuilder in an inconsistent state on exceptions When one of the LazyValues raises an exception, don't leave its name in __pending stack of the AttributeBuilder, preventing evaluation of any other LazyValues. --- factory/containers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/factory/containers.py b/factory/containers.py index 0ae354b8..ec33ca1b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -102,8 +102,10 @@ def __getattr__(self, name): val = self.__attrs[name] if isinstance(val, LazyValue): self.__pending.append(name) - val = val.evaluate(self, self.__containers) - last = self.__pending.pop() + try: + val = val.evaluate(self, self.__containers) + finally: + last = self.__pending.pop() assert name == last self.__values[name] = val return val From 05082c661655df319ce641dd7976c02d1799ab14 Mon Sep 17 00:00:00 2001 From: mluszczyk Date: Mon, 28 Dec 2015 13:06:13 +0100 Subject: [PATCH 139/714] Fixed spelling. --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index 9b209bc0..0afda696 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -15,7 +15,7 @@ Django The first versions of factory_boy were designed specifically for Django, -but the library has now evolved to be framework-independant. +but the library has now evolved to be framework-independent. Most features should thus feel quite familiar to Django users. From 5000ddaaef582e7504babf4f8163de13b93e7459 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Wed, 6 Jan 2016 19:36:10 -0300 Subject: [PATCH 140/714] optional forced flush on SQLAlchemyModelFactory fixes rbarrois/factory_boy#81 --- docs/orms.rst | 4 ++++ factory/alchemy.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index 9b209bc0..bd481bd7 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -333,6 +333,10 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. + .. attribute:: force_flush + + Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + A (very) simple example: .. code-block:: python diff --git a/factory/alchemy.py b/factory/alchemy.py index 20da6cf5..a9aab231 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -27,6 +27,7 @@ class SQLAlchemyOptions(base.FactoryOptions): def _build_default_options(self): return super(SQLAlchemyOptions, self)._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), + base.OptionDefault('force_flush', False, inherit=True), ] @@ -43,4 +44,6 @@ def _create(cls, model_class, *args, **kwargs): session = cls._meta.sqlalchemy_session obj = model_class(*args, **kwargs) session.add(obj) + if cls._meta.force_flush: + session.flush() return obj From 28ce31db61a46fbd73126630c758d32a7245da42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 6 Jan 2016 23:10:26 +0100 Subject: [PATCH 141/714] Clarify the (dis)advantages of randomized tests. As noted in #259, fully random tests have some issues, notably possibly flaky builds: it is quite helpful to be able to choose the random seeds used by factory_boy and friends. --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9b824069..8914c62f 100644 --- a/README.rst +++ b/README.rst @@ -181,7 +181,7 @@ It is also possible to create a bunch of objects in a single call: Realistic, random values """""""""""""""""""""""" -Tests look better with random yet realistic values. +Demos look better with random yet realistic values; and those realistic values can also help discover bugs. For this, factory_boy relies on the excellent `fake-factory `_ library: .. code-block:: python @@ -199,6 +199,10 @@ For this, factory_boy relies on the excellent `fake-factory +.. note:: Use of fully randomized data in tests is quickly a problem for reproducing broken builds. + To that purpose, factory_boy provides helpers to handle the random seeds it uses. + + Lazy Attributes """"""""""""""" From 4172dd686ce483191b33e3189d716f11b3da921e Mon Sep 17 00:00:00 2001 From: Alejandro Date: Wed, 6 Jan 2016 19:36:10 -0300 Subject: [PATCH 142/714] optional forced flush on SQLAlchemyModelFactory fixes rbarrois/factory_boy#81 --- docs/orms.rst | 4 ++++ factory/alchemy.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index 9b209bc0..bd481bd7 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -333,6 +333,10 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. + .. attribute:: force_flush + + Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + A (very) simple example: .. code-block:: python diff --git a/factory/alchemy.py b/factory/alchemy.py index 20da6cf5..a9aab231 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -27,6 +27,7 @@ class SQLAlchemyOptions(base.FactoryOptions): def _build_default_options(self): return super(SQLAlchemyOptions, self)._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), + base.OptionDefault('force_flush', False, inherit=True), ] @@ -43,4 +44,6 @@ def _create(cls, model_class, *args, **kwargs): session = cls._meta.sqlalchemy_session obj = model_class(*args, **kwargs) session.add(obj) + if cls._meta.force_flush: + session.flush() return obj From b8050b1d61cd3171c2640eeaa6b3f71a6cbef5f5 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Thu, 7 Jan 2016 12:52:57 -0300 Subject: [PATCH 143/714] added unittests for rbarrois/factory_boy#81 --- tests/test_alchemy.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 9d7288a2..5d8f275a 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -23,6 +23,7 @@ import factory from .compat import unittest +import mock try: @@ -55,6 +56,16 @@ class Meta: foo = factory.Sequence(lambda n: 'foo%d' % n) +class ForceFlushingStandardFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_session = mock.MagicMock() + force_flush = True + + id = factory.Sequence(lambda n: n) + foo = factory.Sequence(lambda n: 'foo%d' % n) + + class NonIntegerPkFactory(SQLAlchemyModelFactory): class Meta: model = models.NonIntegerPk @@ -102,6 +113,27 @@ def test_pk_force_value(self): self.assertEqual(0, std2.id) +@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") +class SQLAlchemyForceFlushTestCase(unittest.TestCase): + def setUp(self): + super(SQLAlchemyForceFlushTestCase, self).setUp() + ForceFlushingStandardFactory.reset_sequence(1) + ForceFlushingStandardFactory._meta.sqlalchemy_session.rollback() + ForceFlushingStandardFactory._meta.sqlalchemy_session.reset_mock() + + def test_force_flush_called(self): + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory.create() + self.assertTrue(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + + def test_force_flush_not_called(self): + ForceFlushingStandardFactory._meta.force_flush = False + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory.create() + self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) + ForceFlushingStandardFactory._meta.force_flush = True + + @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): From 229d43874723f36b380eb49e53538bf21511fa5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 9 Feb 2016 23:57:31 +0100 Subject: [PATCH 144/714] Clarify the use of SelfAttribute in RelatedFactory (Closes #264) --- docs/reference.rst | 17 +++++++++++++++++ tests/test_using.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 6398d9a2..b5ccd16f 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1412,6 +1412,23 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables 1 +.. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated. + This means that calls to :class:`factory.SelfAttribute` cannot go higher than this :class:`RelatedFactory`: + + .. code-block:: python + + class CountryFactory(factory.Factory): + class Meta: + model = Country + + lang = 'fr' + capital_city = factory.RelatedFactory(CityFactory, 'capital_of', + # factory.SelfAttribute('..lang') will crash, since the context of + # ``CountryFactory`` has already been evaluated. + main_lang=factory.SelfAttribute('capital_of.lang'), + ) + + PostGeneration """""""""""""" diff --git a/tests/test_using.py b/tests/test_using.py index c7d2b857..0a893c14 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1924,6 +1924,36 @@ class Meta: self.assertEqual(3, related.one) self.assertEqual(4, related.two) + def test_related_factory_selfattribute(self): + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + obj.related = self + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactory(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj', + two=factory.SelfAttribute('obj.two'), + ) + + obj = TestObjectFactory.build(two=4) + self.assertEqual(3, obj.one) + self.assertEqual(4, obj.two) + self.assertEqual(1, obj.related.one) + self.assertEqual(4, obj.related.two) + + class RelatedFactoryExtractionTestCase(unittest.TestCase): def setUp(self): From 2eb8242a31f303d36c15b4644c54afb2cef8257e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 9 Feb 2016 23:58:01 +0100 Subject: [PATCH 145/714] doc: Use ReadTheDocs theme for local doc builds. --- dev_requirements.txt | 3 +++ docs/conf.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dev_requirements.txt b/dev_requirements.txt index 22261a17..c78aa9d6 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -8,3 +8,6 @@ SQLAlchemy mongoengine mock wheel + +Sphinx +sphinx_rtd_theme diff --git a/docs/conf.py b/docs/conf.py index c3512e0b..d5b86f44 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -114,7 +114,7 @@ def get_version(*module_dir_components): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From 8269885f9a71850838ee003627bcfd6d6d53e2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:07:32 +0100 Subject: [PATCH 146/714] fuzzy: Fix decimal.FloatOperation warning (Closes #261) Under Python 2.7+, the previous versions was directly casting fuzzy Decimal values into a float, which led to warnings in code trying to avoid such conversions in its tested code. Since we're just building random values, that behavior led to false positives or required jumping through weird hoops whenever a FuzzyDecimal was used. We now go trough a ``str()`` call to avoid such warnings. --- factory/compat.py | 8 -------- factory/fuzzy.py | 2 +- tests/test_fuzzy.py | 12 ++++++++++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/factory/compat.py b/factory/compat.py index 785d174e..737d91a8 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -42,14 +42,6 @@ def is_string(obj): from io import BytesIO -if sys.version_info[:2] == (2, 6): # pragma: no cover - def float_to_decimal(fl): - return decimal.Decimal(str(fl)) -else: # pragma: no cover - def float_to_decimal(fl): - return decimal.Decimal(fl) - - try: # pragma: no cover # Python >= 3.2 UTC = datetime.timezone.utc diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 923d8b74..a7e834cc 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -164,7 +164,7 @@ def __init__(self, low, high=None, precision=2, **kwargs): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - base = compat.float_to_decimal(_random.uniform(self.low, self.high)) + base = decimal.Decimal(str(_random.uniform(self.low, self.high))) return base.quantize(decimal.Decimal(10) ** -self.precision) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 3f9c434d..d83f3dde 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -189,6 +189,18 @@ def test_precision(self): self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) + def test_no_approximation(self): + """We should not go through floats in our fuzzy calls unless actually needed.""" + fuzz = fuzzy.FuzzyDecimal(0, 10) + + decimal_context = decimal.getcontext() + old_traps = decimal_context.traps[decimal.FloatOperation] + try: + decimal_context.traps[decimal.FloatOperation] = True + fuzz.evaluate(2, None, None) + finally: + decimal_context.traps[decimal.FloatOperation] = old_traps + class FuzzyDateTestCase(unittest.TestCase): @classmethod From efd5c65b99a31992001a9581a41ec4627c4d94fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:15:52 +0100 Subject: [PATCH 147/714] Clarify precedence on factory.django.FileField (Closes #257). When both ``from_file`` and ``filename`` are provided, ``filename`` takes precedence. Thanks to @darkowic for spotting this :) --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index bd481bd7..d1b30fc8 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -126,7 +126,7 @@ Extra fields :param str from_path: Use data from the file located at ``from_path``, and keep its filename :param file from_file: Use the contents of the provided file object; use its filename - if available + if available, unless ``filename`` is also provided. :param bytes data: Use the provided bytes as file contents :param str filename: The filename for the FileField From fb613d3cd9513f283072e2792317a5874e148815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:32:36 +0100 Subject: [PATCH 148/714] Fix "no FloatOperation test", invalid until PY3 --- tests/test_fuzzy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index d83f3dde..4c3873a9 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -189,6 +189,7 @@ def test_precision(self): self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) + @unittest.skipIf(compat.PY2, "decimal.FloatOperation was added in Py3") def test_no_approximation(self): """We should not go through floats in our fuzzy calls unless actually needed.""" fuzz = fuzzy.FuzzyDecimal(0, 10) From 97804f061ca8b0e090136c0d02e7549000c201ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:34:17 +0100 Subject: [PATCH 149/714] Update testing targets (Closes #265) Thanks to @jeffwidman for suggesting this! --- .travis.yml | 1 + Makefile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1600bdf..ff805b01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python python: - "2.7" - "3.4" + - "3.5" - "pypy" script: diff --git a/Makefile b/Makefile index 35f635cb..da8ac88b 100644 --- a/Makefile +++ b/Makefile @@ -7,13 +7,13 @@ EXAMPLES_DIR=examples COVERAGE = python $(shell which coverage) # Dependencies -DJANGO ?= 1.8 +DJANGO ?= 1.9 NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") ALCHEMY ?= 1.0 NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") -MONGOENGINE ?= 0.9 +MONGOENGINE ?= 0.10 NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt From 8050e9941408d29e339e47066c09f3d9ed19ffe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:51:39 +0100 Subject: [PATCH 150/714] Switch badges to shields.io --- README.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 8914c62f..576ba39e 100644 --- a/README.rst +++ b/README.rst @@ -1,20 +1,24 @@ factory_boy =========== -.. image:: https://pypip.in/version/factory_boy/badge.svg +.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master + :target: http://travis-ci.org/rbarrois/factory_boy/ + +.. image:: https://img.shields.io/pypi/v/factory_boy.svg :target: http://factoryboy.readthedocs.org/en/latest/changelog.html :alt: Latest Version -.. image:: https://pypip.in/py_versions/factory_boy/badge.svg +.. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg :target: https://pypi.python.org/pypi/factory_boy/ :alt: Supported Python versions -.. image:: https://pypip.in/wheel/factory_boy/badge.svg +.. image:: https://img.shields.io/pypi/wheel/factory_boy.svg :target: https://pypi.python.org/pypi/factory_boy/ :alt: Wheel status -.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master - :target: http://travis-ci.org/rbarrois/factory_boy/ +.. image:: https://img.shields.io/pypi/l/factory_boy.svg + :target: https://pypi.python.org/pypi/factory_boy/ + :alt: License factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. From 023070446c6251e563f8eb087e0a1fe7fbeb248b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:52:36 +0100 Subject: [PATCH 151/714] Announce support for Python3.5 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 8ca7e4b2..003dd088 100755 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def get_version(package_name): "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", From 99337aaa01860c771704e1c558c225f8fead5b3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:53:47 +0100 Subject: [PATCH 152/714] Update README: support 2.6-3.5 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 576ba39e..ee737c95 100644 --- a/README.rst +++ b/README.rst @@ -79,7 +79,7 @@ Links * Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ -factory_boy supports Python 2.6, 2.7, 3.2 and 3.3, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.6, 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. Download From 5ec286d3b666a8f570e90ea18ec492b6db996fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 00:54:00 +0100 Subject: [PATCH 153/714] Document mailing-list --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index ee737c95..4d114e51 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,7 @@ Links * Documentation: http://factoryboy.readthedocs.org/ * Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ +* Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy factory_boy supports Python 2.6, 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. @@ -336,6 +337,7 @@ Contributing factory_boy is distributed under the MIT License. Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. +Questions and suggestions are welcome on the `mailing-list `_. All pull request should pass the test suite, which can be launched simply with: @@ -356,7 +358,7 @@ To test with a specific framework version, you may use: .. code-block:: sh - $ make DJANGO=1.7 test + $ make DJANGO=1.9 test Valid options are: @@ -365,6 +367,13 @@ Valid options are: * ``ALCHEMY`` for ``SQLAlchemy`` +To avoid running ``mongoengine`` tests (e.g no mongo server installed), run: + +.. code-block:: sh + + $ make SKIP_MONGOENGINE=1 test + + Contents, indices and tables ---------------------------- From 41560aa54e83fe539c0a5a1935bcaaf6363a522c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 01:07:54 +0100 Subject: [PATCH 154/714] Release v2.6.1 --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/__init__.py b/factory/__init__.py index 4a4a09f0..c8bc3961 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.6.0' +__version__ = '2.6.1' __author__ = 'Raphaël Barrois ' From 38f4a69db8f71cb52b9e7fd8d6e20e7d052a5b8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 10 Feb 2016 01:13:17 +0100 Subject: [PATCH 155/714] Add ChangeLog for 2.6.1 --- docs/changelog.rst | 9 +++++++++ docs/recipes.rst | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fa542f4c..dc3f967e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,15 @@ ChangeLog ========= +.. _v2.6.1: + +2.6.1 (2016-02-10) +------------------ + +*New:* + + - :issue:`262`: Allow optional forced flush on SQLAlchemy, courtesy of `Minjung `_. + .. _v2.6.0: 2.6.0 (2015-10-20) diff --git a/docs/recipes.rst b/docs/recipes.rst index df86bac4..a627e8b4 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -88,7 +88,7 @@ When a :class:`UserFactory` is instantiated, factory_boy will call Example: Django's Profile -""""""""""""""""""""""""" +~~~~~~~~~~~~~~~~~~~~~~~~~ Django (<1.5) provided a mechanism to attach a ``Profile`` to a ``User`` instance, using a :class:`~django.db.models.OneToOneField` from the ``Profile`` to the ``User``. From f2c075c40fd331b7d26a9db72aad249b2165eac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Fri, 12 Feb 2016 17:31:04 +0100 Subject: [PATCH 156/714] factory: LazyFunction to just call a function in the simplest case No need to wrap it in a lambda to strip the object argument from LazyAttribute or the sequence argument from Sequence. --- README.rst | 5 +++++ docs/examples.rst | 2 ++ docs/introduction.rst | 29 +++++++++++++++++++++++++++++ docs/reference.rst | 40 ++++++++++++++++++++++++++++++++++++++-- factory/__init__.py | 1 + factory/declarations.py | 17 +++++++++++++++++ tests/test_base.py | 11 +++++++---- tests/test_containers.py | 23 +++++++++++++++++++++++ 8 files changed, 122 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 4d114e51..762fedb9 100644 --- a/README.rst +++ b/README.rst @@ -226,6 +226,7 @@ These "lazy" attributes can be added as follows: first_name = 'Joe' last_name = 'Blow' email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower()) + date_joined = factory.LazyFunction(datetime.now) .. code-block:: pycon @@ -233,6 +234,10 @@ These "lazy" attributes can be added as follows: "joe.blow@example.com" +.. note:: ``LazyAttribute`` calls the function with the object being constructed as an argument, when + ``LazyFunction`` does not send any argument. + + Sequences """"""""" diff --git a/docs/examples.rst b/docs/examples.rst index e7f60577..6f26b7ed 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -49,6 +49,7 @@ And now, we'll define the related factories: .. code-block:: python + import datetime import factory import random @@ -61,6 +62,7 @@ And now, we'll define the related factories: username = factory.Sequence(lambda n: 'john%s' % n) email = factory.LazyAttribute(lambda o: '%s@example.org' % o.username) + date_joined = factory.LazyFunction(datetime.datetime.now) class ProfileFactory(factory.Factory): diff --git a/docs/introduction.rst b/docs/introduction.rst index d00154dc..9a16c398 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -117,6 +117,35 @@ This is achieved with the :class:`~factory.Sequence` declaration: return 'user%d' % n +LazyFunction +------------ + +In simple cases, calling a function is enough to compute the value. If that function doesn't depend on the object +being built, use :class:`~factory.LazyFunction` to call that function; it should receive a function taking no +argument and returning the value for the field: + +.. code-block:: python + + class LogFactory(factory.Factory): + class Meta: + model = models.Log + + timestamp = factory.LazyFunction(datetime.now) + +.. code-block:: pycon + + >>> LogFactory() + + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + + + +.. note:: For complex cases when you happen to write a specific function, + the :meth:`~factory.@lazy_attribute` decorator should be more appropriate. + + LazyAttribute ------------- diff --git a/docs/reference.rst b/docs/reference.rst index b5ccd16f..9e012138 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -90,7 +90,7 @@ The :class:`Factory` class model = Order exclude = ('now',) - now = factory.LazyAttribute(lambda o: datetime.datetime.utcnow()) + now = factory.LazyFunction(datetime.datetime.utcnow) started_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(hours=1)) paid_at = factory.LazyAttribute(lambda o: o.now - datetime.timedelta(minutes=50)) @@ -551,6 +551,42 @@ Faker smiley = factory.Faker('smiley') +LazyFunction +"""""""""""" + +.. class:: LazyFunction(method_to_call) + +The :class:`LazyFunction` is the simplest case where the value of an attribute +does not depend on the object being built. + +It takes as argument a method to call (function, lambda...); that method should +not take any argument, though keyword arguments are safe but unused, +and return a value. + +.. code-block:: python + + class LogFactory(factory.Factory): + class Meta: + model = models.Log + + timestamp = factory.LazyFunction(datetime.now) + +.. code-block:: pycon + + >>> LogFactory() + + + >>> # The LazyFunction can be overriden + >>> LogFactory(timestamp=now - timedelta(days=1)) + + +Decorator +~~~~~~~~~ + +The class :class:`LazyFunction` does not provide a decorator. + +For complex cases, use :meth:`LazyAttribute.lazy_attribute` directly. + LazyAttribute """"""""""""" @@ -1041,7 +1077,7 @@ gains an "upward" semantic through the double-dot notation, as used in Python im >>> company.owner.language 'fr' -Obviously, this "follow parents" hability also handles overriding some attributes on call: +Obviously, this "follow parents" ability also handles overriding some attributes on call: .. code-block:: pycon diff --git a/factory/__init__.py b/factory/__init__.py index c8bc3961..1fa581b3 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -43,6 +43,7 @@ from .faker import Faker from .declarations import ( + LazyFunction, LazyAttribute, Iterator, Sequence, diff --git a/factory/declarations.py b/factory/declarations.py index f0dbfe54..9ab74628 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -57,6 +57,23 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): raise NotImplementedError('This is an abstract method') +class LazyFunction(OrderedDeclaration): + """Simplest OrderedDeclaration computed by calling the given function. + + Attributes: + function (function): a function without arguments and + returning the computed value. + """ + + def __init__(self, function, *args, **kwargs): + super(LazyFunction, self).__init__(*args, **kwargs) + self.function = function + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + logger.debug("LazyFunction: Evaluating %r on %r", self.function, obj) + return self.function() + + class LazyAttribute(OrderedDeclaration): """Specific OrderedDeclaration computed using a lambda. diff --git a/tests/test_base.py b/tests/test_base.py index 24f64e58..dd74e350 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -134,25 +134,28 @@ class AbstractFactory(base.Factory): self.assertEqual(AbstractFactory, AbstractFactory._meta.counter_reference) def test_declaration_collecting(self): - lazy = declarations.LazyAttribute(lambda _o: 1) + lazy = declarations.LazyFunction(int) + lazy2 = declarations.LazyAttribute(lambda _o: 1) postgen = declarations.PostGenerationDeclaration() class AbstractFactory(base.Factory): x = 1 y = lazy + y2 = lazy2 z = postgen # Declarations aren't removed self.assertEqual(1, AbstractFactory.x) self.assertEqual(lazy, AbstractFactory.y) + self.assertEqual(lazy2, AbstractFactory.y2) self.assertEqual(postgen, AbstractFactory.z) # And are available in class Meta - self.assertEqual({'x': 1, 'y': lazy}, AbstractFactory._meta.declarations) + self.assertEqual({'x': 1, 'y': lazy, 'y2': lazy2}, AbstractFactory._meta.declarations) self.assertEqual({'z': postgen}, AbstractFactory._meta.postgen_declarations) def test_inherited_declaration_collecting(self): - lazy = declarations.LazyAttribute(lambda _o: 1) + lazy = declarations.LazyFunction(int) lazy2 = declarations.LazyAttribute(lambda _o: 2) postgen = declarations.PostGenerationDeclaration() postgen2 = declarations.PostGenerationDeclaration() @@ -178,7 +181,7 @@ class OtherFactory(AbstractFactory): self.assertEqual({'z': postgen, 'b': postgen2}, OtherFactory._meta.postgen_declarations) def test_inherited_declaration_shadowing(self): - lazy = declarations.LazyAttribute(lambda _o: 1) + lazy = declarations.LazyFunction(int) lazy2 = declarations.LazyAttribute(lambda _o: 2) postgen = declarations.PostGenerationDeclaration() postgen2 = declarations.PostGenerationDeclaration() diff --git a/tests/test_containers.py b/tests/test_containers.py index 083b306f..825e8972 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -215,6 +215,29 @@ def _generate_next_sequence(cls): ab = containers.AttributeBuilder(FakeFactory, extra={'one': seq2}) self.assertEqual({'one': 'yy1'}, ab.build(create=False)) + def test_lazy_function(self): + lf = declarations.LazyFunction(int) + + class FakeFactory(object): + @classmethod + def declarations(cls, extra): + d = {'one': 1, 'two': lf} + d.update(extra) + return d + + @classmethod + def _generate_next_sequence(cls): + return 1 + + ab = containers.AttributeBuilder(FakeFactory) + self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False)) + + ab = containers.AttributeBuilder(FakeFactory, {'one': 4}) + self.assertEqual({'one': 4, 'two': 0}, ab.build(create=False)) + + ab = containers.AttributeBuilder(FakeFactory, {'one': 4, 'three': lf}) + self.assertEqual({'one': 4, 'two': 0, 'three': 0}, ab.build(create=False)) + def test_lazy_attribute(self): la = declarations.LazyAttribute(lambda a: a.one * 2) From 4b8008386804045c10d7bca10e2de0464cd5a85b Mon Sep 17 00:00:00 2001 From: yamaneko Date: Sat, 13 Feb 2016 18:18:39 +0900 Subject: [PATCH 157/714] Remove a nonexisting argumen "tz" --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 6b066080..fde1af16 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -199,7 +199,7 @@ FuzzyDate 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) +.. class:: FuzzyDateTime(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:`FuzzyDateTime` fuzzer generates random timezone-aware datetime within a given inclusive range. From efc8a7e873aaab5170e1dc2477aaf92c6fb59fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Feb 2016 00:00:03 +0100 Subject: [PATCH 158/714] fuzzy: Minor cleanup in BaseFuzzyDateTime The ``_now()`` method wasn't declared on the base class, only in its subclasses. --- factory/fuzzy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index a7e834cc..71d18847 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -217,6 +217,9 @@ def _check_bounds(self, start_dt, end_dt): """%s boundaries should have start <= end, got %r > %r""" % ( self.__class__.__name__, start_dt, end_dt)) + def _now(self): + raise NotImplementedError() + 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, From 1e98b18af457b8dc5e795be8d00c226c78682942 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Fri, 19 Feb 2016 00:14:33 -0800 Subject: [PATCH 159/714] Switch Travis builds to the container-based infrastructure https://docs.travis-ci.com/user/migrating-from-legacy/ --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ff805b01..938787b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: From 639b3a3763194374a71d2049a2da7d2e12cb1b1f Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Sun, 21 Feb 2016 15:14:58 -0800 Subject: [PATCH 160/714] Add note about deprecate/remove Fuzzy attributes Full discussion in https://github.com/rbarrois/factory_boy/issues/271 Wanted to get something mentioned in the docs immediately. --- docs/fuzzy.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index fde1af16..5b03ec64 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -3,6 +3,12 @@ Fuzzy attributes .. module:: factory.fuzzy +.. note:: Now that FactoryBoy includes the :class:`factory.Faker` class, most of + these built-in fuzzers are deprecated in favor of their + `Faker `_ equivalents. Further + discussion here: + ``_ + Some tests may be interested in testing with fuzzy, random values. This is handled by the :mod:`factory.fuzzy` module, which provides a few From fb608987a4fb61ff6198a6497359ec00058b9253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 23 Feb 2016 00:53:44 +0100 Subject: [PATCH 161/714] Add test for "build as dict" trick (See #68). --- tests/test_using.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 0a893c14..3ef54035 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -292,6 +292,19 @@ def test_make_factory(self): self.assertEqual(obj.three, 5) self.assertEqual(obj.four, None) + def test_build_to_dict(self): + # We have a generic factory + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = 'one' + two = factory.LazyAttribute(lambda o: o.one * 2) + + # Now, get a dict out of it + obj = factory.build(dict, FACTORY_CLASS=TestObjectFactory) + self.assertEqual({'one': 'one', 'two': 'oneone'}, obj) + class UsingFactoryTestCase(unittest.TestCase): def test_attribute(self): From 1210a06717fd5ebc866c977c30ae204822bbc4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 23 Feb 2016 00:59:45 +0100 Subject: [PATCH 162/714] docs: Add a proper recipe for dumping to dict This trick should help with #68. --- docs/recipes.rst | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index a627e8b4..a3df7be7 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -444,3 +444,34 @@ Forcing the initial value for all projects >>> Account.objects.create(uid=42, name="Blah") >>> AccountFactory.create() # Sets up the account number based on the latest uid + + +Converting a factory's output to a dict +--------------------------------------- + +In order to inject some data to, say, a REST API, it can be useful to fetch the factory's data +as a dict. + +Internally, a factory will: + +1. Merge declarations and overrides from all sources (class definition, call parameters, ...) +2. Resolve them into a dict +3. Pass that dict as keyword arguments to the model's ``build`` / ``create`` function + + +In order to get a dict, we'll just have to swap the model; the easiest way is to use +:meth:`factory.build`: + +.. code-block:: python + + class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.User + + first_name = factory.Sequence(lambda n: "Agent %03d" % n) + username = factory.Faker('username') + +.. code-block:: pycon + + >>> factory.build(dict, FACTORY_CLASS=UserFactory) + {'first_name': "Agent 001", 'username': 'john_doe'} From 5071655e34ba7d51e42454a0fa0463bc4d7c9e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Herv=C3=A9=20Cauwelier?= Date: Fri, 4 Mar 2016 18:19:07 +0100 Subject: [PATCH 163/714] Stop advertising factory_boy supports Python 2.6 Support was already gone with literal sets. --- README.rst | 2 +- setup.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 762fedb9..a08d37fd 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.6, 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. Download diff --git a/setup.py b/setup.py index 003dd088..3a0e6990 100755 --- a/setup.py +++ b/setup.py @@ -61,7 +61,6 @@ def get_version(package_name): "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", From dc1de7297b9b508afad9386285aebd57ac3f11f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 12 Mar 2016 12:45:34 +0100 Subject: [PATCH 164/714] django: Clarify behavior around ``abstract=True`` See issue #280. --- tests/test_django.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_django.py b/tests/test_django.py index 103df911..08349b91 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -362,6 +362,22 @@ def test_auto_sequence(self): obj = ConcreteGrandSonFactory() self.assertEqual(1, obj.pk) + def test_optional_abstract(self): + """Users need not describe the factory for an abstract model as abstract.""" + class AbstractBaseFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.AbstractBase + + foo = factory.Sequence(lambda n: "foo%d" % n) + + class ConcreteSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteSon + + obj = ConcreteSonFactory() + self.assertEqual(1, obj.pk) + self.assertEqual("foo0", obj.foo) + @unittest.skipIf(django is None, "Django not installed.") class DjangoRelatedFieldTestCase(django_test.TestCase): From 094a66fb0e6a70c15cc7cbdee5d40ba5e128c433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 12 Mar 2016 12:47:25 +0100 Subject: [PATCH 165/714] docs: Del obsolete note on abstract Django models That section described code required when factory_boy was automagically computing sequence numbers from the current PK value. Closes #280 --- docs/orms.rst | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index af20917f..fb3543de 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -96,22 +96,6 @@ 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): - class Meta: - model = models.MyAbstractModel - abstract = True - - class MyConcreteModelFactory(MyAbstractModelFactory): - class Meta: - model = models.MyConcreteModel - - Otherwise, factory_boy will try to get the 'next PK' counter from the abstract model. - Extra fields """""""""""" From eea28cce1544021f3d152782c9932a20402d6240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:11:58 +0200 Subject: [PATCH 166/714] Refactor: move error defs to a dedicated module. --- factory/__init__.py | 7 +++++-- factory/base.py | 31 ++++++++--------------------- factory/containers.py | 8 ++------ factory/errors.py | 42 ++++++++++++++++++++++++++++++++++++++++ tests/test_base.py | 11 ++++++----- tests/test_containers.py | 3 ++- 6 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 factory/errors.py diff --git a/factory/__init__.py b/factory/__init__.py index 1fa581b3..ccd71cd5 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -32,14 +32,17 @@ ListFactory, StubFactory, - FactoryError, - BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, use_strategy, ) + +from .errors import ( + FactoryError, +) + from .faker import Faker from .declarations import ( diff --git a/factory/base.py b/factory/base.py index 0f2af597..1ddb7427 100644 --- a/factory/base.py +++ b/factory/base.py @@ -24,6 +24,7 @@ from . import containers from . import declarations +from . import errors from . import utils logger = logging.getLogger('factory.generate') @@ -35,22 +36,6 @@ -class FactoryError(Exception): - """Any exception raised by factory_boy.""" - - -class AssociatedClassError(FactoryError): - """Exception for Factory subclasses lacking Meta.model.""" - - -class UnknownStrategy(FactoryError): - """Raised when a factory uses an unknown strategy.""" - - -class UnsupportedStrategy(FactoryError): - """Raised when trying to use a strategy on an incompatible Factory.""" - - # Factory metaclasses def get_factory_bases(bases): @@ -82,7 +67,7 @@ def __call__(cls, **kwargs): elif cls._meta.strategy == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise UnknownStrategy('Unknown Meta.strategy: {0}'.format( + raise errors.UnknownStrategy('Unknown Meta.strategy: {0}'.format( cls._meta.strategy)) def __new__(mcs, class_name, bases, attrs): @@ -296,12 +281,12 @@ class BaseFactory(object): """Factory base support for sequences, attributes and stubs.""" # Backwards compatibility - UnknownStrategy = UnknownStrategy - UnsupportedStrategy = UnsupportedStrategy + UnknownStrategy = errors.UnknownStrategy + UnsupportedStrategy = errors.UnsupportedStrategy def __new__(cls, *args, **kwargs): """Would be called if trying to instantiate the class.""" - raise FactoryError('You cannot instantiate BaseFactory') + raise errors.FactoryError('You cannot instantiate BaseFactory') _meta = FactoryOptions() @@ -477,7 +462,7 @@ def _generate(cls, create, attrs): attrs (dict): attributes to use for generating the object """ if cls._meta.abstract: - raise FactoryError( + raise errors.FactoryError( "Cannot generate instances of abstract factory %(f)s; " "Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract " "is either not set or False." % dict(f=cls.__name__)) @@ -680,7 +665,7 @@ def simple_generate_batch(cls, create, size, **kwargs): # Backwards compatibility -Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201 +Factory.AssociatedClassError = errors.AssociatedClassError # pylint: disable=W0201 class StubFactory(Factory): @@ -695,7 +680,7 @@ def build(cls, **kwargs): @classmethod def create(cls, **kwargs): - raise UnsupportedStrategy() + raise errors.UnsupportedStrategy() class BaseDictFactory(Factory): diff --git a/factory/containers.py b/factory/containers.py index 0ae354b8..c591988b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -25,13 +25,10 @@ logger = logging.getLogger(__name__) from . import declarations +from . import errors from . import utils -class CyclicDefinitionError(Exception): - """Raised when cyclic definition were found.""" - - class LazyStub(object): """A generic container that only allows getting attributes. @@ -93,7 +90,7 @@ def __getattr__(self, name): attributes being computed. """ if name in self.__pending: - raise CyclicDefinitionError( + raise errors.CyclicDefinitionError( "Cyclic lazy attribute definition for %s; cycle found in %r." % (name, self.__pending)) elif name in self.__values: @@ -112,7 +109,6 @@ def __getattr__(self, name): "The parameter %s is unknown. Evaluated attributes are %r, " "definitions are %r." % (name, self.__values, self.__attrs)) - def __setattr__(self, name, value): """Prevent setting attributes once __init__ is done.""" if not self.__initialized: diff --git a/factory/errors.py b/factory/errors.py new file mode 100644 index 00000000..79d85f4e --- /dev/null +++ b/factory/errors.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2011-2015 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +class FactoryError(Exception): + """Any exception raised by factory_boy.""" + + +class AssociatedClassError(FactoryError): + """Exception for Factory subclasses lacking Meta.model.""" + + +class UnknownStrategy(FactoryError): + """Raised when a factory uses an unknown strategy.""" + + +class UnsupportedStrategy(FactoryError): + """Raised when trying to use a strategy on an incompatible Factory.""" + + +class CyclicDefinitionError(FactoryError): + """Raised when a cyclical declaration occurs.""" + + diff --git a/tests/test_base.py b/tests/test_base.py index dd74e350..a3b3704c 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -24,6 +24,7 @@ from factory import base from factory import declarations +from factory import errors from .compat import unittest @@ -63,7 +64,7 @@ class TestModel(FakeDjangoModel): class SafetyTestCase(unittest.TestCase): def test_base_factory(self): - self.assertRaises(base.FactoryError, base.BaseFactory) + self.assertRaises(errors.FactoryError, base.BaseFactory) class AbstractFactoryTestCase(unittest.TestCase): @@ -88,8 +89,8 @@ 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) + self.assertRaises(errors.FactoryError, TestObjectFactory.build) + self.assertRaises(errors.FactoryError, TestObjectFactory.create) def test_abstract_factory_not_inherited(self): """abstract=True isn't propagated to child classes.""" @@ -110,8 +111,8 @@ class Meta: abstract = False model = None - self.assertRaises(base.FactoryError, TestObjectFactory.build) - self.assertRaises(base.FactoryError, TestObjectFactory.create) + self.assertRaises(errors.FactoryError, TestObjectFactory.build) + self.assertRaises(errors.FactoryError, TestObjectFactory.create) class OptionsTests(unittest.TestCase): diff --git a/tests/test_containers.py b/tests/test_containers.py index 825e8972..20c773a1 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -23,6 +23,7 @@ from factory import base from factory import containers from factory import declarations +from factory import errors from .compat import unittest @@ -88,7 +89,7 @@ def evaluate(self, obj, container=None): stub = containers.LazyStub({'one': LazyAttr('two'), 'two': LazyAttr('one')}) - self.assertRaises(containers.CyclicDefinitionError, getattr, stub, 'one') + self.assertRaises(errors.CyclicDefinitionError, getattr, stub, 'one') def test_representation(self): class RandomObj(object): From c77962de7dd7206ccab85b44da173832acbf5921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:13:34 +0200 Subject: [PATCH 167/714] Add a new Params section to factories. This handles parameters that alter the declarations of a factory. A few technical notes: - A parameter's outcome may alter other parameters - In order to fix that, we perform a (simple) cyclic definition detection at class declaration time. - Parameters may only be either naked values or ComplexParameter subclasses - Parameters are never passed to the underlying class --- docs/reference.rst | 46 ++++++++++++++ factory/base.py | 44 +++++++++++++- factory/containers.py | 108 ++++++++++++++++++++++++++++++--- factory/declarations.py | 33 +++++++++++ factory/utils.py | 4 +- tests/test_containers.py | 125 ++++++++------------------------------- tests/test_using.py | 9 +++ 7 files changed, 256 insertions(+), 113 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index e2f63db2..8550f882 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1299,6 +1299,52 @@ with the :class:`Dict` and :class:`List` attributes: argument, if another type (tuple, set, ...) is required. +Parameters +"""""""""" + +Some models have many fields that can be summarized by a few parameters; for instance, +a train with many cars — each complete with serial number, manufacturer, ...; +or an order that can be pending/shipped/received, with a few fields to describe each step. + +When building instances of such models, a couple of parameters can be enough to determine +all other fields; this is handled by the :class:`~Factory.Params` section of a :class:`Factory` declaration. + + +Simple parameters +~~~~~~~~~~~~~~~~~ + +Some factories only need little data: + +.. code-block:: python + + class ConferenceFactory(factory.Factory): + class Meta: + model = Conference + + class Params: + duration = 'short' # Or 'long' + + start_date = factory.fuzzy.FuzzyDate() + end_date = factory.LazyAttribute( + lambda o: o.start_date + datetime.timedelta(days=2 if o.duration == 'short' else 7) + ) + sprints_start = factory.LazyAttribute( + lambda o: o.end_date - datetime.timedelta(days=0 if o.duration == 'short' else 1) + ) + +.. code-block:: pycon + + >>> Conference(duration='short') + + >>> Conference(duration='long') + + + +Any simple parameter provided to the :class:`Factory.Params` section is available to the whole factory, +but not passed to the final class (similar to the :attr:`~FactoryOptions.exclude` behavior). + + + Post-generation hooks """"""""""""""""""""" diff --git a/factory/base.py b/factory/base.py index 1ddb7427..282e3b10 100644 --- a/factory/base.py +++ b/factory/base.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import collections import logging from . import containers @@ -92,6 +93,7 @@ def __new__(mcs, class_name, bases, attrs): base_factory = None attrs_meta = attrs.pop('Meta', None) + attrs_params = attrs.pop('Params', None) base_meta = resolve_attribute('_meta', bases) options_class = resolve_attribute('_options_class', bases, FactoryOptions) @@ -106,6 +108,7 @@ def __new__(mcs, class_name, bases, attrs): meta=attrs_meta, base_meta=base_meta, base_factory=base_factory, + params=attrs_params, ) return new_class @@ -148,6 +151,8 @@ def __init__(self): self.base_factory = None self.declarations = {} self.postgen_declarations = {} + self.parameters = {} + self.parameters_dependencies = {} def _build_default_options(self): """"Provide the default value for all allowed fields. @@ -186,7 +191,7 @@ def _fill_from_meta(self, meta, base_meta): % (self.factory, ','.join(sorted(meta_attrs.keys())))) def contribute_to_class(self, factory, - meta=None, base_meta=None, base_factory=None): + meta=None, base_meta=None, base_factory=None, params=None): self.factory = factory self.base_factory = base_factory @@ -204,6 +209,7 @@ def contribute_to_class(self, factory, continue self.declarations.update(parent._meta.declarations) self.postgen_declarations.update(parent._meta.postgen_declarations) + self.parameters.update(parent._meta.parameters) for k, v in vars(self.factory).items(): if self._is_declaration(k, v): @@ -211,6 +217,13 @@ def contribute_to_class(self, factory, if self._is_postgen_declaration(k, v): self.postgen_declarations[k] = v + if params is not None: + for k, v in vars(params).items(): + if not k.startswith('_'): + self.parameters[k] = v + + self.parameters_dependencies = self._compute_parameter_dependencies(self.parameters) + def _get_counter_reference(self): """Identify which factory should be used for a shared counter.""" @@ -242,6 +255,32 @@ def _is_postgen_declaration(self, name, value): """Captures instances of PostGenerationDeclaration.""" return isinstance(value, declarations.PostGenerationDeclaration) + def _compute_parameter_dependencies(self, parameters): + """Find out in what order parameters should be called.""" + # Warning: parameters only provide reverse dependencies; we reverse them into standard dependencies. + # deep_revdeps: set of fields a field depend indirectly upon + deep_revdeps = collections.defaultdict(set) + # Actual, direct dependencies + deps = collections.defaultdict(set) + + for name, parameter in parameters.items(): + if isinstance(parameter, declarations.ComplexParameter): + field_revdeps = parameter.get_revdeps(parameters) + if not field_revdeps: + continue + deep_revdeps[name] = set.union(*(deep_revdeps[dep] for dep in field_revdeps)) + deep_revdeps[name] |= set(field_revdeps) + for dep in field_revdeps: + deps[dep].add(name) + + # Check for cyclical dependencies + cyclic = [name for name, field_deps in deep_revdeps.items() if name in field_deps] + if cyclic: + raise errors.CyclicDefinitionError( + "Cyclic definition detected on %s' Params around %s" + % (self.factory, ', '.join(cyclic))) + return deps + def __str__(self): return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__) @@ -439,6 +478,9 @@ def _prepare(cls, create, **kwargs): # Remove 'hidden' arguments. for arg in cls._meta.exclude: del kwargs[arg] + # Remove parameters, if defined + for arg in cls._meta.parameters: + kwargs.pop(arg, None) # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls._meta.inline_args) diff --git a/factory/containers.py b/factory/containers.py index c591988b..d3f39c46 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -117,6 +117,69 @@ def __setattr__(self, name, value): raise AttributeError('Setting of object attributes is not allowed') +class DeclarationStack(object): + """An ordered stack of declarations. + + This is intended to handle declaration precedence among different mutating layers. + """ + def __init__(self, ordering): + self.ordering = ordering + self.layers = dict((name, {}) for name in self.ordering) + + def __getitem__(self, key): + return self.layers[key] + + def __setitem__(self, key, value): + assert key in self.ordering + self.layers[key] = value + + def current(self): + """Retrieve the current, flattened declarations dict.""" + result = {} + for layer in self.ordering: + result.update(self.layers[layer]) + return result + + +class ParameterResolver(object): + """Resolve a factory's parameter declarations.""" + def __init__(self, parameters, deps): + self.parameters = parameters + self.deps = deps + self.declaration_stack = None + + self.resolved = set() + + def resolve_one(self, name): + """Compute one field is needed, taking dependencies into accounts.""" + if name in self.resolved: + return + + for dep in self.deps.get(name, ()): + self.resolve_one(dep) + + self.compute(name) + self.resolved.add(name) + + def compute(self, name): + """Actually compute the value for a given name.""" + value = self.parameters[name] + if isinstance(value, declarations.ComplexParameter): + overrides = value.compute(name, self.declaration_stack.current()) + else: + overrides = {name: value} + self.declaration_stack['overrides'].update(overrides) + + def resolve(self, declaration_stack): + """Resolve parameters for a given declaration stack. + + Modifies the stack in-place. + """ + self.declaration_stack = declaration_stack + for name in self.parameters: + self.resolve_one(name) + + class LazyValue(object): """Some kind of "lazy evaluating" object.""" @@ -125,7 +188,7 @@ def evaluate(self, obj, containers=()): # pragma: no cover raise NotImplementedError("This is an abstract method.") -class OrderedDeclarationWrapper(LazyValue): +class DeclarationWrapper(LazyValue): """Lazy wrapper around an OrderedDeclaration. Attributes: @@ -136,7 +199,7 @@ class OrderedDeclarationWrapper(LazyValue): """ def __init__(self, declaration, sequence, create, extra=None, **kwargs): - super(OrderedDeclarationWrapper, self).__init__(**kwargs) + super(DeclarationWrapper, self).__init__(**kwargs) self.declaration = declaration self.sequence = sequence self.create = create @@ -166,7 +229,7 @@ class AttributeBuilder(object): Attributes: factory (base.Factory): the Factory for which attributes are being built - _attrs (DeclarationDict): the attribute declarations for the factory + _declarations (DeclarationDict): the attribute declarations for the factory _subfields (dict): dict mapping an attribute name to a dict of overridden default values for the related SubFactory. """ @@ -179,20 +242,47 @@ def __init__(self, factory, extra=None, log_ctx=None, **kwargs): self.factory = factory self._containers = extra.pop('__containers', ()) - self._attrs = factory.declarations(extra) + + initial_declarations = dict(factory._meta.declarations) self._log_ctx = log_ctx - initial_declarations = factory.declarations({}) + # Parameters + # ---------- + self._declarations = self.merge_declarations(initial_declarations, extra) + + # Subfields + # --------- + attrs_with_subfields = [ k for k, v in initial_declarations.items() - if self.has_subfields(v)] + if self.has_subfields(v) + ] + # Extract subfields; THIS MODIFIES self._declarations. self._subfields = utils.multi_extract_dict( - attrs_with_subfields, self._attrs) + attrs_with_subfields, self._declarations) def has_subfields(self, value): return isinstance(value, declarations.ParameteredAttribute) + def merge_declarations(self, initial, extra): + """Compute the final declarations, taking into account paramter-based overrides.""" + # Precedence order: + # - Start with class-level declarations + # - Add overrides from parameters + # - Finally, use callsite-level declarations & values + declaration_stack = DeclarationStack(['initial', 'overrides', 'extra']) + declaration_stack['initial'] = initial.copy() + declaration_stack['extra'] = extra.copy() + + # Actually compute the final stack + resolver = ParameterResolver( + parameters=self.factory._meta.parameters, + deps=self.factory._meta.parameters_dependencies, + ) + resolver.resolve(declaration_stack) + return declaration_stack.current() + def build(self, create, force_sequence=None): """Build a dictionary of attributes. @@ -210,9 +300,9 @@ def build(self, create, force_sequence=None): # Parse attribute declarations, wrapping SubFactory and # OrderedDeclaration. wrapped_attrs = {} - for k, v in self._attrs.items(): + for k, v in self._declarations.items(): if isinstance(v, declarations.OrderedDeclaration): - v = OrderedDeclarationWrapper(v, + v = DeclarationWrapper(v, sequence=sequence, create=create, extra=self._subfields.get(k, {}), diff --git a/factory/declarations.py b/factory/declarations.py index 9ab74628..ad1f72f6 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -440,6 +440,39 @@ def generate(self, sequence, obj, create, params): **params) +# Parameters +# ========== + + +class ComplexParameter(object): + """A complex parameter, to be used in a Factory.Params section. + + Must implement: + - A "compute" function, performing the actual declaration override + - Optionally, a get_revdeps() function (to compute other parameters it may alter) + """ + + def compute(self, field_name, declarations): + """Compute the overrides for this parameter. + + Args: + - field_name (str): the field this parameter is installed at + - declarations (dict): the global factory declarations + + Returns: + dict: the declarations to override + """ + raise NotImplementedError() + + def get_revdeps(self, parameters): + """Retrieve the list of other parameters modified by this one.""" + return [] + + +# Post-generation +# =============== + + class ExtractionContext(object): """Private class holding all required context from extraction to postgen.""" def __init__(self, value=None, did_extract=False, extra=None, for_field=''): diff --git a/factory/utils.py b/factory/utils.py index 15dba0ab..cfae4ecd 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -35,7 +35,7 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): Args: prefix (str): the prefix to use for lookups - kwargs (dict): the dict from which values should be extracted + kwargs (dict): the dict from which values should be extracted; WILL BE MODIFIED. pop (bool): whether to use pop (True) or get (False) exclude (iterable): list of prefixed keys that shouldn't be extracted @@ -68,7 +68,7 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): Args: prefixes (str list): the prefixes to use for lookups - kwargs (dict): the dict from which values should be extracted + kwargs (dict): the dict from which values should be extracted; WILL BE MODIFIED. pop (bool): whether to use pop (True) or get (False) exclude (iterable): list of prefixed keys that shouldn't be extracted diff --git a/tests/test_containers.py b/tests/test_containers.py index 20c773a1..a3083530 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -103,97 +103,56 @@ class RandomObj(object): class AttributeBuilderTestCase(unittest.TestCase): - def test_empty(self): - """Tests building attributes from an empty definition.""" + + def make_fake_factory(self, decls): + class Meta: + declarations = decls + parameters = {} + parameters_dependencies = {} class FakeFactory(object): - @classmethod - def declarations(cls, extra): - return extra + _meta = Meta @classmethod def _generate_next_sequence(cls): return 1 + return FakeFactory + + def test_empty(self): + """Tests building attributes from an empty definition.""" + + FakeFactory = self.make_fake_factory({}) ab = containers.AttributeBuilder(FakeFactory) self.assertEqual({}, ab.build(create=False)) def test_factory_defined(self): - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 - + FakeFactory = self.make_fake_factory({'one': 1}) ab = containers.AttributeBuilder(FakeFactory) + self.assertEqual({'one': 1}, ab.build(create=False)) def test_extended(self): - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 - + FakeFactory = self.make_fake_factory({'one': 1}) ab = containers.AttributeBuilder(FakeFactory, {'two': 2}) self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False)) def test_overridden(self): - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 - + FakeFactory = self.make_fake_factory({'one': 1}) ab = containers.AttributeBuilder(FakeFactory, {'one': 2}) self.assertEqual({'one': 2}, ab.build(create=False)) def test_factory_defined_sequence(self): seq = declarations.Sequence(lambda n: 'xx%d' % n) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': seq} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': seq}) ab = containers.AttributeBuilder(FakeFactory) self.assertEqual({'one': 'xx1'}, ab.build(create=False)) def test_additionnal_sequence(self): seq = declarations.Sequence(lambda n: 'xx%d' % n) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': 1}) ab = containers.AttributeBuilder(FakeFactory, extra={'two': seq}) self.assertEqual({'one': 1, 'two': 'xx1'}, ab.build(create=False)) @@ -201,34 +160,14 @@ def _generate_next_sequence(cls): def test_replaced_sequence(self): seq = declarations.Sequence(lambda n: 'xx%d' % n) seq2 = declarations.Sequence(lambda n: 'yy%d' % n) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': seq} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': seq}) ab = containers.AttributeBuilder(FakeFactory, extra={'one': seq2}) self.assertEqual({'one': 'yy1'}, ab.build(create=False)) def test_lazy_function(self): lf = declarations.LazyFunction(int) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1, 'two': lf} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': 1, 'two': lf}) ab = containers.AttributeBuilder(FakeFactory) self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False)) @@ -241,17 +180,7 @@ def _generate_next_sequence(cls): def test_lazy_attribute(self): la = declarations.LazyAttribute(lambda a: a.one * 2) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': 1, 'two': la} - d.update(extra) - return d - - @classmethod - def _generate_next_sequence(cls): - return 1 + FakeFactory = self.make_fake_factory({'one': 1, 'two': la}) ab = containers.AttributeBuilder(FakeFactory) self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False)) @@ -267,18 +196,12 @@ class FakeInnerFactory(object): pass sf = declarations.SubFactory(FakeInnerFactory) - - class FakeFactory(object): - @classmethod - def declarations(cls, extra): - d = {'one': sf, 'two': 2} - d.update(extra) - return d + FakeFactory = self.make_fake_factory({'one': sf, 'two': 2}) ab = containers.AttributeBuilder(FakeFactory, {'one__blah': 1, 'two__bar': 2}) self.assertTrue(ab.has_subfields(sf)) self.assertEqual(['one'], list(ab._subfields.keys())) - self.assertEqual(2, ab._attrs['two__bar']) + self.assertEqual(2, ab._declarations['two__bar']) def test_sub_factory(self): pass diff --git a/tests/test_using.py b/tests/test_using.py index 3ef54035..67db3bc7 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -40,6 +40,15 @@ def __init__(self, one=None, two=None, three=None, four=None, five=None): self.four = four self.five = five + def as_dict(self): + return dict( + one=self.one, + two=self.two, + three=self.three, + four=self.four, + five=self.five, + ) + class FakeModel(object): @classmethod From 03c40fd80707ad4837523a07cdf3f82564ab0259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:14:06 +0200 Subject: [PATCH 168/714] Add Traits (Closes #251). Based on a boolean flag, those will alter the definitions of the current factory, taking precedence over pre-defined behavior but overridden by callsite-level arguments. --- README.rst | 21 ++-- docs/introduction.rst | 55 ++++++++++ docs/reference.rst | 230 ++++++++++++++++++++++++++++++++-------- factory/__init__.py | 1 + factory/declarations.py | 16 +++ tests/test_using.py | 114 ++++++++++++++++++++ 6 files changed, 382 insertions(+), 55 deletions(-) diff --git a/README.rst b/README.rst index a08d37fd..0e1f0007 100644 --- a/README.rst +++ b/README.rst @@ -292,6 +292,17 @@ The associated object's strategy will be used: True +ORM Support +""""""""""" + +factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses: + +* Django, with ``factory.django.DjangoModelFactory`` +* Mogo, with ``factory.mogo.MogoFactory`` +* MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` +* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` + + Debugging factory_boy """"""""""""""""""""" @@ -326,16 +337,6 @@ This will yield messages similar to those (artificial indentation): BaseFactory: Generating tests.test_using.TestModel2Factory(two=) -ORM Support -""""""""""" - -factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses: - -* Django, with ``factory.django.DjangoModelFactory`` -* Mogo, with ``factory.mogo.MogoFactory`` -* MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` -* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` - Contributing ------------ diff --git a/docs/introduction.rst b/docs/introduction.rst index 9a16c398..5b535c94 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -266,6 +266,61 @@ This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute: +Altering a factory's behaviour: parameters and traits +----------------------------------------------------- + +Some classes are better described with a few, simple parameters, that aren't fields on the actual model. +In that case, use a :attr:`~factory.Factory.Params` declaration: + +.. code-block:: python + + class RentalFactory(factory.Factory): + class Meta: + model = Rental + + begin = factory.fuzzy.FuzzyDate(start_date=datetime.date(2000, 1, 1)) + end = factory.LazyAttribute(lambda o: o.begin + o.duration) + + class Params: + duration = 12 + +.. code-block:: pycon + + >>> RentalFactory(duration=0) + 2012-03-03> + >>> RentalFactory(duration=10) + 2012-12-26> + + +When many fields should be updated based on a flag, use :class:`Traits ` instead: + +.. code-block:: python + + class OrderFactory(factory.Factory): + status = 'pending' + shipped_by = None + shipped_on = None + + class Meta: + model = Order + + class Params: + shipped = factory.Trait( + status='shipped', + shipped_by=factory.SubFactory(EmployeeFactory), + shipped_on=factory.LazyFunction(datetime.date.today), + ) + +A trait is toggled by a single boolean value: + +.. code-block:: pycon + + >>> OrderFactory() + + >>> OrderFactory(shipped=True) + + + Strategies ---------- diff --git a/docs/reference.rst b/docs/reference.rst index 8550f882..ad68fafa 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -11,6 +11,9 @@ For internals and customization points, please refer to the :doc:`internals` sec The :class:`Factory` class -------------------------- +Meta options +"""""""""""" + .. class:: FactoryOptions .. versionadded:: 2.4.0 @@ -135,11 +138,16 @@ The :class:`Factory` class +Attributes and methods +"""""""""""""""""""""" + + .. class:: Factory **Class-level attributes:** + .. attribute:: Meta .. attribute:: _meta .. versionadded:: 2.4.0 @@ -147,6 +155,14 @@ The :class:`Factory` class The :class:`FactoryOptions` instance attached to a :class:`Factory` class is available as a :attr:`_meta` attribute. + .. attribute:: Params + + .. versionadded:: 2.7.0 + + The extra parameters attached to a :class:`Factory` are declared through a :attr:`Params` + class. + See :ref:`the "Parameters" section ` for more information. + .. attribute:: _options_class .. versionadded:: 2.4.0 @@ -353,6 +369,175 @@ The :class:`Factory` class factory in the chain. +.. _parameters: + +Parameters +"""""""""" + +.. versionadded:: 2.7.0 + +Some models have many fields that can be summarized by a few parameters; for instance, +a train with many cars — each complete with serial number, manufacturer, ...; +or an order that can be pending/shipped/received, with a few fields to describe each step. + +When building instances of such models, a couple of parameters can be enough to determine +all other fields; this is handled by the :class:`~Factory.Params` section of a :class:`Factory` declaration. + + +Simple parameters +~~~~~~~~~~~~~~~~~ + +Some factories only need little data: + +.. code-block:: python + + class ConferenceFactory(factory.Factory): + class Meta: + model = Conference + + class Params: + duration = 'short' # Or 'long' + + start_date = factory.fuzzy.FuzzyDate() + end_date = factory.LazyAttribute( + lambda o: o.start_date + datetime.timedelta(days=2 if o.duration == 'short' else 7) + ) + sprints_start = factory.LazyAttribute( + lambda o: o.end_date - datetime.timedelta(days=0 if o.duration == 'short' else 1) + ) + +.. code-block:: pycon + + >>> Conference(duration='short') + + >>> Conference(duration='long') + + + +Any simple parameter provided to the :class:`Factory.Params` section is available to the whole factory, +but not passed to the final class (similar to the :attr:`~FactoryOptions.exclude` behavior). + + +Traits +~~~~~~ + +.. class:: Trait(**kwargs) + + .. OHAI VIM** + + .. versionadded:: 2.7.0 + + A trait's parameters are the fields it sohuld alter when enabled. + + +For more complex situations, it is helpful to override a few fields at once: + +.. code-block:: python + + class OrderFactory(factory.Factory): + class Meta: + model = Order + + state = 'pending' + shipped_on = None + shipped_by = None + + class Params: + shipped = factory.Trait( + state='shipped', + shipped_on=datetime.date.today, + shipped_by=factory.SubFactory(EmployeeFactory), + ) + +Such a :class:`Trait` is activated or disabled by a single boolean field: + + +.. code-block:: pycon + + >>> OrderFactory() + + Order(state='pending') + >>> OrderFactory(shipped=True) + + + +A :class:`Trait` can be enabled/disabled by a :class:`Factory` subclass: + +.. code-block:: python + + class ShippedOrderFactory(OrderFactory): + shipped = True + + +Values set in a :class:`Trait` can be overridden by call-time values: + +.. code-block:: pycon + + >>> OrderFactory(shipped=True, shipped_on=last_year) + + + +:class:`Traits ` can be chained: + +.. code-block:: python + + class OrderFactory(factory.Factory): + class Meta: + model = Order + + # Can be pending/shipping/received + state = 'pending' + shipped_on = None + shipped_by = None + received_on = None + received_by = None + + class Params: + shipped = factory.Trait( + state='shipped', + shipped_on=datetime.date.today, + shipped_by=factory.SubFactory(EmployeeFactory), + ) + received = factory.Trait( + shipped=True, + state='received', + shipped_on=datetime.date.today - datetime.timedelta(days=4), + received_on=datetime.date.today, + received_by=factory.SubFactory(CustomerFactory), + ) + +.. code-block:: pycon + + >>> OrderFactory(received=True) + + + + +A :class:`Trait` might be overridden in :class:`Factory` subclasses: + +.. code-block:: python + + class LocalOrderFactory(OrderFactory): + + class Params: + received = factory.Trait( + shipped=True, + state='received', + shipped_on=datetime.date.today - datetime.timedelta(days=1), + received_on=datetime.date.today, + received_by=factory.SubFactory(CustomerFactory), + ) + + +.. code-block:: pycon + + >>> LocalOrderFactory(received=True) + + + +.. note:: When overriding a :class:`Trait`, the whole declaration **MUST** be replaced. + + .. _strategies: Strategies @@ -1299,51 +1484,6 @@ with the :class:`Dict` and :class:`List` attributes: argument, if another type (tuple, set, ...) is required. -Parameters -"""""""""" - -Some models have many fields that can be summarized by a few parameters; for instance, -a train with many cars — each complete with serial number, manufacturer, ...; -or an order that can be pending/shipped/received, with a few fields to describe each step. - -When building instances of such models, a couple of parameters can be enough to determine -all other fields; this is handled by the :class:`~Factory.Params` section of a :class:`Factory` declaration. - - -Simple parameters -~~~~~~~~~~~~~~~~~ - -Some factories only need little data: - -.. code-block:: python - - class ConferenceFactory(factory.Factory): - class Meta: - model = Conference - - class Params: - duration = 'short' # Or 'long' - - start_date = factory.fuzzy.FuzzyDate() - end_date = factory.LazyAttribute( - lambda o: o.start_date + datetime.timedelta(days=2 if o.duration == 'short' else 7) - ) - sprints_start = factory.LazyAttribute( - lambda o: o.end_date - datetime.timedelta(days=0 if o.duration == 'short' else 1) - ) - -.. code-block:: pycon - - >>> Conference(duration='short') - - >>> Conference(duration='long') - - - -Any simple parameter provided to the :class:`Factory.Params` section is available to the whole factory, -but not passed to the final class (similar to the :attr:`~FactoryOptions.exclude` behavior). - - Post-generation hooks """"""""""""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index ccd71cd5..ad9da800 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -52,6 +52,7 @@ Sequence, LazyAttributeSequence, SelfAttribute, + Trait, ContainerAttribute, SubFactory, Dict, diff --git a/factory/declarations.py b/factory/declarations.py index ad1f72f6..895f2ac3 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -469,6 +469,22 @@ def get_revdeps(self, parameters): return [] +class Trait(ComplexParameter): + """The simplest complex parameter, it enables a bunch of new declarations based on a boolean flag.""" + def __init__(self, **overrides): + self.overrides = overrides + + def compute(self, field_name, declarations): + if declarations.get(field_name): + return self.overrides + else: + return {} + + def get_revdeps(self, parameters): + """This might alter fields it's injecting.""" + return [param for param in parameters if param in self.overrides] + + # Post-generation # =============== diff --git a/tests/test_using.py b/tests/test_using.py index 67db3bc7..eaeb8daa 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -27,6 +27,7 @@ import warnings import factory +from factory import errors from .compat import is_python2, unittest from . import tools @@ -1114,6 +1115,119 @@ class Meta: self.assertEqual(42, obj.attributes) +class TraitTestCase(unittest.TestCase): + def test_traits(self): + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + even = factory.Trait(two=True, four=True) + odd = factory.Trait(one=True, three=True, five=True) + + obj1 = TestObjectFactory() + self.assertEqual(obj1.as_dict(), + dict(one=None, two=None, three=None, four=None, five=None)) + + obj2 = TestObjectFactory(even=True) + self.assertEqual(obj2.as_dict(), + dict(one=None, two=True, three=None, four=True, five=None)) + + obj3 = TestObjectFactory(odd=True) + self.assertEqual(obj3.as_dict(), + dict(one=True, two=None, three=True, four=None, five=True)) + + obj4 = TestObjectFactory(even=True, odd=True) + self.assertEqual(obj4.as_dict(), + dict(one=True, two=True, three=True, four=True, five=True)) + + obj5 = TestObjectFactory(odd=True, two=True) + self.assertEqual(obj5.as_dict(), + dict(one=True, two=True, three=True, four=None, five=True)) + + def test_traits_inheritance(self): + """A trait can be set in an inherited class.""" + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + even = factory.Trait(two=True, four=True) + odd = factory.Trait(one=True, three=True, five=True) + + class EvenObjectFactory(TestObjectFactory): + even = True + + # Simple call + obj1 = EvenObjectFactory() + self.assertEqual(obj1.as_dict(), + dict(one=None, two=True, three=None, four=True, five=None)) + + # Force-disable it + obj2 = EvenObjectFactory(even=False) + self.assertEqual(obj2.as_dict(), + dict(one=None, two=None, three=None, four=None, five=None)) + + def test_traits_override(self): + """Override a trait in a subclass.""" + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + even = factory.Trait(two=True, four=True) + odd = factory.Trait(one=True, three=True, five=True) + + class WeirdMathFactory(TestObjectFactory): + class Params: + # Here, one is even. + even = factory.Trait(two=True, four=True, one=True) + + obj = WeirdMathFactory(even=True) + self.assertEqual(obj.as_dict(), + dict(one=True, two=True, three=None, four=True, five=None)) + + def test_traits_chaining(self): + """Use a trait to enable other traits.""" + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + even = factory.Trait(two=True, four=True) + odd = factory.Trait(one=True, three=True, five=True) + full = factory.Trait(even=True, odd=True) + + # Setting "full" should enable all fields. + obj = TestObjectFactory(full=True) + self.assertEqual(obj.as_dict(), + dict(one=True, two=True, three=True, four=True, five=True)) + + # Does it break usual patterns? + obj1 = TestObjectFactory() + self.assertEqual(obj1.as_dict(), + dict(one=None, two=None, three=None, four=None, five=None)) + + obj2 = TestObjectFactory(even=True) + self.assertEqual(obj2.as_dict(), + dict(one=None, two=True, three=None, four=True, five=None)) + + obj3 = TestObjectFactory(odd=True) + self.assertEqual(obj3.as_dict(), + dict(one=True, two=None, three=True, four=None, five=True)) + + def test_prevent_cyclic_traits(self): + + with self.assertRaises(errors.CyclicDefinitionError): + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + a = factory.Trait(b=True, one=True) + b = factory.Trait(a=True, two=True) + + class SubFactoryTestCase(unittest.TestCase): def test_sub_factory(self): class TestModel2(FakeModel): From c22729d03d291814ae196ce7652954db9e42ed97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Apr 2016 16:48:10 +0200 Subject: [PATCH 169/714] Plan for 2.7.0 release. --- docs/changelog.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index dc3f967e..b1fd314a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,21 @@ ChangeLog ========= +.. _v2.7.0: + +2.7.0 (2016-04-03) +------------------ + +*New:* + + - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambada parameters, + thanks to `Hervé Cauwelier `_. + - :issue:`251`: Add :ref:`parameterized factories ` and :class:`traits ` + +*Removed:* + + - :issue:`278`: Formally drop support for Python2.6 + .. _v2.6.1: 2.6.1 (2016-02-10) From 5836329889ac45034978c69b6a6f7de4b0b5b75d Mon Sep 17 00:00:00 2001 From: Samuel Paccoud Date: Sun, 3 Apr 2016 09:42:40 +0200 Subject: [PATCH 170/714] Add documentation and test for subfactory using "factory_parent" attribute Add documentation on how to use a LazyAttribute in a SubFactory and poke the "factory_parent" attribute to indirectly derive the value of a field on the child factory from a field on the parent factory. This commit adds an example to recipes that explains how it can be done. It also adds a test to make sure that this feature continues to work as is now described in the documentation. --- docs/recipes.rst | 14 ++++++++++++++ tests/test_using.py | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index a3df7be7..fe18f501 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -327,7 +327,21 @@ Here, we want: country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, country=factory.SelfAttribute('..country')) +If the value of a field on the child factory is indirectly derived from a field on the parent factory, you will need to use LazyAttribute and poke the "factory_parent" attribute. +This time, we want the company owner to live in a country neighboring the country of the company: + +.. code-block:: python + + class CompanyFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.Company + + name = "ACME, Inc." + country = factory.SubFactory(CountryFactory) + owner = factory.SubFactory(UserFactory, + country=factory.LazyAttribute(lambda o: get_random_neighbor(o.factory_parent.country))) + Custom manager methods ---------------------- diff --git a/tests/test_using.py b/tests/test_using.py index eaeb8daa..0ce29e97 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1268,6 +1268,26 @@ class Meta: self.assertEqual('x0x', test_model.two.one) self.assertEqual('x0xx0x', test_model.two.two) + def test_sub_factory_with_lazy_fields_access_factory_parent(self): + class TestModel2(FakeModel): + pass + + class TestModelFactory(FakeModelFactory): + class Meta: + model = TestModel + one = 3 + + class TestModel2Factory(FakeModelFactory): + class Meta: + model = TestModel2 + one = 'parent' + child = factory.SubFactory(TestModelFactory, + one=factory.LazyAttribute(lambda o: '%s child' % o.factory_parent.one), + ) + + test_model = TestModel2Factory() + self.assertEqual('parent child', test_model.child.one) + def test_sub_factory_and_sequence(self): class TestObject(object): def __init__(self, **kwargs): From b401c3d2eac2aa9b73815034b44af75f65e6d7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 6 Apr 2016 23:51:15 +0200 Subject: [PATCH 171/714] Refactor test setup/commands, enable tox. Closes #273. --- MANIFEST.in | 2 + Makefile | 42 ++++++-------------- dev_requirements.txt => requirements_dev.txt | 5 ++- requirements_test.txt | 2 + tox.ini | 35 ++++++++++++++++ 5 files changed, 54 insertions(+), 32 deletions(-) rename dev_requirements.txt => requirements_dev.txt (75%) create mode 100644 requirements_test.txt create mode 100644 tox.ini diff --git a/MANIFEST.in b/MANIFEST.in index 3dfc1bef..22f4a5c1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include README.rst +include requirements*.txt + include docs/Makefile recursive-include docs *.py *.rst include docs/_static/.keep_dir diff --git a/Makefile b/Makefile index da8ac88b..de0243f8 100644 --- a/Makefile +++ b/Makefile @@ -6,55 +6,37 @@ EXAMPLES_DIR=examples # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) -# Dependencies -DJANGO ?= 1.9 -NEXT_DJANGO = $(shell python -c "v='$(DJANGO)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") - -ALCHEMY ?= 1.0 -NEXT_ALCHEMY = $(shell python -c "v='$(ALCHEMY)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") - -MONGOENGINE ?= 0.10 -NEXT_MONGOENGINE = $(shell python -c "v='$(MONGOENGINE)'; parts=v.split('.'); parts[-1]=str(int(parts[-1])+1); print('.'.join(parts))") - -REQ_FILE = auto_dev_requirements_django$(DJANGO)_alchemy$(ALCHEMY)_mongoengine$(MONGOENGINE).txt -EXAMPLES_REQ_FILES = $(shell find $(EXAMPLES_DIR) -name requirements.txt) - all: default default: -install-deps: $(REQ_FILE) - pip install --upgrade pip setuptools - pip install --upgrade -r $< - pip freeze - -$(REQ_FILE): dev_requirements.txt requirements.txt $(EXAMPLES_REQ_FILES) - grep --no-filename "^[^#-]" $^ | egrep -v "^(Django|SQLAlchemy|mongoengine)" > $@ - echo "Django>=$(DJANGO),<$(NEXT_DJANGO)" >> $@ - echo "SQLAlchemy>=$(ALCHEMY),<$(NEXT_ALCHEMY)" >> $@ - echo "mongoengine>=$(MONGOENGINE),<$(NEXT_MONGOENGINE)" >> $@ - - clean: find . -type f -name '*.pyc' -delete find . -type f -path '*/__pycache__/*' -delete find . -type d -empty -delete - @rm -f auto_dev_requirements_* @rm -rf tmp_test/ -test: install-deps example-test - python -W default setup.py test +install-deps: + pip install --upgrade pip setuptools + pip install --upgrade -r requirements_dev.txt + pip freeze + +testall: + tox + +test: + python -Wdefault -m unittest $(TESTS_DIR) example-test: $(MAKE) -C $(EXAMPLES_DIR) test -pylint: +lint: pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ -coverage: install-deps +coverage: $(COVERAGE) erase $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" diff --git a/dev_requirements.txt b/requirements_dev.txt similarity index 75% rename from dev_requirements.txt rename to requirements_dev.txt index c78aa9d6..a7706c65 100644 --- a/dev_requirements.txt +++ b/requirements_dev.txt @@ -1,4 +1,5 @@ --r requirements.txt +-e . +-r requirements_test.txt -r examples/requirements.txt coverage @@ -6,8 +7,8 @@ Django Pillow SQLAlchemy mongoengine -mock wheel +tox Sphinx sphinx_rtd_theme diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 00000000..e37a8fea --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,2 @@ +mock +pylint diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..bbcf5384 --- /dev/null +++ b/tox.ini @@ -0,0 +1,35 @@ +[tox] +envlist = + py{27,34}-django{17,18,19} + py{27,34}-alchemy10 + py{27,34}-mongoengine010 + examples + lint + +toxworkdir = {env:TOX_WORKDIR:.tox} + +[testenv] +deps = + -rrequirements_test.txt + django17: Django>=1.7,<1.8 + django18: Django>=1.8,<1.9 + django19: Django>=1.9,<1.10 + django{17,18,19}: Pillow + alchemy10: SQLAlchemy>=1.0,<1.1 + mongoengine010: mongoengine>=0.10,<0.11 + +whitelist_externals = make +commands = make test + +[testenv:examples] +basepython = python3.4 +deps = + -rrequirements_test.txt + -rexamples/requirements.txt + +whitelist_externals = make +commands = make example-test + +[testenv:lint] +whitelist_externals = make +commands = make lint From 94a9b2cf9ddd7be181c984793a5404e2b95a779e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 6 Apr 2016 23:51:41 +0200 Subject: [PATCH 172/714] Cleanup manifest. Also, use check_manifest to check for MANIFEST.in / git mismatches. --- MANIFEST.in | 16 ++++++++++------ Makefile | 1 + tox.ini | 4 ++++ 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 22f4a5c1..19b0f8cd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,12 @@ -include README.rst +include ChangeLog LICENSE README.rst include requirements*.txt -include docs/Makefile -recursive-include docs *.py *.rst -include docs/_static/.keep_dir -prune docs/_build -recursive-include tests *.py *.data +graft factory + +prune docs +prune examples +prune tests + +global-exclude .py[cod] __pycache__ + +exclude Makefile tox.ini .pylintrc diff --git a/Makefile b/Makefile index de0243f8..8f48c6b4 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ example-test: $(MAKE) -C $(EXAMPLES_DIR) test lint: + check-manifest pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ coverage: diff --git a/tox.ini b/tox.ini index bbcf5384..4ccade64 100644 --- a/tox.ini +++ b/tox.ini @@ -31,5 +31,9 @@ whitelist_externals = make commands = make example-test [testenv:lint] +deps = + pylint + check_manifest + whitelist_externals = make commands = make lint From fbcf1285b9b756ef7b8d8cf7fcfe8ad245f79bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 7 Apr 2016 00:16:14 +0200 Subject: [PATCH 173/714] Fix a17b036: Error messages have moved. --- tests/test_containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_containers.py b/tests/test_containers.py index ea6ddffd..1b87b2ec 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -102,7 +102,7 @@ def __init__(self, attname, defvalue): def evaluate(self, obj, container=None): try: return super(LazyAttrDefault, self).evaluate(obj, container) - except containers.CyclicDefinitionError: + except errors.CyclicDefinitionError: return self.defvalue stub = containers.LazyStub({ From d9098a809db170f8ffc52efe3e5c6e7f48aa2893 Mon Sep 17 00:00:00 2001 From: Rich Rauenzahn Date: Tue, 12 Apr 2016 14:57:28 -0700 Subject: [PATCH 174/714] Add custom error message when django_get_or_create is missing an input. --- factory/django.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/factory/django.py b/factory/django.py index b3c508c4..43434c22 100644 --- a/factory/django.py +++ b/factory/django.py @@ -29,6 +29,8 @@ import logging import functools +from . import errors + """factory_boy extensions for use with the Django framework.""" try: @@ -157,6 +159,11 @@ def _get_or_create(cls, model_class, *args, **kwargs): key_fields = {} for field in cls._meta.django_get_or_create: + if field not in kwargs: + raise errors.FactoryError( + "django_get_or_create - " + "Unable to find initialization value for '%s' in factory %s" % + (field, cls.__name__)) key_fields[field] = kwargs.pop(field) key_fields['defaults'] = kwargs From ae43bc4c116e6e740f62c6ce4b134e62e21fa682 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 19 Apr 2016 11:55:22 -0700 Subject: [PATCH 175/714] Fix typo --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index ad68fafa..a060f758 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -111,7 +111,7 @@ Meta options .. attribute:: rename - Sometimes, a model expect a field with a name already used by one + Sometimes, a model expects a field with a name already used by one of :class:`Factory`'s methods. In this case, the :attr:`rename` attributes allows to define renaming From f1ed74e06dfb6851bc691ebfd8135c875154ad50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 19 Apr 2016 22:54:36 +0200 Subject: [PATCH 176/714] Release version 2.7.0 --- docs/changelog.rst | 3 ++- factory/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b1fd314a..2341dfa1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. _v2.7.0: -2.7.0 (2016-04-03) +2.7.0 (2016-04-19) ------------------ *New:* @@ -11,6 +11,7 @@ ChangeLog - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambada parameters, thanks to `Hervé Cauwelier `_. - :issue:`251`: Add :ref:`parameterized factories ` and :class:`traits ` + - :issue:`256`, :issue:`292`: Improve error messages in corner cases *Removed:* diff --git a/factory/__init__.py b/factory/__init__.py index ad9da800..421e1abc 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.6.1' +__version__ = '2.7.0' __author__ = 'Raphaël Barrois ' From aaa00465c11847e39d0a6e77cc660c45b9f48100 Mon Sep 17 00:00:00 2001 From: tsouvarev Date: Wed, 20 Apr 2016 11:51:39 +0400 Subject: [PATCH 177/714] Fix typo in changelog.rst --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2341dfa1..c2068c0e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,7 @@ ChangeLog *New:* - - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambada parameters, + - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambda parameters, thanks to `Hervé Cauwelier `_. - :issue:`251`: Add :ref:`parameterized factories ` and :class:`traits ` - :issue:`256`, :issue:`292`: Improve error messages in corner cases From 28f958189199a2c7002c943e0bff248253fbaeb0 Mon Sep 17 00:00:00 2001 From: tsouvarev Date: Wed, 20 Apr 2016 11:52:55 +0400 Subject: [PATCH 178/714] Fix typo in reference.rst --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index a060f758..a0c774b4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -427,7 +427,7 @@ Traits .. versionadded:: 2.7.0 - A trait's parameters are the fields it sohuld alter when enabled. + A trait's parameters are the fields it should alter when enabled. For more complex situations, it is helpful to override a few fields at once: From 9cbdac28e3122fd58f20c00fd38054de434ba823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 9 May 2016 14:36:21 +0200 Subject: [PATCH 179/714] Fix typo in reference/traits section. Parameters to a trait have the same behavior as usual factory declarations. --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index a0c774b4..08678d95 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -445,7 +445,7 @@ For more complex situations, it is helpful to override a few fields at once: class Params: shipped = factory.Trait( state='shipped', - shipped_on=datetime.date.today, + shipped_on=datetime.date.today(), shipped_by=factory.SubFactory(EmployeeFactory), ) From 4d867ff24a39afc13429e7bbaa191519ce35c096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 May 2016 11:06:38 +0200 Subject: [PATCH 180/714] Add test for mixed RelatedFactory/Params (See #296). --- tests/test_using.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 0ce29e97..16a692e0 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2109,6 +2109,40 @@ class Meta: self.assertEqual(1, obj.related.one) self.assertEqual(4, obj.related.two) + def test_parameterized_related_factory(self): + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + obj.related = self + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactory(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + blah = 1 + + one = 3 + two = 2 + three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') + three__two = factory.SelfAttribute('blah') + + obj = TestObjectFactory.build() + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + self.assertEqual(1, obj.related.one) + self.assertEqual(1, obj.related.two) + + obj2 = TestObjectFactory.build(blah='blah') + self.assertEqual('blah', obj2.related.two) class RelatedFactoryExtractionTestCase(unittest.TestCase): From 555d999e8f9d18aee06749b826579059960d8536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 May 2016 11:52:59 +0200 Subject: [PATCH 181/714] Improve debug logging of mixed str/bytes repr (Py2) Fixes #300. --- factory/base.py | 8 +++++--- factory/compat.py | 8 ++++++++ factory/containers.py | 2 ++ factory/declarations.py | 21 +++++++++++---------- factory/utils.py | 15 +++++++++++---- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/factory/base.py b/factory/base.py index 282e3b10..88bee997 100644 --- a/factory/base.py +++ b/factory/base.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals + import collections import logging @@ -377,7 +379,7 @@ def _setup_counter(cls): 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) + logger.debug("%s: Setting up next sequence (%d)", cls, first_seq) @classmethod def _generate_next_sequence(cls): @@ -415,10 +417,10 @@ def attributes(cls, create=False, extra=None): if extra: force_sequence = extra.pop('__sequence', None) log_ctx = '%s.%s' % (cls.__module__, cls.__name__) - logger.debug('BaseFactory: Preparing %s.%s(extra=%r)', + logger.debug('BaseFactory: Preparing %s.%s(extra=%s)', cls.__module__, cls.__name__, - extra, + utils.log_repr(extra), ) return containers.AttributeBuilder(cls, extra, log_ctx=log_ctx).build( create=create, diff --git a/factory/compat.py b/factory/compat.py index 737d91a8..da8f48e0 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -35,12 +35,20 @@ def is_string(obj): from StringIO import StringIO as BytesIO + def force_text(str_or_unicode): + if isinstance(str_or_unicode, unicode): + return str_or_unicode + return str_or_unicode.decode('utf-8') + else: # pragma: no cover def is_string(obj): return isinstance(obj, str) from io import BytesIO + def force_text(text): + return text + try: # pragma: no cover # Python >= 3.2 diff --git a/factory/containers.py b/factory/containers.py index 49611154..0e2eb01f 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals + import logging logger = logging.getLogger(__name__) diff --git a/factory/declarations.py b/factory/declarations.py index 895f2ac3..f5dd149e 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -20,6 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from __future__ import unicode_literals import itertools import logging @@ -70,7 +71,7 @@ def __init__(self, function, *args, **kwargs): self.function = function def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyFunction: Evaluating %r on %r", self.function, obj) + logger.debug("LazyFunction: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(obj)) return self.function() @@ -87,7 +88,7 @@ def __init__(self, function, *args, **kwargs): self.function = function def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyAttribute: Evaluating %r on %r", self.function, obj) + logger.debug("LazyAttribute: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(obj)) return self.function(obj) @@ -153,7 +154,7 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): else: target = obj - logger.debug("SelfAttribute: Picking attribute %r on %r", self.attribute_name, target) + logger.debug("SelfAttribute: Picking attribute %r on %s", self.attribute_name, utils.log_repr(target)) return deepgetattr(target, self.attribute_name, self.default) def __repr__(self): @@ -190,7 +191,7 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): if self.iterator is None: self.iterator = self.iterator_builder() - logger.debug("Iterator: Fetching next value from %r", self.iterator) + logger.debug("Iterator: Fetching next value from %s", utils.log_repr(self.iterator)) value = next(iter(self.iterator)) if self.getter is None: return value @@ -232,8 +233,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=%s, obj=%r", - self.function, sequence, obj) + logger.debug("LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%s", + self.function, sequence, utils.log_repr(obj)) return self.function(obj, self.type(sequence)) @@ -588,9 +589,9 @@ def call(self, obj, create, extraction_context): if extraction_context.did_extract: # The user passed in a custom value - logger.debug('RelatedFactory: Using provided %r instead of ' + logger.debug('RelatedFactory: Using provided %s instead of ' 'generating %s.%s.', - extraction_context.value, + utils.log_repr(extraction_context.value), factory.__module__, factory.__name__, ) return extraction_context.value @@ -640,8 +641,8 @@ def call(self, obj, create, extraction_context): passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(extraction_context.extra) method = getattr(obj, self.method_name) - logger.debug('PostGenerationMethodCall: Calling %r.%s(%s)', - obj, + logger.debug('PostGenerationMethodCall: Calling %s.%s(%s)', + utils.log_repr(obj), self.method_name, utils.log_pprint(passed_args, passed_kwargs), ) diff --git a/factory/utils.py b/factory/utils.py index cfae4ecd..0ad7f545 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -24,6 +24,8 @@ import collections +from . import compat + #: String for splitting an attribute name into a #: (subfactory_name, subfactory_field) tuple. ATTR_SPLITTER = '__' @@ -104,10 +106,7 @@ def _safe_repr(obj): except Exception: return '' % id(obj) - try: # Convert to "text type" (= unicode) - return '%s' % obj_repr - except UnicodeError: # non-ascii bytes repr on Py2 - return obj_repr.decode('utf-8') + return log_repr(obj) class log_pprint(object): @@ -135,6 +134,14 @@ def __str__(self): ) +def log_repr(obj): + """Generate a text-compatible repr of an object. + + Some projects have a tendency to generate bytes-style repr in Python2. + """ + return compat.force_text(repr(obj)) + + class ResetableIterator(object): """An iterator wrapper that can be 'reset()' to its start.""" def __init__(self, iterator, **kwargs): From caac625927994fb9c558d59a1552481f05d92ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 May 2016 12:06:01 +0200 Subject: [PATCH 182/714] Py2: Fixes for type() and import() expecting bytes. --- factory/base.py | 4 +++- factory/utils.py | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/factory/base.py b/factory/base.py index 88bee997..4967b37b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -698,7 +698,9 @@ def simple_generate_batch(cls, create, size, **kwargs): return cls.generate_batch(strategy, size, **kwargs) -Factory = FactoryMetaClass('Factory', (BaseFactory,), { +# Note: we're calling str() on the class name to avoid issues with Py2's type() expecting bytes +# instead of unicode. +Factory = FactoryMetaClass(str('Factory'), (BaseFactory,), { 'Meta': BaseMeta, '__doc__': """Factory base with build and create support. diff --git a/factory/utils.py b/factory/utils.py index 0ad7f545..4a495c73 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -96,8 +96,9 @@ def import_object(module_name, attribute_name): >>> import_object('datetime', 'datetime') """ - module = __import__(module_name, {}, {}, [attribute_name], 0) - return getattr(module, attribute_name) + # Py2 compatibility: force str (i.e bytes) when importing. + module = __import__(str(module_name), {}, {}, [str(attribute_name)], 0) + return getattr(module, str(attribute_name)) def _safe_repr(obj): From b81b2d2f95ba5f3446404d8be0c942a412036baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 May 2016 14:03:14 +0200 Subject: [PATCH 183/714] Add tests for missing key in django_get_or_create. Closes #297. --- tests/djapp/models.py | 5 +++++ tests/test_django.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index cadefbc9..b7cad0d7 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -44,6 +44,11 @@ class NonIntegerPk(models.Model): bar = models.CharField(max_length=20, blank=True) +class MultifieldModel(models.Model): + slug = models.SlugField(max_length=20, unique=True) + text = models.CharField(max_length=20) + + class AbstractBase(models.Model): foo = models.CharField(max_length=20) diff --git a/tests/test_django.py b/tests/test_django.py index 08349b91..be69266e 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -118,6 +118,14 @@ class Meta: bar = '' + class MultifieldModelFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.MultifieldModel + django_get_or_create = ['slug'] + + text = factory.Faker('text') + + class AbstractBaseFactory(factory.django.DjangoModelFactory): class Meta: model = models.AbstractBase @@ -225,6 +233,29 @@ def test_pk_force_value(self): self.assertEqual(11, std2.pk) +@unittest.skipIf(django is None, "Django not installed.") +class DjangoGetOrCreateTests(django_test.TestCase): + def test_simple_call(self): + obj1 = MultifieldModelFactory(slug='slug1') + obj2 = MultifieldModelFactory(slug='slug1') + obj3 = MultifieldModelFactory(slug='alt') + + self.assertEqual(obj1, obj2) + self.assertEqual(2, models.MultifieldModel.objects.count()) + + def test_missing_arg(self): + with self.assertRaises(factory.FactoryError): + MultifieldModelFactory() + + def test_multicall(self): + objs = MultifieldModelFactory.create_batch(6, + slug=factory.Iterator(['main', 'alt']), + ) + self.assertEqual(6, len(objs)) + self.assertEqual(2, len(set(objs))) + self.assertEqual(2, models.MultifieldModel.objects.count()) + + @unittest.skipIf(django is None, "Django not installed.") class DjangoPkForceTestCase(django_test.TestCase): def setUp(self): @@ -784,6 +815,7 @@ def generate(cls): self.assertSignalsReactivated() + @unittest.skipIf(django is None, "Django not installed.") class DjangoCustomManagerTestCase(unittest.TestCase): @@ -800,5 +832,6 @@ class Meta: # invalid for the actual model. ObjFactory.create(arg='invalid') + if __name__ == '__main__': # pragma: no cover unittest.main() From b6b137162c7afd3e21aa66fb3d8949235f673123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 May 2016 16:05:37 +0200 Subject: [PATCH 184/714] [lint] Fix code quality, switch to flake8. --- .flake8 | 4 + .pylintrc | 240 ---------------------------------------- MANIFEST.in | 2 +- Makefile | 9 +- factory/__init__.py | 10 +- factory/alchemy.py | 2 + factory/base.py | 28 +++-- factory/compat.py | 9 +- factory/containers.py | 26 +++-- factory/declarations.py | 47 ++++---- factory/django.py | 23 ++-- factory/errors.py | 2 - factory/faker.py | 5 +- factory/fuzzy.py | 5 +- factory/helpers.py | 12 +- factory/mogo.py | 5 +- factory/mongoengine.py | 4 +- factory/utils.py | 5 +- requirements_test.txt | 2 +- tox.ini | 2 +- 20 files changed, 110 insertions(+), 332 deletions(-) create mode 100644 .flake8 delete mode 100644 .pylintrc diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..eceea153 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +# Ignore "and" at start of line. +ignore = W503 +max-line-length = 120 diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index e9f43c99..00000000 --- a/.pylintrc +++ /dev/null @@ -1,240 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -init-hook='import os, sys; sys.path.append(os.getcwd())' - -# Profiled execution. -profile=no - -# Add to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore= - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). -#disable=C0103,C0111,C0302,E1002,E1101,E1102,E1103,I0011,I0013,R0201,R0801,R0901,R0902,R0903,R0904,R0912,R0914,R0915,R0921,R0923,W0108,W0212,W0232,W0141,W0142,W0401,W0613,R0924 -disable=C0103,C0111,I0011,R0201,R0903,R0922,W0142,W0212,W0232,W0613 -# see http://www.logilab.org/card/pylintfeatures - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=text - -# Include message's id in output -include-ids=yes - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Add a comment according to your evaluation note. This is used by the global -# evaluation report (RP0004). -comment=no - - -[BASIC] - -# Required attributes for module, separated by a comma -required-attributes= - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,apply,input - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct instance attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=__.*__ - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=80 - -# Maximum number of lines in a module -max-module-lines=1200 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=SQLObject - -# When zope mode is activated, add a predefined set of Zope acquired attributes -# to generated-members. -zope=no - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. -generated-members=REQUEST,acl_users,aq_parent - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the beginning of the name of dummy variables -# (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -[CLASSES] - -# List of interface methods to ignore, separated by a comma. This is used for -# instance to not check methods defines in Zope's Interface base class. -ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=8 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branchs=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=10 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= diff --git a/MANIFEST.in b/MANIFEST.in index 19b0f8cd..786a034f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -9,4 +9,4 @@ prune tests global-exclude .py[cod] __pycache__ -exclude Makefile tox.ini .pylintrc +exclude Makefile tox.ini .flake8 diff --git a/Makefile b/Makefile index 8f48c6b4..011fee54 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ EXAMPLES_DIR=examples # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) +FLAKE8 = flake8 all: default @@ -33,9 +34,13 @@ test: example-test: $(MAKE) -C $(EXAMPLES_DIR) test + + +# Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude lint: check-manifest - pylint --rcfile=.pylintrc --report=no $(PACKAGE)/ + $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) + $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py coverage: $(COVERAGE) erase @@ -47,4 +52,4 @@ doc: $(MAKE) -C $(DOC_DIR) html -.PHONY: all default clean coverage doc install-deps pylint test +.PHONY: all default clean coverage doc install-deps lint test diff --git a/factory/__init__.py b/factory/__init__.py index 421e1abc..4b4563f0 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,10 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.7.0' -__author__ = 'Raphaël Barrois ' - - from .base import ( Factory, BaseDictFactory, @@ -92,6 +88,10 @@ from . import mogo from . import mongoengine + +__version__ = '2.7.0' +__author__ = 'Raphaël Barrois ' + + MogoFactory = mogo.MogoFactory DjangoModelFactory = django.DjangoModelFactory - diff --git a/factory/alchemy.py b/factory/alchemy.py index a9aab231..1ef13495 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -18,6 +18,7 @@ # 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 from . import base @@ -35,6 +36,7 @@ class SQLAlchemyModelFactory(base.Factory): """Factory for SQLAlchemy models. """ _options_class = SQLAlchemyOptions + class Meta: abstract = True diff --git a/factory/base.py b/factory/base.py index 4967b37b..cec38db2 100644 --- a/factory/base.py +++ b/factory/base.py @@ -38,9 +38,9 @@ STUB_STRATEGY = 'stub' - # Factory metaclasses + def get_factory_bases(bases): """Retrieve all FactoryMetaClass-derived bases from a list.""" return [b for b in bases if issubclass(b, BaseFactory)] @@ -106,7 +106,8 @@ def __new__(mcs, class_name, bases, attrs): new_class = super(FactoryMetaClass, mcs).__new__( mcs, class_name, bases, attrs) - meta.contribute_to_class(new_class, + meta.contribute_to_class( + new_class, meta=attrs_meta, base_meta=base_meta, base_factory=base_factory, @@ -176,7 +177,8 @@ def _fill_from_meta(self, meta, base_meta): if meta is None: meta_attrs = {} else: - meta_attrs = dict((k, v) + meta_attrs = dict( + (k, v) for (k, v) in vars(meta).items() if not k.startswith('_') ) @@ -189,11 +191,11 @@ def _fill_from_meta(self, meta, base_meta): if meta_attrs: # Some attributes in the Meta aren't allowed here - raise TypeError("'class Meta' for %r got unknown attribute(s) %s" - % (self.factory, ','.join(sorted(meta_attrs.keys())))) + raise TypeError( + "'class Meta' for %r got unknown attribute(s) %s" + % (self.factory, ','.join(sorted(meta_attrs.keys())))) - def contribute_to_class(self, factory, - meta=None, base_meta=None, base_factory=None, params=None): + def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=None, params=None): self.factory = factory self.base_factory = base_factory @@ -279,8 +281,8 @@ def _compute_parameter_dependencies(self, parameters): cyclic = [name for name, field_deps in deep_revdeps.items() if name in field_deps] if cyclic: raise errors.CyclicDefinitionError( - "Cyclic definition detected on %s' Params around %s" - % (self.factory, ', '.join(cyclic))) + "Cyclic definition detected on %s' Params around %s" + % (self.factory, ', '.join(cyclic))) return deps def __str__(self): @@ -417,7 +419,8 @@ def attributes(cls, create=False, extra=None): if extra: force_sequence = extra.pop('__sequence', None) log_ctx = '%s.%s' % (cls.__module__, cls.__name__) - logger.debug('BaseFactory: Preparing %s.%s(extra=%s)', + logger.debug( + "BaseFactory: Preparing %s.%s(extra=%s)", cls.__module__, cls.__name__, utils.log_repr(extra), @@ -487,7 +490,8 @@ def _prepare(cls, create, **kwargs): # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls._meta.inline_args) - logger.debug('BaseFactory: Generating %s.%s(%s)', + logger.debug( + "BaseFactory: Generating %s.%s(%s)", cls.__module__, cls.__name__, utils.log_pprint(args, kwargs), @@ -711,7 +715,7 @@ def simple_generate_batch(cls, create, size, **kwargs): # Backwards compatibility -Factory.AssociatedClassError = errors.AssociatedClassError # pylint: disable=W0201 +Factory.AssociatedClassError = errors.AssociatedClassError class StubFactory(Factory): diff --git a/factory/compat.py b/factory/compat.py index da8f48e0..77f967fe 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -24,19 +24,18 @@ """Compatibility tools""" import datetime -import decimal import sys PY2 = (sys.version_info[0] == 2) if PY2: # pragma: no cover def is_string(obj): - return isinstance(obj, (str, unicode)) + return isinstance(obj, (str, unicode)) # noqa - from StringIO import StringIO as BytesIO + from StringIO import StringIO as BytesIO # noqa def force_text(str_or_unicode): - if isinstance(str_or_unicode, unicode): + if isinstance(str_or_unicode, unicode): # noqa return str_or_unicode return str_or_unicode.decode('utf-8') @@ -44,7 +43,7 @@ def force_text(str_or_unicode): def is_string(obj): return isinstance(obj, str) - from io import BytesIO + from io import BytesIO # noqa def force_text(text): return text diff --git a/factory/containers.py b/factory/containers.py index 0e2eb01f..ef749ba4 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -24,12 +24,12 @@ import logging -logger = logging.getLogger(__name__) - from . import declarations from . import errors from . import utils +logger = logging.getLogger(__name__) + class LazyStub(object): """A generic container that only allows getting attributes. @@ -75,12 +75,15 @@ def __fill__(self): dict: map of attribute name => computed value """ res = {} - logger.debug("LazyStub: Computing values for %s(%s)", + 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)", + + logger.debug( + "LazyStub: Computed values, got %s(%s)", self.__log_ctx, utils.log_pprint(kwargs=res), ) return res @@ -217,10 +220,11 @@ def evaluate(self, obj, containers=()): containers (object list): the chain of containers of the object being built, its immediate holder being first. """ - return self.declaration.evaluate(self.sequence, obj, - create=self.create, - extra=self.extra, - containers=containers, + return self.declaration.evaluate( + self.sequence, obj, + create=self.create, + extra=self.extra, + containers=containers, ) def __repr__(self): @@ -306,14 +310,16 @@ def build(self, create, force_sequence=None): wrapped_attrs = {} for k, v in self._declarations.items(): if isinstance(v, declarations.OrderedDeclaration): - v = DeclarationWrapper(v, + v = DeclarationWrapper( + v, sequence=sequence, create=create, extra=self._subfields.get(k, {}), ) wrapped_attrs[k] = v - stub = LazyStub(wrapped_attrs, containers=self._containers, + stub = LazyStub( + wrapped_attrs, containers=self._containers, model_class=self.factory, log_ctx=self._log_ctx) return stub.__fill__() diff --git a/factory/declarations.py b/factory/declarations.py index f5dd149e..5f9bc38c 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -140,7 +140,7 @@ class SelfAttribute(OrderedDeclaration): def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs): super(SelfAttribute, self).__init__(*args, **kwargs) - depth = len(attribute_name) - len(attribute_name.lstrip('.')) + depth = len(attribute_name) - len(attribute_name.lstrip('.')) attribute_name = attribute_name[depth:] self.depth = depth @@ -213,7 +213,7 @@ class Sequence(OrderedDeclaration): type (function): A function converting an integer into the expected kind of counter for the 'function' attribute. """ - def __init__(self, function, type=int): # pylint: disable=W0622 + def __init__(self, function, type=int): super(Sequence, self).__init__() self.function = function self.type = type @@ -233,8 +233,9 @@ 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=%s, obj=%s", - self.function, sequence, utils.log_repr(obj)) + logger.debug( + "LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%s", + self.function, sequence, utils.log_repr(obj)) return self.function(obj, self.type(sequence)) @@ -353,9 +354,9 @@ def __init__(self, factory_or_path): else: if not (compat.is_string(factory_or_path) and '.' in factory_or_path): raise ValueError( - "A factory= argument must receive either a class " - "or the fully qualified path to a Factory subclass; got " - "%r instead." % factory_or_path) + "A factory= argument must receive either a class " + "or the fully qualified path to a Factory subclass; got " + "%r instead." % factory_or_path) self.module, self.name = factory_or_path.rsplit('.', 1) def get(self): @@ -402,7 +403,8 @@ def generate(self, sequence, obj, create, params): override the wrapped factory's defaults """ subfactory = self.get_factory() - logger.debug("SubFactory: Instantiating %s.%s(%s), create=%r", + logger.debug( + "SubFactory: Instantiating %s.%s(%s), create=%r", subfactory.__module__, subfactory.__name__, utils.log_pprint(kwargs=params), create, @@ -419,7 +421,8 @@ def __init__(self, params, dict_factory='factory.DictFactory'): 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, + return dict_factory.simple_generate( + create, __sequence=sequence, **params) @@ -433,10 +436,12 @@ def __init__(self, params, list_factory='factory.ListFactory'): def generate(self, sequence, obj, create, params): list_factory = self.get_factory() - logger.debug('List: Building list(%s)', + logger.debug( + "List: Building list(%s)", utils.log_pprint(args=[v for _i, v in sorted(params.items())]), ) - return list_factory.simple_generate(create, + return list_factory.simple_generate( + create, __sequence=sequence, **params) @@ -551,7 +556,8 @@ def __init__(self, function): self.function = function def call(self, obj, create, extraction_context): - logger.debug('PostGeneration: Calling %s.%s(%s)', + logger.debug( + "PostGeneration: Calling %s.%s(%s)", self.function.__module__, self.function.__name__, utils.log_pprint( @@ -559,7 +565,8 @@ def call(self, obj, create, extraction_context): extraction_context.extra, ), ) - return self.function(obj, create, + return self.function( + obj, create, extraction_context.value, **extraction_context.extra) @@ -589,10 +596,10 @@ def call(self, obj, create, extraction_context): if extraction_context.did_extract: # The user passed in a custom value - logger.debug('RelatedFactory: Using provided %s instead of ' - 'generating %s.%s.', - utils.log_repr(extraction_context.value), - factory.__module__, factory.__name__, + logger.debug( + "RelatedFactory: Using provided %s instead of generating %s.%s.", + utils.log_repr(extraction_context.value), + factory.__module__, factory.__name__, ) return extraction_context.value @@ -601,7 +608,8 @@ def call(self, obj, create, extraction_context): if self.name: passed_kwargs[self.name] = obj - logger.debug('RelatedFactory: Generating %s.%s(%s)', + logger.debug( + "RelatedFactory: Generating %s.%s(%s)", factory.__module__, factory.__name__, utils.log_pprint((create,), passed_kwargs), @@ -641,7 +649,8 @@ def call(self, obj, create, extraction_context): passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(extraction_context.extra) method = getattr(obj, self.method_name) - logger.debug('PostGenerationMethodCall: Calling %s.%s(%s)', + logger.debug( + "PostGenerationMethodCall: Calling %s.%s(%s)", utils.log_repr(obj), self.method_name, utils.log_pprint(passed_args, passed_kwargs), diff --git a/factory/django.py b/factory/django.py index 43434c22..caa64a65 100644 --- a/factory/django.py +++ b/factory/django.py @@ -21,18 +21,15 @@ # THE SOFTWARE. +"""factory_boy extensions for use with the Django framework.""" + from __future__ import absolute_import from __future__ import unicode_literals import os -import types import logging import functools -from . import errors - -"""factory_boy extensions for use with the Django framework.""" - try: import django from django.core import files as django_files @@ -44,6 +41,7 @@ from . import base from . import declarations +from . import errors from .compat import BytesIO, is_string logger = logging.getLogger('factory.generate') @@ -60,6 +58,7 @@ def require_django(): _LAZY_LOADS = {} + def get_model(app, model): """Wrapper around django's get_model.""" if 'get_model' not in _LAZY_LOADS: @@ -76,16 +75,16 @@ def _lazy_load_get_model(): the settings haven't been configured yet. """ if django is None: - def get_model(app, model): + def _get_model(app, model): raise import_failure elif django.VERSION[:2] < (1, 7): - from django.db.models.loading import get_model + from django.db.models.loading import get_model as _get_model else: from django import apps as django_apps - get_model = django_apps.apps.get_model - _LAZY_LOADS['get_model'] = get_model + _get_model = django_apps.apps.get_model + _LAZY_LOADS['get_model'] = _get_model class DjangoOptions(base.FactoryOptions): @@ -118,6 +117,7 @@ class DjangoModelFactory(base.Factory): """ _options_class = DjangoOptions + class Meta: abstract = True # Optional, but explicit. @@ -133,8 +133,8 @@ def _load_model_class(cls, definition): @classmethod def _get_manager(cls, model_class): if model_class is None: - raise base.AssociatedClassError("No model set on %s.%s.Meta" - % (cls.__module__, cls.__name__)) + raise errors.AssociatedClassError( + "No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) try: manager = model_class.objects @@ -338,4 +338,3 @@ def wrapper(*args, **kwargs): with self.copy(): return callable_obj(*args, **kwargs) return wrapper - diff --git a/factory/errors.py b/factory/errors.py index 79d85f4e..cd4bed8b 100644 --- a/factory/errors.py +++ b/factory/errors.py @@ -38,5 +38,3 @@ class UnsupportedStrategy(FactoryError): class CyclicDefinitionError(FactoryError): """Raised when a cyclical declaration occurs.""" - - diff --git a/factory/faker.py b/factory/faker.py index 5411985d..5b8ff7db 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -43,6 +43,7 @@ class Meta: from . import declarations + class Faker(declarations.OrderedDeclaration): """Wrapper for 'faker' values. @@ -66,8 +67,8 @@ def generate(self, extra_kwargs): kwargs = {} kwargs.update(self.provider_kwargs) kwargs.update(extra_kwargs) - faker = self._get_faker(self.locale) - return faker.format(self.provider, **kwargs) + subfaker = self._get_faker(self.locale) + return subfaker.format(self.provider, **kwargs) def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.generate(extra or {}) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 71d18847..99df591f 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -99,8 +99,7 @@ class FuzzyText(BaseFuzzyAttribute): not important. """ - def __init__(self, prefix='', length=12, suffix='', - chars=string.ascii_letters, **kwargs): + def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters, **kwargs): super(FuzzyText, self).__init__(**kwargs) self.prefix = prefix self.suffix = suffix @@ -114,7 +113,7 @@ def fuzz(self): class FuzzyChoice(BaseFuzzyAttribute): """Handles fuzzy choice of an attribute. - + Args: choices (iterable): An iterable yielding options; will only be unrolled on the first call. diff --git a/factory/helpers.py b/factory/helpers.py index 60a4d75c..6a0462a2 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -49,13 +49,14 @@ def debug(logger='factory', stream=None): def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ + class Meta: model = klass + kwargs['Meta'] = Meta base_class = kwargs.pop('FACTORY_CLASS', base.Factory) - factory_class = type(base.Factory).__new__( - type(base.Factory), factory_name, (base_class,), kwargs) + factory_class = type(base.Factory).__new__(type(base.Factory), factory_name, (base_class,), kwargs) factory_class.__name__ = '%sFactory' % klass.__name__ factory_class.__doc__ = 'Auto-generated factory for class %s' % klass return factory_class @@ -101,10 +102,6 @@ def generate_batch(klass, strategy, size, **kwargs): return make_factory(klass, **kwargs).generate_batch(strategy, size) -# We're reusing 'create' as a keyword. -# pylint: disable=W0621 - - def simple_generate(klass, create, **kwargs): """Create a factory for the given class, and simple_generate an instance.""" return make_factory(klass, **kwargs).simple_generate(create) @@ -115,9 +112,6 @@ def simple_generate_batch(klass, create, size, **kwargs): return make_factory(klass, **kwargs).simple_generate_batch(create, size) -# pylint: enable=W0621 - - def lazy_attribute(func): return declarations.LazyAttribute(func) diff --git a/factory/mogo.py b/factory/mogo.py index aa9f28b0..284dc556 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -21,12 +21,11 @@ # THE SOFTWARE. -from __future__ import unicode_literals - - """factory_boy extensions for use with the mogo library (pymongo wrapper).""" +from __future__ import unicode_literals + from . import base diff --git a/factory/mongoengine.py b/factory/mongoengine.py index f50b727e..c6559ebc 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -21,10 +21,10 @@ # THE SOFTWARE. -from __future__ import unicode_literals +"""factory_boy extensions for use with the mongoengine library (pymongo wrapper).""" -"""factory_boy extensions for use with the mongoengine library (pymongo wrapper).""" +from __future__ import unicode_literals from . import base diff --git a/factory/utils.py b/factory/utils.py index 4a495c73..26363512 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -30,6 +30,7 @@ #: (subfactory_name, subfactory_field) tuple. ATTR_SPLITTER = '__' + def extract_dict(prefix, kwargs, pop=True, exclude=()): """Extracts all values beginning with a given prefix from a dict. @@ -103,12 +104,10 @@ def import_object(module_name, attribute_name): def _safe_repr(obj): try: - obj_repr = repr(obj) + return log_repr(obj) except Exception: return '' % id(obj) - return log_repr(obj) - class log_pprint(object): """Helper for properly printing args / kwargs passed to an object. diff --git a/requirements_test.txt b/requirements_test.txt index e37a8fea..41fb52dc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,2 +1,2 @@ mock -pylint +flake8 diff --git a/tox.ini b/tox.ini index 4ccade64..7e653e93 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = make example-test [testenv:lint] deps = - pylint + flake8 check_manifest whitelist_externals = make From b84b22a9ce5a74235576b5d9aca7b11b8122b0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 21 May 2016 14:32:10 +0200 Subject: [PATCH 185/714] Reconfigure travis to use tox environments. We still have some duplication between tox.ini and .travis.yml; we'll have to decide whether we want to run the full matrix on travis. --- .travis.yml | 32 ++++++++++++++++++++++++-------- tox.ini | 4 +--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 938787b3..2ae6bf20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,33 @@ sudo: false language: python -python: - - "2.7" - - "3.4" - - "3.5" - - "pypy" - script: - - SKIP_MONGOENGINE=1 python setup.py test + - tox install: - - make install-deps + - pip install tox + +matrix: + include: + - python: "2.7" + env: TOXENV=py27-django19-alchemy10-mongoengine010 + - python: "3.4" + env: TOXENV=py34-django19-alchemy10-mongoengine010 + - python: "3.5" + env: TOXENV=py35-django19-alchemy10-mongoengine010 + + # Pypy + - python: "pypy" + env: TOXENV=py27-django19-alchemy10-mongoengine010 + + # Linting + - python: "3.5" + env: TOXENV=examples + - python: "3.5" + env: TOXENV=lint + +services: + - mongodb notifications: email: false diff --git a/tox.ini b/tox.ini index 7e653e93..24e7fb83 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,6 @@ [tox] envlist = - py{27,34}-django{17,18,19} - py{27,34}-alchemy10 - py{27,34}-mongoengine010 + py{27,34,35}-django{17,18,19}-alchemy10-mongoengine010 examples lint From 2635a7e54694aeffa68ea1b573015a6ffd2f8cec Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Wed, 15 Jun 2016 22:48:09 +0100 Subject: [PATCH 186/714] Convert readthedocs links for their .org -> .io migration for hosted projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0e1f0007..bd52a940 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ factory_boy :target: http://travis-ci.org/rbarrois/factory_boy/ .. image:: https://img.shields.io/pypi/v/factory_boy.svg - :target: http://factoryboy.readthedocs.org/en/latest/changelog.html + :target: https://factoryboy.readthedocs.io/en/latest/changelog.html :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg @@ -75,7 +75,7 @@ Its main features include: Links ----- -* Documentation: http://factoryboy.readthedocs.org/ +* Documentation: https://factoryboy.readthedocs.io/ * Repository: https://github.com/rbarrois/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy From 5bfe871d1685d27c0adce1276df6ca8740d93b7a Mon Sep 17 00:00:00 2001 From: Oleg Pidsadnyi Date: Wed, 29 Jun 2016 15:52:00 +0200 Subject: [PATCH 187/714] Preserve postgeneration declarations order review comments --- docs/changelog.rst | 11 +++++++++++ docs/reference.rst | 3 +++ factory/__init__.py | 2 +- factory/base.py | 14 +++++++++++--- factory/declarations.py | 7 +++++++ tests/test_declarations.py | 38 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c2068c0e..86033a28 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ ChangeLog ========= +.. _v2.7.1: + +2.7.1 (2016-07-10) +------------------ + +*New:* + + - :issue:`240`: Call post-generation declarations in the order they were declared, + thanks to `Oleg Pidsadnyi `_. + + .. _v2.7.0: 2.7.0 (2016-04-19) diff --git a/docs/reference.rst b/docs/reference.rst index 08678d95..13d70dec 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1497,6 +1497,9 @@ To support this pattern, factory_boy provides the following tools: - :func:`post_generation`: decorator performing the same functions as :class:`PostGeneration` - :class:`RelatedFactory`: this builds or creates a given factory *after* building/creating the first Factory. +Post-generation hooks are called in the same order they are declared in the factory class, so that +functions can rely on the side effects applied by the previous post-generation hook. + Extracting parameters """"""""""""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index 4b4563f0..4dd75f70 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -89,7 +89,7 @@ from . import mongoengine -__version__ = '2.7.0' +__version__ = '2.7.1' __author__ = 'Raphaël Barrois ' diff --git a/factory/base.py b/factory/base.py index cec38db2..f4d5abcf 100644 --- a/factory/base.py +++ b/factory/base.py @@ -157,6 +157,14 @@ def __init__(self): self.parameters = {} self.parameters_dependencies = {} + @property + def sorted_postgen_declarations(self): + """Get sorted postgen declaration items.""" + return sorted( + self.postgen_declarations.items(), + key=lambda item: item[1].creation_counter, + ) + def _build_default_options(self): """"Provide the default value for all allowed fields. @@ -516,9 +524,9 @@ def _generate(cls, create, attrs): "is either not set or False." % dict(f=cls.__name__)) # Extract declarations used for post-generation - postgen_declarations = cls._meta.postgen_declarations postgen_attributes = {} - for name, decl in sorted(postgen_declarations.items()): + + for name, decl in cls._meta.sorted_postgen_declarations: postgen_attributes[name] = decl.extract(name, attrs) # Generate the object @@ -526,7 +534,7 @@ def _generate(cls, create, attrs): # Handle post-generation attributes results = {} - for name, decl in sorted(postgen_declarations.items()): + for name, decl in cls._meta.sorted_postgen_declarations: extraction_context = postgen_attributes[name] results[name] = decl.call(obj, create, extraction_context) diff --git a/factory/declarations.py b/factory/declarations.py index 5f9bc38c..92059757 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -514,6 +514,13 @@ def __repr__(self): class PostGenerationDeclaration(object): """Declarations to be called once the model object has been generated.""" + creation_counter = 0 + """Global creation counter of the declaration.""" + + def __init__(self, *args, **kwargs): + self.creation_counter = PostGenerationDeclaration.creation_counter + PostGenerationDeclaration.creation_counter += 1 + def extract(self, name, attrs): """Extract relevant attributes from a dict. diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 2601a388..cd8c6bd0 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -23,6 +23,7 @@ import datetime import itertools +from factory import base from factory import declarations from factory import helpers @@ -280,6 +281,43 @@ def test_multi_call_with_kwargs(self): self.obj.method.assert_called_once_with('arg1', 'arg2', x=2) +class PostGenerationOrdering(unittest.TestCase): + + def test_post_generation_declaration_order(self): + postgen_results = [] + + class Related(base.Factory): + class Meta: + model = mock.MagicMock() + + class Ordered(base.Factory): + class Meta: + model = mock.MagicMock() + + a = declarations.RelatedFactory(Related) + z = declarations.RelatedFactory(Related) + + @helpers.post_generation + def a1(*args, **kwargs): + postgen_results.append('a1') + + @helpers.post_generation + def zz(*args, **kwargs): + postgen_results.append('zz') + + @helpers.post_generation + def aa(*args, **kwargs): + postgen_results.append('aa') + + postgen_names = [ + k for k, v in Ordered._meta.sorted_postgen_declarations + ] + self.assertEqual(postgen_names, ['a', 'z', 'a1', 'zz', 'aa']) + + # Test generation happens in desired order + Ordered() + self.assertEqual(postgen_results, ['a1', 'zz', 'aa']) + if __name__ == '__main__': # pragma: no cover unittest.main() From 049ec642cd1910def67bc838e8a0c6c6da93827b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 6 Aug 2016 12:47:16 +0200 Subject: [PATCH 188/714] Add `DefaultOption.checker`: validate Meta values This allows custom factories to define custom validators, ensuring that provided options are valid. --- factory/base.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/factory/base.py b/factory/base.py index f4d5abcf..212f9d66 100644 --- a/factory/base.py +++ b/factory/base.py @@ -129,10 +129,21 @@ class BaseMeta: class OptionDefault(object): - def __init__(self, name, value, inherit=False): + """The default for an option. + + Attributes: + name: str, the name of the option ('class Meta' attribute) + value: object, the default value for the option + inherit: bool, whether to inherit the value from the parent factory's `class Meta` + when no value is provided + checker: callable or None, an optional function used to detect invalid option + values at declaration time + """ + def __init__(self, name, value, inherit=False, checker=None): self.name = name self.value = value self.inherit = inherit + self.checker = checker def apply(self, meta, base_meta): value = self.value @@ -140,6 +151,10 @@ def apply(self, meta, base_meta): value = getattr(base_meta, self.name, value) if meta is not None: value = getattr(meta, self.name, value) + + if self.checker is not None: + self.checker(meta, value) + return value def __str__(self): From 3e28a95ea8df72a94a19ee952f3fbe741cf646ca Mon Sep 17 00:00:00 2001 From: Rob Zyskowski Date: Mon, 27 Jun 2016 14:53:17 +0200 Subject: [PATCH 189/714] Improve sqlalchemy session actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove force_flush OptionDefault and replace it with sqlalchemy_session_persistence, accepting values 'flush' and 'commit'. After a call to ``MyFactory.create()``: - With ``None``, do nothing - With ``'flush'``, perform a SQLAlchemy session flush - With ``'commit'``, perform a session flush and commit. - Other values will raise an error. This also deprecates the ``force_flush`` parameter, which will be removed in a future version. Closes #309, #310. Signed-off-by: Raphaël Barrois --- factory/alchemy.py | 52 ++++++++++++++++++++- tests/test_alchemy.py | 105 +++++++++++++++++++++++++++++++----------- 2 files changed, 127 insertions(+), 30 deletions(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index 1ef13495..f1c8ce61 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -22,13 +22,55 @@ from __future__ import unicode_literals from . import base +import warnings + +SESSION_PERSISTENCE_COMMIT = 'commit' +SESSION_PERSISTENCE_FLUSH = 'flush' +VALID_SESSION_PERSISTENCE_TYPES = [ + None, + SESSION_PERSISTENCE_COMMIT, + SESSION_PERSISTENCE_FLUSH, +] class SQLAlchemyOptions(base.FactoryOptions): + def _check_sqlalchemy_session_persistence(self, meta, value): + if value not in VALID_SESSION_PERSISTENCE_TYPES: + raise TypeError( + "%s.sqlalchemy_session_persistence must be one of %s, got %r" % + (meta, VALID_SESSION_PERSISTENCE_TYPES, value) + ) + + def _check_force_flush(self, meta, value): + if value: + warnings.warn( + "%(meta)s.force_flush has been deprecated as of 2.8.0 and will be removed in 3.0.0. " + "Please set ``%(meta)s.sqlalchemy_session_persistence = 'flush'`` instead." + % dict(meta=meta), + DeprecationWarning, + # Stacklevel: + # declaration -> FactoryMetaClass.__new__ -> meta.contribute_to_class + # -> meta._fill_from_meta -> option.apply -> option.checker + stacklevel=6, + ) + def _build_default_options(self): return super(SQLAlchemyOptions, self)._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), - base.OptionDefault('force_flush', False, inherit=True), + base.OptionDefault( + 'sqlalchemy_session_persistence', + None, + inherit=True, + checker=self._check_sqlalchemy_session_persistence, + ), + + # DEPRECATED as of 2.8.0, remove in 3.0.0 + base.OptionDefault( + 'force_flush', + False, + inherit=True, + checker=self._check_force_flush, + ), ] @@ -44,8 +86,14 @@ class Meta: def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" session = cls._meta.sqlalchemy_session + session_persistence = cls._meta.sqlalchemy_session_persistence + if cls._meta.force_flush: + session_persistence = SESSION_PERSISTENCE_FLUSH + obj = model_class(*args, **kwargs) session.add(obj) - if cls._meta.force_flush: + if session_persistence == SESSION_PERSISTENCE_FLUSH: session.flush() + elif session_persistence == SESSION_PERSISTENCE_COMMIT: + session.commit() return obj diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 5d8f275a..62b2e517 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -23,7 +23,8 @@ import factory from .compat import unittest -import mock +from .compat import mock +import warnings try: @@ -56,16 +57,6 @@ class Meta: foo = factory.Sequence(lambda n: 'foo%d' % n) -class ForceFlushingStandardFactory(SQLAlchemyModelFactory): - class Meta: - model = models.StandardModel - sqlalchemy_session = mock.MagicMock() - force_flush = True - - id = factory.Sequence(lambda n: n) - foo = factory.Sequence(lambda n: 'foo%d' % n) - - class NonIntegerPkFactory(SQLAlchemyModelFactory): class Meta: model = models.NonIntegerPk @@ -114,24 +105,82 @@ def test_pk_force_value(self): @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") -class SQLAlchemyForceFlushTestCase(unittest.TestCase): +class SQLAlchemySessionPersistenceTestCase(unittest.TestCase): def setUp(self): - super(SQLAlchemyForceFlushTestCase, self).setUp() - ForceFlushingStandardFactory.reset_sequence(1) - ForceFlushingStandardFactory._meta.sqlalchemy_session.rollback() - ForceFlushingStandardFactory._meta.sqlalchemy_session.reset_mock() - - def test_force_flush_called(self): - self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) - ForceFlushingStandardFactory.create() - self.assertTrue(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) - - def test_force_flush_not_called(self): - ForceFlushingStandardFactory._meta.force_flush = False - self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) - ForceFlushingStandardFactory.create() - self.assertFalse(ForceFlushingStandardFactory._meta.sqlalchemy_session.flush.called) - ForceFlushingStandardFactory._meta.force_flush = True + super(SQLAlchemySessionPersistenceTestCase, self).setUp() + self.mock_session = mock.NonCallableMagicMock(spec=models.session) + + def test_flushing(self): + class FlushingPersistenceFactory(StandardFactory): + class Meta: + sqlalchemy_session = self.mock_session + sqlalchemy_session_persistence = 'flush' + + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_not_called() + + FlushingPersistenceFactory.create() + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_called_once_with() + + def test_committing(self): + class CommittingPersistenceFactory(StandardFactory): + class Meta: + sqlalchemy_session = self.mock_session + sqlalchemy_session_persistence = 'commit' + + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_not_called() + + CommittingPersistenceFactory.create() + self.mock_session.commit.assert_called_once_with() + self.mock_session.flush.assert_not_called() + + def test_noflush_nocommit(self): + class InactivePersistenceFactory(StandardFactory): + class Meta: + sqlalchemy_session = self.mock_session + sqlalchemy_session_persistence = None + + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_not_called() + + InactivePersistenceFactory.create() + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_not_called() + + + def test_type_error(self): + with self.assertRaises(TypeError): + class BadPersistenceFactory(StandardFactory): + class Meta: + sqlalchemy_session_persistence = 'invalid_persistence_option' + model = models.StandardModel + + def test_force_flush_deprecation(self): + with warnings.catch_warnings(record=True) as warning_list: + class OutdatedPersistenceFactory(StandardFactory): + class Meta: + force_flush = True + sqlalchemy_session = self.mock_session + + # There should be *1* DeprecationWarning + self.assertEqual(len(warning_list), 1) + warning = warning_list[0] + self.assertTrue(issubclass(warning.category, DeprecationWarning)) + + # The warning text should point to the class declaration. + text = warnings.formatwarning(warning.message, warning.category, warning.filename, warning.lineno) + self.assertIn('test_alchemy.py', text) + self.assertIn('class OutdatedPersistenceFactory', text) + + # However, we shall keep the old-style behavior. + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_not_called() + + OutdatedPersistenceFactory.create() + self.mock_session.commit.assert_not_called() + self.mock_session.flush.assert_called_once_with() @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") From cff7fb6344a036dc68d4a3813bdb2fe78a39e3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 13 Aug 2016 19:54:24 +0200 Subject: [PATCH 190/714] Move code to the FactoryBoy github organization. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks: - Update links - Update travis-ci - Update PyPI information Signed-off-by: Raphaël Barrois --- README.rst | 12 ++++++------ docs/changelog.rst | 2 +- docs/conf.py | 2 +- docs/fuzzy.rst | 2 +- setup.py | 2 +- tests/test_django.py | 4 ++-- tests/test_using.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.rst b/README.rst index bd52a940..4926559a 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ factory_boy =========== -.. image:: https://secure.travis-ci.org/rbarrois/factory_boy.png?branch=master - :target: http://travis-ci.org/rbarrois/factory_boy/ +.. image:: https://secure.travis-ci.org/FactoryBoy/factory_boy.png?branch=master + :target: http://travis-ci.org/FactoryBoy/factory_boy/ .. image:: https://img.shields.io/pypi/v/factory_boy.svg :target: https://factoryboy.readthedocs.io/en/latest/changelog.html @@ -76,7 +76,7 @@ Links ----- * Documentation: https://factoryboy.readthedocs.io/ -* Repository: https://github.com/rbarrois/factory_boy +* Repository: https://github.com/FactoryBoy/factory_boy * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy @@ -92,11 +92,11 @@ PyPI: https://pypi.python.org/pypi/factory_boy/ $ pip install factory_boy -Source: https://github.com/rbarrois/factory_boy/ +Source: https://github.com/FactoryBoy/factory_boy/ .. code-block:: sh - $ git clone git://github.com/rbarrois/factory_boy/ + $ git clone git://github.com/FactoryBoy/factory_boy/ $ python setup.py install @@ -342,7 +342,7 @@ Contributing factory_boy is distributed under the MIT License. -Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. +Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. Questions and suggestions are welcome on the `mailing-list `_. All pull request should pass the test suite, which can be launched simply with: diff --git a/docs/changelog.rst b/docs/changelog.rst index 86033a28..88062cc2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -103,7 +103,7 @@ ChangeLog - Remove deprecated features from :ref:`v2.4.0` - Remove the auto-magical sequence setup (based on the latest primary key value in the database) for Django and SQLAlchemy; - this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. See https://github.com/rbarrois/factory_boy/commit/13d310f for technical details. + this relates to issues :issue:`170`, :issue:`153`, :issue:`111`, :issue:`103`, :issue:`92`, :issue:`78`. See https://github.com/FactoryBoy/factory_boy/commit/13d310f for technical details. .. warning:: Version 2.5.0 removes the 'auto-magical sequence setup' bug-and-feature. This could trigger some bugs when tests expected a non-zero sequence reference. diff --git a/docs/conf.py b/docs/conf.py index d5b86f44..dcf2064c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -34,7 +34,7 @@ ] extlinks = { - 'issue': ('https://github.com/rbarrois/factory_boy/issues/%s', 'issue #'), + 'issue': ('https://github.com/FactoryBoy/factory_boy/issues/%s', 'issue #'), } # Add any paths that contain templates here, relative to this directory. diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 5b03ec64..eb0aa459 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -7,7 +7,7 @@ Fuzzy attributes these built-in fuzzers are deprecated in favor of their `Faker `_ equivalents. Further discussion here: - ``_ + ``_ Some tests may be interested in testing with fuzzy, random values. diff --git a/setup.py b/setup.py index 3a0e6990..3c2a76fe 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def get_version(package_name): author_email='mark@deliciouslynerdy.com', maintainer='Raphaël Barrois', maintainer_email='raphael.barrois+fboy@polytechnique.org', - url='https://github.com/rbarrois/factory_boy', + url='https://github.com/FactoryBoy/factory_boy', keywords=['factory_boy', 'factory', 'fixtures'], packages=['factory'], license='MIT', diff --git a/tests/test_django.py b/tests/test_django.py index be69266e..aca3eb25 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -304,7 +304,7 @@ class Meta: def test_inherited_loading(self): """Proper loading of a model within 'child' factories. - See https://github.com/rbarrois/factory_boy/issues/109. + See https://github.com/FactoryBoy/factory_boy/issues/109. """ class ExampleFactory(factory.DjangoModelFactory): class Meta: @@ -319,7 +319,7 @@ class Example2Factory(ExampleFactory): def test_inherited_loading_and_sequence(self): """Proper loading of a model within 'child' factories. - See https://github.com/rbarrois/factory_boy/issues/109. + See https://github.com/FactoryBoy/factory_boy/issues/109. """ class ExampleFactory(factory.DjangoModelFactory): class Meta: diff --git a/tests/test_using.py b/tests/test_using.py index 16a692e0..0f7f3040 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -871,7 +871,7 @@ class Meta: 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 + See issue https://github.com/FactoryBoy/factory_boy/issues/93 Problem: sequence counter is somewhat shared between factories until the "slave" factory has been called. From c4cc7be5a7c3388705e962bbe0ff2fc79ae0b449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 13 Aug 2016 20:46:02 +0200 Subject: [PATCH 191/714] Add minimal, naive logo. Designed with Inkscape. Based on the official Python logo, fits within the https://www.python.org/psf/trademarks/#derived-logos PSF trademark policy. --- docs/logo.png | Bin 0 -> 21638 bytes docs/logo.svg | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 docs/logo.png create mode 100644 docs/logo.svg diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ff94a1a683736a8011680042ea0b6ff142f15e47 GIT binary patch literal 21638 zcmXtg2UJr{v~@y)iHHV}j)u^rHxW<>5Rl%b3J54l?;yPe1PQ%Jm)<)_??ie>0V&cu z(t9s2-~0cUwI*4~y7$i9Ip?0U&z?OAQdO3DNI*>h0017!$x5o>KHL6l(0jOdn#HHQ zxDT+Sgq#Kx_ws~(!r;E&x0BUz#2r8JUjs_8e0RnDNcCCj?Pqmclg}<69E<@jE-qZ= zHkOVbKiC;_**chh-$hab00@Abq_~D_@=mi`9g~dP+C@v0+qj4AY>B*Pp%*tNT0rXq zm@;&y24K5i*Zlr__dSyt5gW0ORKbCtCCft+bFzKE&`R0$3 zmB^{ij4 z3mOIhkosJel+7K{71#W6PED#7!?${}x=!M%hG%;{tG&})(zk{0dsm7MBbdz37d=l^ zLGE`8dYz|UvZz>8m(*m|kkA7F_*SG?NQ_#sD||BM5>ROPKyNYL=Cb2V#@nIDy1IU+ zInDk3tULxQfN#}wF$%NUyp!hy4>RvyMm8-s*UsK$Mx{IGrquds)0Ykneg6<8Ndk~1 z0%XG9KJaCsC&mkPm!zcMJUQR@9c}%y(?eUGCKtdC@T{4yKbP^(4j|a{+G}vSyE`Ag z+fVsH*RqA{-q8+v zSgxX*1&`sl2hK8;6^f!?N;)txkz6Qez}s&un>i~$1+C+q+pHDvGm2i~Oxn8FPOBxp zYTNWz>w7IPlfSgcOAcA-X1E-WQN$Yk%%VfeoxA34ljOct>5 zr3ZxW`!u>bLUUZUn{<&E7pnpKNe5#6&|6< z#6L-*PV+VL@s@bc96?N%;RHG5o}&sb94$MV9zKa!8u8Dg!-q0{$~)70O{=%-8kcK0 zcX+kaOxtJ+LgzNCENs91=J;hFMfnwjV!$JE_J@vO2hzoO&c5^G0h&%UJ?;oq#h^>7 zn*gj9@OIfl^Z>h6 zqlo|u*0?!RX)dj!)-dct{?X9zIoQ=Z^?A?F|FK%rMw~hZ-(I#GBB8MgtjN>P&hJJL z(s>XQ-^-sEm59tD#RJS7e`}05C3hE-=)9XgI;PQ5=hM<#&HWS%W9e9QfTO}8#q2A= z=r4A`AmUR5bbm8(0&;~=c0~oxDj=ohrAOqwF&@+MQ>@}zUaVS6mcB8G+j%%D{GdJJB zJ`l8`mv%s!j;_V*ZRzx>Vav>NagC424$i-Tq&0OB)zc>o?fbYexh-Y?jt&Ktt1u|a z@lR*b(t-j|YRx#?c^kfICYc%|TzD?;>Gj_zrcUj)7L@=?RMK>=g_>oYb%DewGcl0t``F*lHh_en z_f1cqpoH8$4=dG*PH_q4MG*ks{&&L7ca4Aeuf%&^w;!PrNPqxI;z_GNDM=xUG#G6j`ldHg22$&>!IRg<8 zF>tkcvj6+|Lr9*g_B$wAZjjRFkc?oz#LN5jWbxh6m5rC#*2T;CKhBHaC4|2IvI+OmpQL~&>3J81kmlR+?*2mbJgWx+jv)P zTW>$FJv{L$mcMpCQEijc*5jn*?o1->+cekd5xWKLId-S|<3AD!i{htG*jIg7`aw~P zzCCqHKrlW1NJKzXxOiwSO}AXKI)OmG@JXu5Y%LkqciSx`p7`g*kmnY=iw|b|27nG6 z9k&0XaoyG^`r~e5c<ie_uTIQ_~=*yD%yDSYCC(&pfd5OOtKA$4|1?Dyzmf5|Baa zZEs%SbR{h2J*CXNr?>aqGrp}DIs2E3zVFT8<(A1OSDBsDlY4Ns05Rd_$kO zu+p<+VZbcL9qw6i)WoDWd@}6J;GF!Jr9r_jf}o})2HHg#z}(d;_faRe+x=6O8Sw=n z#oFD~-I7Kz28mlh@1C)!@x!2qmaDic+m?UNoKoF#4VSq&IICMOhV z`6GPN;$bqE2dE}#x=xVd0ieV#6C+N9lJ^o8(GtUae!fxqUDR{f^|VQ=tFD;+Gaq!B zV3EJ~K@0k=DZYksOOSfsI1hk5f-*RCC2HGhGFFKX7W&GW-4DAO12P8_;p8=0bZak@)iEiWGXQ|0z zF$NditlLykIMx#FWO(m@mD6w0zzf(}q(UJ~P+Pll;=jSc19%CXg!lLQYRjf;J9nzhEv z?woDdL_}yX1PzEri1*2=ZHm2p(CIrMw_;_MEn(+|a1J1ZZKu_Jx!MEd>75-edi-oZ zfxI$+L_8sMV+40U3P6&>R|*(_Weqhq%;KEqE(n36o7Sz`1Ug_9AQF%uf55Ype z9H3LZVaW4CSjc`2BKDvrN zvP(*$0{~1aS$K1XWU*5^or&Z2r*p*yzZb6g$T3E5_Vm>!Njv|oiL&m0kGt!3lB~O( zt*ozK;@^E=?^ze*&$upI3oRwR2Pmx+$V+ea{~Zs#Wt zU?jijUur%^I&DM0XDr})cNIX;Fh{5lM#xN`4$hE*uQfjvnV>`(^D?LB9k*_{^@h&! zNBo#bl+s_9-%nR?*lFkutg%kIks}VijGiw!5b)x)mxlmi0`Daa)QCJk?U{?dz*ST^S(Bfn$$j`ZYv*vW>z~kP zzjWj!e}Rdh<@|6CgBH`BY2A|jK~{VJ4tiFBsSyy z{F3*N?o74g;nNb6ePPx0VX~kz;tk(?ac<|be-`$;T8d`(Qf7rsZr>Ymc*4+~M^4@- z@272(NnMN2;j;flh_`*9=qT@pj5&%G8r|PdtTs=ocT?CT7VMDwX5kP48*t%1AlKK? zw1<_CKv7valROej?eCY2rRE5JWl(<;JzPfhz$am$InfdB_J@!Y4@E?p@~AP}J_HMw z`~9z6rhGy_6H#wHy~1pgYgtAtkxZdoK)yMZS+2!L4_%H9YAH#mu}Zxh$f4=-$>9MA zE*g`4Fx%LO7y8-%F zeaDHAkr+Ti8pDr!(6FK^7VQ1dkK&OuHKiV(*27ODpi$rJsm%Wn#^b;<_{Fb}fle=e zWx+YYa_QmlpATueXy+`K#;EG2dSV>2fqm zg&2CC%s|okgaV&3Rk#~poNFlUqpT|3wpFW9yH8=@?O5m=jlrHn0Cg!eZW0CZdy3}- zy>g9DPO#$I{VU+)SoE~1^V@?L&$Su*LBy!vTO+f^^wA$!gd1IkuNOO9cg_H% z2}Lop+)Ch=OY6Ynbzkf-DCmK42!PV%el!Nl^QrJ%lLMZm0o{sPUNaKF4XZ04$w3m4 z9spi(S3%MIZ;4DlYC|byScZrnFGJUd?LDmsLmnJGp`m$Q$1}VWZ+ouVsN?alU_H-z z8Cqy^?wW3(BKgZnk|=`oM94km(VMc5-Ch6mOiif8UW@ybjyUKO!(fCJ{9EIk>S?p( zr6;)vBK9RVW7AMP;wM4rUN_rgW`=OgHIfLtCfqr%)2NJx7yT(KRk`=8!~v%K9b8t8 z>2e=H0`F;%sIEuC6yFk~LIB~=IP$e1XPTdFc(v9IE8j=I4Qi|A`}E$X_sb2+9s$&m zzl;N$AF)h+C{s9{%-BGSd_LZ4)IWvVBtP}}G;#Vb?SbV_nIoCEsqC3<={3bAIul9} zA~hfT2Ym7;+NY(xy%Rq*B8X(?Q0IEpSgTqUWNkY~YupS0cje?-< z{W-|RWzbJ~hpR1hnQQiv&vouyKOVMy{?0&Vu)eJmt_|*Ie^9Un^7`TD|6gsx1 z49;m5Hl3TbaU0DIbde(D;5M}!hkhdJ8A^T1x0gm)cJtFrKc~ua3~F!H>55W`$d*I^ zm6gU|B%#sjZ9`{X=NWR2VI!R1*?8UI333Nm&&X8joo-ObgOyCgYjG}4v;t!Z^e@i^ ziZgx6S~7ko<)YdB^|kF2aSQ3P(KJXEdkhBPls=&(3BY0?lTFr_H4nGCy|p44dhz=X zu{?T}4SKxR)5x28rT8izC8!C*MSFw1Io%5jnXik$2{QR3fEp6P%G%qJ5>4EWI+l!N z1IbP8iagkdW)Q$OG=d0WU`@9LjbI4RtNXMt#p6X_)iksYEAaOhz1hcJN*#o3mDA?& zoc<^+EsgChSjs(5iZCJRO)PF?pY)kkiMDxx69#1pB&4%kc_z9LXIs4^A=|&l=+Yhp zQniZTvSw^)Wngl-u}1O6y_wk4qJABc`w_bHW9|0`SFBUOhEu=`SY41?3^6DJKOWzt z!1dt*u>=|XSE=e(NO)Krgq#BwQzv0o`*X`+zsTE< zz>BGYiIU``HvteZ)!D6+St($EMYSl)H1Bz%ex?DaQRH4p*GTw|<(%+bY4gYWdK8XH z_&r_>l&H(m^lRAIo)tQg`S$JP4)4)A8`arIHc^$Fh7#(bP=82v8f4q-^?eD(Eafcn z>|9k&o^}6U5s;d*RbMBD6(Esp2<{$)LXHT@dzrY(D`oE)`KCzmEZ}`%YW7ATs5qj+ z?hWbJo6dgU%s2-TMkRf4L&fX^o+BfhROZY6pz|Lv;Hl*)Tef;1tsSW7Kc9A=EjJ%s zF{?58(`--AqNi&-dKtuR^={(w1R*msa5PLv;i1iGH;6+;lbl_0(-Z=Y!ZI;Ml+VX-?B@VpF7RMbQ z9RBr*?f5sKz4t9?u};Dmsme_R{QMVH@`wTNyHMCmCwCL;BW$?B1YxXzNMVW2@(7!( zZKq`P=;%)O*>=KBn7hdJ0Aih4GMCU35l%P+`i(-q&olah0I=SN5=B4$7}m{~=d!9q z+me_1*l*Rh{X~dc0FI)C%LZX9y-}_3nHIOr{`i3^pJ>u4DSrDup)(Qx_R)a1Js^EC zT)929^J9XE!P$HQ7x+erv1_g$%_nLK&XlUQ_VAkPQF9GaS$~tN&DfvOY!iU~OjSE> zMocKam{Tz-=({Me;><#K4?$oXQ!H}O4?%+jY;@mBpc?8oL;_-Va(5`aoy<)c6=TAS!q>#}^f+y(L!ujYcOa|4 zvCkHt2eW0@$6<(GioZ?vOX^z_r58+e2}<8>*cZ4f*9CF&brr8WRyw+-^w@B+%6m!B zzo!^-I;KgISTL%J`RE3R#tnmHk5C+VtOegX8A0HDe0qL1aPY*6|F1Mic+4U8oftep zj}HF64fNE{=>E1t&@590i<5iu8`C}GE$_TH6tY_TLE@Ux@t$S}Giw^D!uRVXqIBe`P8PD2+s{ufJTpr4 zaz&eMOs*f&W3yV0^zOsU^>}rNKgFwc?M8>z{TtyoZ1{Y0ZW|Ojy=Yky_mYcYCz=kR z6$~LEPIv<$ro#&r+=8f2JaUy*hJubz;+vmSAv_XrGdDM^zWl#Kl%RoFxkeKAi$4~$ z(=ASe+<)*6#Gc2mZpa^^qJ_d3ov1GmQ%siMBX;L_ufA>#Lc%PYoRE-zSNq0hP&en;{r9D>uE@K; z&DMtzdAZbg@d)*R6nr-elZq45{{Irn2oiCFL@gW1HM%P)=u5wU7Mcw^#&M;2@)d^F z_MQX%AZAXOnYL^hy|DGW3_kF6DG^WjezJ1_`BQD^IGLYQ00|tUpV3L84ecydrGll; zq0024*?bwDbU!zm!#8rJ-h1$G5$J%<8YG%amEC$cazZb2l+Qd1NKj)6N2nn(wiRC% zZ$mYw25MLby?D(SeRS%?V_J-_)|&o*KnZx?{2g8MT0)e1JIfXU&RWSKp-`ae$P)@u zU^=7g+3m7V5{bDMm8L5~2WLk>fjSeH1Kdt>K`C}4TRpGe72)8TK0rnt12H1Ta{K*0 zKzUn^vmqDs#nOchAvWL6QP(M7mw*~ez%q75JO;cd65svewqCI+@3-*s0uuIq&{Hl4 zS2&8zuqRg@n?hakgcg+qaSwu4l&TuS^H-%1PwbCFV3QX&Z>$D&V#`!WSYNzBs2AR#CB7sy|dkK2#X|K&^$ZbN7Q@>dZS zM2t;`n(KE9F|U4 z_0>&gdqYi|-vR2M4wgIH7;-nFXU5wiAnwf3ygmS~EP+JfEFmzkeOy5rBs7+}_2bZn zh(tM3RYTX968@HL)%Wz!6@VLRHz-p*S}0`^xOfOSLaG{DRE0u<<%!Sy`g@!xP6<2^`daYz}{$ z%q-Ih(xOz+gQLlPzq1k7E3o#vqP^k!dCC!~kcu2~9NGG^YkLvcv!rpq_XVs+6iHN9 z8W{-jr^5F`QBtt&JqehCaKIFmfJZ;6mWsn+1H*~u*yJEs^5}tYfTAY?j=#W_lUxR; z?a@eqV<3G?pc9J&-&DEK$Fp2n4gYo;yY@Z0$Tt6=qD6k4pXfiP-S87R#0Gs+4-6l$ zM)KE|+kePtw9lAst{RP{$&P+VCS`rUO4GkVqk=QmiXER0^D0M!u?Jt(3BOJ@lL61Z zhTNZdlTM{3>0}vKc(updyglkDeHi})M(x{YJxEQ~0l7Rt3yMar0p)liqXY_B{1$<# ze$O$X?2JmDSY%lsy+qO*gkLxe+#d!5o5bwzqn8PO2V-Ztp8QsUzJg=o)lvuZl;aPu zeyhL;c8xb+CBQ5md_xHNivhDsA{n?XAcVUgO6`2mZ=lD?BX`EF*-|)u!zG`GzA0(> zRE7?GgX{?Vjd?AP_NCpnPTm@Mnww3=(SQy2UxplNP|U@A^Pe9IvGoCJ7}96yq>L*u z-fw@-NpR+fu9sfHB=%dBOUOlE@Xj@Pv=2anY9` zu9VgQh-DopLnw0Uy5CXHKR@fUV$7VXdmg2n=RyXE64l5ZEVZ_*8ubU(t69a{uhR~!JeXLFJNE|PNLZy0lBjkcq{bkFav(_ZymJrcFIDC7`bSO&JWl09bc zgk+Az6smv?wDbKT+xc(!MHg0qm5=CnvLg1e70fl|$>&&4QBm|JGBO;7=mfQlY-uWR z2HQ*o!6Hv|n-d_&1K(kZfIu#eI?Bf;o{29bKW2>K#mh>Z3& zaQbd*_6$BFFfd-+tJb&L3GpL@xyGx-bwU;nQFrHP5ktPb%>kY%9TquLMpkkOQBkY~ za=~|Lh<=jJtk#4x1oo-csILohm@{#NL=+wZu2x=cNBy4`U?2UM_I&7u*+tBIds{eN zgOk^y6&lSvdM}B8?AD0Q*{jK@585p{9^@YHxx01Nw1s_~XA(G@#Q&5QdU*&?jfF&p zz&f%*5~-guilRPV+b3fefkDUlAT11o277fvILsgaz*md?-_6khP?%p=MD!O+D9ly9 zC?*JSABKzXk4TFQlxA-BGivjsBC&Qq#L@(BtL$L{nS@-qD^72?lUhzf=DoMtBx-lN zAcZ!AN#U@!aM>l$)X0{x0&8M0OqN*`B`@UnoJ&c^6R}Ew-?op&p@9SlVP3YBYA=5w zE-S40uI4C5_(6nT!HHg&kGT-W9HJyBz+I zPq13AnwuFOq5@{wzpC6F+32V$pPX{qir@8&{gLTgV{&5Cxw4OHICjT}v*^p~q(IR0 z|Eup|vR>tpd^(f4PZ8nt#|=zocT6~5mAdEV`mCrz9%9Nk40eMu=#fV~z? z;{5c)((b#DBqbU$Gs0#LUwDuFWy_xX9KP|L?M)b@qJSi1cBs-p)l#F}sl8*bk>2ho zz~FXUfBvuYzhl}sm2a7nVA}_tezK_EM;(@ z&pEzFYr+sk#a5_O%dh^j`I}B2B8US+FvKD-@mUGEqih6Aph~jaPaHjAp>6g885yHR z6$eor0F4ZT`nvdLz%AYqu0jW>Xyd<}9L)wpAQ96Wl)L5l>+a!VO;!H12hYdz=O2-l zFpp2=o**=qcP^jS6XyUd^nWt8v0`4ugtD0P=E%f!1`G``N2DfGf40=J48={-=vRpU zs}}NtY$MSL7Q>;O7|6^yswf$f#B>0RvmMms7meh|DSoBETGS0$8Z#drFFtzET+Do?cUZ_DP%@$FJAwFY;rTw9A?EkuoONmrAplhER5ew3jnYU3QWR<}E5 zX6}i-&QoKE%63{O)v^8%9swax;EX%Kjy!ioXDtD>)_k|s`f_9i-hKQW=8!QuYvz_- zJhtSS=VBfOC7Bix#07arI>cJzSsJOtv>heMM1TG8g${{YV_==imw0v10%- zhumumQW&ROoY-CM0XjWuR~a6hKNl18*@M+kS2tSFDLXe>oX(7l{9#ioX)_-gGt5eR zb`ru$oC3Z)LjAi0-iXfrPU_Bn(Og$4#M$VLz{9V=;dSMdIGaLAAdFI*MVNpQ8R<=% z!#G%f`q)<+`bVJ*h6OQgK{gLAfm;L*v7mu6TB5HI*^0na7gl49t$#To&8cvFcYT)V zH)Rl#&|hEFqBXDWSdFh{+LlhdK_`meOdg6JmF>Z|KL#+!-#!VECRO9z4d92pRA6P2 zicyY@1jTB}y<_HveHw>FCg#L=xnkMz0wJHmAyu+5F(yvBm*+P9_0s+8zMci0v6KJH zko1EZ=2((P*r?ze>bg?UY(g6%G33^;_Rh&@_YtO7Fmv*8kEj-D=lEk5b^~{>VO>vT+w4u=HGPRa6W9@)KZLtY>(yl6u<{mecaFZr^fgSWJ<$FKe>@ViZg>-6ICy^#DU(E95jA0Le?Vx|>s4+3o zbmRf>L!Q>4`%>mPYH`dg7!rLBkYpvzlSF6+5NG&!V(TQBawbBSK9jYjSf%trHtf7xoxk%bU)#-^F$1SY&;SA7C#(30itkJ z8z{Blci{oh_#M(DyewUYtiY-`D}w01kU$KB7SD_qXR$ZKC0Kh~ykAyGCsK(2t?QSL zVKbt(k+h%X3kty{UCK&DS8T3)wk!I^6_x9X24u|aMVA>xrHKw6qUB=uhX`;LRF8XL z>^STgm!G;o##^{2YvQSPzARYwFknV4MA&%%{&u@Zuu&i1Ix;L;K030ODGX$H+O5r) z4MqTfEICJ;vGh3r|EVe|x!!JAn&;5svZ;X%WA5fI)n%h8X-}hs+^?cYNM<3DC{OwI z@8hMYi_pF;$N&pbWq{%lTFdSSJEKTx*=jNy?G3aA4 z8ZYMz;QkNpxoYC>VQjciSBwZlu;%POZy-H9a)i2uhL)$|bT>{Zw2XybTXo>m59Uipy_7IOS~^4W4+4UQ1W(ZoT`2nWk?4j5z31Rv54m*T5) zG|iO6rnfxwf8d>QIFBCE%Z9M(j*l0YDyWmw=4;@9bVS0O8a{w zf0dNe>R7NcjVQ1##_4W5J-`9sSoyNXEOa|4vEcXPy!Ea$q)@H5O@VNA`ItUK?7zYo zc?;uB9SHQkNTpu~*+>~NCZ?qfB;9sQ-v*us6wu>IaHFH!>h6)qv&vuneOZ?kK?guO zt<98f@&;CYSSJ)^$SAgx#nnFzf1`TC7F|x?Kv@ckollAiWT7l9n`tHWuF6Bmld+Mm zydnI`BO%SUe{RP2VK}F>kFaq?$@3M;Hpi&RsS*^(q@gx7?muoOzeH(_NERr)5Vj8h zXFEs}t9R0)ACW%GQlX^_{R&}=)6x-#pU3afdUyR~$TJje{Z-#E8s-dKHKrPkOgw9a zIEr72m-Id1wwz(7a(q7kvTVPZb{5z>H>?y;{j00`z+x%mTl3)j?c7rTt?~VD3b>`a zPu7P-$c69JPOj(?X1^xO7?mHCNszA`)o;uBx5|bF4zq5*7alg`-VwDQcb-W`raG$r50V? z7VhdCeqrY0ZPEC$WFn^i%;)=WYoV9>OEkJZ2YlmR2M2(`wEn_o*BnuEI3&yJsyG` z4+i_y4Z1gUpPKlXqHWaJay%<o+xtCMqgr0XAb;9{aO-jFC!u zdglQ!LY#4s-<|60dY2{A{Skp=nYYwV4NXxOguo4eAp0Cw7^I<%lU$~d=7;LxGj~c* zEv7FBdr12S>s7AFm&G4p^J`6R`f6dSDL}r|sM|V^)S)Y6VI!4>>J2PHmST z8Pg6U1z8{IH?uZbI<|o*gCP|^zOL@0)$A___=L*uFt|$Y-*|CO&V;tJ<+$*!Se32! zkF)6H4|KDyFX17y1(OWgr|r!~hau?=9$yCz?j=0qw4xuP=)1ct{Sbe@?WoR6nRS<- z#r<^~uF_=t3c070qI>|rX%AAUo=Lb(^}JS~xEn8qJwcun30&&USzZ!aLdBl^8y?(CzFp{Je@z>;Kgg(?x=>}BcS_)?rq6r4{8W1)rR16{H9 z85PL)`gFa=8_P9KmM?f2+1XJDhOW!-0q9Uk*YLZWS2X;>8R1e?Rl& z9E~by;dh#hxfR1)zx988+33LJ+{B? z)Efbn+Jbyt^{w#C@J6CU0!)fD#XMI!2GsHzxov0Imjksa)Dxi#uMBRl9U0`;ZdP}7 zP6YT~+5hFrDgO8Yxp$|Rb%sm@*#Gn($$);Q`R+Ge`>-8k)PYQA%%9OIVzZ2kujIG% zfn!lfK(<1e*kQ*^Y=dBLf`l(IQ&)^Y%j)GKh2B7h1p}4!>!I0IV#SR zh^+^cR)MEg^M*AsBdfkW;}p;Jc%7ymr{tIoyB)M^(kDp1#2?K*ty!on3SqrTxePYP zl7Ia<)94YXV2f+z3S3xW&Q_rsvins}n!Bq%{xOJOx)+mD@)9;Wt z-slpYN+dez7WnQz-ngJ7!2;Lduk75L{Vdu)@&ql`eU1Gm()?9(7IsEETyHzS8_MF< zsqs@pA;kP8F#v1aDClM4kl8G2@kN(?x0P=GNZ2nQ(BVeBI?u-dpX4EgW#>uj!+(K7 zZ}V{L6j?A7uq_X)f_hk7#lMA-FQYg>IkV@stE5cZXI$@xuI`A~&KK$d_)mW3yaBJDr|k`b97{96ugh(@x}#QKYp zReGLWJ|i*Ds?luOlf&2hQe^C_-)u09r8n2yEm zGEmpW&#mfPif&-6$qpqp4l~#AKHtcbgHWi;Yk+FpIXyRHxf^#?YHgFes;7?z|Ag zs*M?s4F;LM@a4y|Hx4JTj4NShY|xi>nw`cGTR|S%ojDV~UD$JR>iS@agt|=6Y;eK! z&b#Q(g}t5HiPJM5+bYM z6CTkj>}&N0)cLV2G2hZ?zAU% z0tGZ2Pf*Ceqbq|x(Uxr;#sIwG>HQ!9g9TFkk4^5Jv$$p2ppt~8VEbRGDd&gShd5X({6(*rn;lNjDgSQeQ4jRjCiD=KY}HPay{e6t7>a5bYsj2 zG4KVfToyc@=kSh&6<|Ee$;6tSNFDo^Oiis(!u}2aI^UvBWil->L5)%2AB&9>tNSNE zYm6kC0@RZrL|+e5ji>Zkdkwbf@jpQSs1F`Ns?9;%BZ!JF@H6)If zI5$HW8A?}ubKMJXk`G|F$Be12HcVpk?3fR8-@Hr19UscM4N*4*XZ_6Y0`(o@BpGNm z+#0tmyNp@O;Zrwho&(<}u;ZDMgoZB%fP2e8YS}o&%`o{MccU0%`tsJ-2H@UhscThp z$v30lcFOdBd|X~HYc)AL10v=kxj%+Bv{trPuvV}$#O=r@ayG-YQv?b|HRU zp7yR=Kicy&M#PW1KAg6k@d|C(^mslT;84nCChhwG3(_Gjm*?NEI7jiq2({gtF}RgT zgui45f)K!?7O8CdZMPTlMfc39)2J`_{h;g7E`?$3I(OORj|0`k(ike@Z22;UnBeqF z90lFST|2gP+4b787Gmgg`gebEJ@D<@2RfgQ@gw3z1gkJ;>UfJUhX83TsJt{LTzYHx zIlsmg!3R>kO6fBjDC{cV}gbzTYXgZzBMXnpT&d$yI?8Z*hmN$+u0LK5ozt2ve zZ^D$vABok5cPiDhH*T+O>0eQrFFGc6$KSYp3Cd^wE&^^ylDazw^toZl@o;482)~BZ zyPz5CS9FXx%|ZbQPor99`SB%-8V)`XNKeD7K%#rW$bwiXITezWo*ER}7$VlZJTVj;8&GbY&jiB)S3NrNpkVrgk z!_dMZARRw;Hkg&rO~O!nqOM;Ou9&4qS|xfn zjW^pnqxsIcpVE1#e8HzlgQ&g9^er)2E3;a4j#RaCFPSeMD}fplPJD?AWz*`h0k0UQ zlB$Qb6myV(J1xbJgO^c9)shu&%ATGF#Tz<(pOtYH^7OpyTU+;bT$N0Zl(!$E`g=G* zv_8kz<^-uHpd514uTeIgfKgg;tpEk}sx3TmQYSmf_wVSAUgg(lIO5#63yC7^1pO$I z9N9guvnf1DsQDf{d{cjV$Vp2eh4=y>s-QvoTz_nZmkW*#XG3hgLPd>3FG$x%&WiIf zo_hyA1`u^-;4jYVw{AD+MPRNgq9EgMJR}slZS>?>h(lp0G*vC6Cj_CK!u3`RvGdHm z=Lq1Dy$X~jW`e1~RP$T$qmX7Yd=hSi!B zz*88fW<k(Db5FEpi~$ zg&FXCG4yy7q1C&FLoW)1bkWG_A-VOc74JG|oiTW#~2NRkullzvE zm^&r$+FJ`8B*HDm@6n|1GcZU&4O14W>}X)G5tUAR-jzp36pz^;Jrj9B1e2wO^<=nV z!|^cq_YfzhBZmwUR4~auZaDGH*G1nMt`o-B{&hxYmw~9`cf_I3`5XV3C8N^0$0^e+ z2#%l0TQ^n3o}`a&c;P~{3o)L35GEB1Lw@GnM|;h*aP_Xp>!uRp>RpBv-*Rr80tS;W zg=gnp{A1E)e0MMedkdAKM_FCp5=*s;6^GzUx^wP)vin3enloi^XedZ@HCKr#cr5c} z{R!u?Z+2B*zAG9X0pW!8SJZ4BVNG%lSu%?PfB7-zuKR(u}J0+grv zUE8c`zxDB$_2cyfen&y8l5o2|EcIt9UlJs88AsyB9-^9zxEFmp$RrlW@^RCZ|Dc4o z%yC6011Bq1ZShWzfoxC!pTeOZOiM^8@^E7N(z(&V12vsV<|}VC%HZb@+ul0lA_am?SaWQBT^fu%dY>D% zbZis_=T~rx`D4C07F|uAuMhZ2}}tK!-wKkI-2DG5 zYb3zo>@noNpC!>KZi`l%z41W7fp8!9s)DKDY=dxCe-+3$k(Te|#E@P0Ysf(s#9=BG zrzH+p0^)PRR-cHCI!NHI+qQayl3Z61Mj{vrX6XyWDx^;O%a)nE^VYJ<$qd+wdJoXz?ehya|OXz zJ=d3`I15NAu*&=d?H>Xz_fZQNx*+xqXnD*+him24rMBT+9(SF^*Jv`V@}K5NUquXqV0aGA<~k!zSU*wi zpwO>8N~L&LeX}!vImd7XcfX$GXI2>$sT|!I`pghYX74YoyOD{Iwo@IG4(>ozNUND; zOYQX{;}4q^QfdCDgL98(>i^^TW;ECDj3i<+_qmfxmtmGWx#fOcrQE77<+90TMCKBU z2}R_RyCmh3B)O~R8cHSiOUmY6eEWU;{ymTL=lPuX<@0>LmUYeKY<|0czsXW2Tzq;f zak#W&4xx{n1RJ~X|7?+eQ#)}sj$S>l>2{gxl%RN`F?24Kjdob!D~`RO=DNvy?#qX? z{_bBVP#6C4A$kC%Sm=GY_N19DHNNF&mPcSgZW-{Eb`>{<2+fD zW=;EU-x{SWwA?f z$7>a(3kpu&lp|*(+T_bUOZPug?b4gGDu!-3l4|m)_5%+DpYeo-^#&sqJ1p5R`Y8Z{ zO5+P=XA4dFuO{%OzjY*rYG^2sZho>oy&$$Q*u1h#z{!;B(TzLBGwmc&o2U=C+Fj3! z(L_*ez&OQb7|yySariLDjZg#O~$`wurElMiQ3R50oyOCALw|aFQA>kB&hUTeVh<7%+u? zSfA3eZCvcqtj6X1$b!EkzM7hd4&8sDh(*sy2F&0#oOt;z`wwly+m}*mV+|~x@BmIX zO}p^WYpBy^Kb&NJOitTFaBtUQQ?*e4R=5Jv4%@!zS8}{s@%_8Rx*7De29vOyO-+)< z)@K^9S+h70;@_X8E%zScb@sn|nSS91_nq7DD`^ksV|H}=XQ3r{gD>Q4xj#H_lG;@2 z&KG;gfQh|-9_n&JOSFVU0kBt4{JW8g{Yh}QQM!e$6|CJmHC-Czuv-&|`M8Hqo z_I1?dR2L(@WVi7vp$wrILi5N4Efg&Z)dp6+FKtdDNkcv0$S>_cET8;p zCRFvogzRL7YG@&k>KSf5Ff6i|j8ATBv=QJHvq}C^I9@%3D>yG==>0-A(++;Q;!u$S zqrXz`m&>ju4>W!b06l4lo)ZTYMO+rVjv3?qrj;yCn~AYQQd>9y=v9t?^xE0~xobJU zaUZPg15BeIHs78KEQIwOJroVOp`s}gjZ?1@(Esaq?=mK{k4X{ds1K6q`Y{IuEx`pR zUv{t=tKw-fe(e#N%c z=Dc6#IGq=9!as?U_t&)@2n#$DCG`FE_!cJA z_^G;FxquLfqK8s*(mLAdXsPC=j!{v5DAv%!M-DfkzJGYk6D9AdU+{$d!#rm~j-%MF z6nwpx>gXD7{)>w;sYQXO*AwwXW zQQop}<22)q?Pd;Ruih+l2^B|RCdq%U|MeveZqp76?^tEPzLpO{$!%E*>S_pTJ?C`= ziqFR~I=oO5Vfa+NNFUMoMk4{9j&-mx9%$D%U7swktEEsqkZX%yr+gCvG_Gaa?aO+$ zY-VDZP%YX~_ZOwQh-c=l-NNc2vnQUk_&`^zM@J$8Zh!l%g+Wi4oLHC-cwFj@MmdL8 z7>Xcz_Oz@?fXjs}K?+39(sD#OgbMKG`w|Kz^=~YsAd#yZ&&(4bf4Co+M^H2_&|Qpf z21f8d3R<*7_T9pd4>Hv^Fm!1;m*ksu<- zqm^Ud-nX)+kyR5}EEn7tg$rD)>48~A0FwM{ zHyqFjZLib-06`(3dOF~fP0JANYJ4`G2ea7|)3k;|*xIA|Q z@A5WRj8?Y8I+-z0^C#QMh_*Y39WrrX2Qq_o;GaLKpe$Bmsa&GKb3+NSxGW^EQf+qx zO&wwa*uU`qb(?zzb`QR2c}H?9{tLPTH8uy;xpkZ$YPIzu7h24ENk~1P7VOw7uFPU= zj^Eeuk_{r=We6`5!&^-`lN-HLu?J+ij0INlN~iby=GEG313Q#=op8u32{Kx0EQks; zBF2BU=DY`2$ULCLT6PISWQj!v$RD8;hNeZ}3i8UN!qu<^`_m`*NW<0JZQ|d@4aUQm*4c(RcF)L#=h4? z>S1ziKchYDGdvVy?*ehdGs{h%m{)Xn4e|5|&4q__pWhgEF^ zaFS0>gu4Iy?fQvg!UuZL;?JU+Q>rV^5VH-n7 zo!D90@7vk{pIay(04YELQr>1Ga*!ny4{r0lp5X?5L;I1p)+zNWM?s%xpLrnRjwf;a zo;)l1Vuf4%0*z1tpJlWo6x8$>queOl~W z{kBD(ue{-Kzh3e2L4b7yJZvN=TvmS(#!dJt-tzs107iaOz`Trf>5TtAMe^((JH7d| z?DQt=lj#ihoO2)}%T>w4JTcZjv2GK_fg^STaQ!hE@9XqVT%=BUyN8II0(fCMyf`I$ z0eYAP@jwCJBAsWB1yNuExoSq+*+9i`*rK6dC#pbhWp`;r2r?ZdNsX2p^|R{#^YE!{ z)SS`Ei1^rOP%|+BT>@8LO%=NC&WEnZi_}V4pRj*JO^$`v@sW1f?<_jJJI14K_7qh zr(Ji%u5v3^-enmu1@oiTc6R?5gDM&Q{v3`H2S04-qs4|A_x99aoh-mJ$AG;De(sCV#ubY4lfXG-?I)?hh-fag)2` zSJjZOYub#@Yw^72CeG^*u$gZMPvuSq{#1Mwwm)}qkgvh8Oqp~iiXGol zW!!hMW$W}`u>)?sxdDLZOWts#Gz1&!f1KRJZ%+P7F^?8QA%1quS-uSJmuIA zYFjg*z9==QvMx*6tF!%_mwW?VW1bU%AT9GWl@EWc4O!NlzOQ2-{an$z(wW+`F+Y^T zrlF&CAD}1xnr~qmiMdCZcw&2pZ&jvvB(wHOVa)r`kqeI_VtKGqX069no@dh z%-3;Cn*)od4&yS81N76J_M}^nb(DLEg)6j;5tx;j~b;|I$0Q1Mn zvmE7RDT>8C#FJr>chB|`O5)HI)CI5<$n0&d;mW48JfSS5h;#d;jM2G?CqHIPo-`RoE>viz zT^W3JR^Nc=E$Q5`ktjG_@dqvNWdEngwTG$}c`9D|1$;GV#CX8NIVeZo_LKc^uD7tu zn!WcN2OD-bVG5|L^aOZzP`o)=Vuc$k6fHczFA?og6os1#i+4xiPa^1m)N6{u!?Rg9 zVuV{pn$&xrB{ys_viYI5WhQ2j;xlQRbAboLUaVeMle-v&c+EO%wp%T-*)w}2YKsL_ zx)16hfiLl;IoiMH(%6#DFDPm22Z`Q9MH5;UzY9)(5~PF**9SRz>C+Ue*#ctJN*oSj z6sypI72kK=F%2UZ21k5B$iulJ4Vgo#2ta-4wzpZWZV3`Vm`*gRR#!X8PR5)hF(j3c z2A9U4=cY`!>YUOerp_fI%3^o$6~T#?)^|R|dWs$&!cB4iV;XLXd57>T8a8f(vRfsJ z`&i9H@7^xllf`tF>~!6BA^++M!Zio+Cg_ZeEGUFWRK37!gGXC&ZE1{_pH(5TZJ66 z{Y*j!&p(Rs|9)opPa9rLhs!<~WQMlI8+*JT&9o!9GJKy2%}hiT-<}ae zI16*DBYZ!LrNb$7PP;+N^}JUW*hkr3`b0%j>Dz>hbTBkml+|;q^n9#}1Y#H!0j3`> zs9a>bAiBq-=*tF=pox0>qQ0c_#jQ1ncTh!~rkwxH`QnXc6^DoKg%vFFKCs%Z10K_! z>*_m6hb9nf2l+$7AjVN1&SR7y&*b-!qO}{%#<*j>+iMizq-QwVERRgNz>AE&b^K-o zrUPel(u?yx+?SW(DR=r4zsxSdJ3UWyfk!69b(6Df2F4^roKANXy=rMPU=7|Yv`>nE zFI=a72vn4Fg%c5v49t~>grI#S$k&KR(s!$};Tu}&`T_=6!MCIOv|>%aF1lrtgI$ho z??x*Zf*J?EAocDg!9^c8$1NgNyjrC?RPB)m1E!D+C^w30_y{Fc1ThXL@qq_HTL8FQB%c!v{u{LMbI@aG zQ&QXMPxzO%qsDPa7XBJmW*`gh9<4JE_2UT9BZ2>X(Nv7LDT75(ex5xOQ`-5l3Jhav zgw=jn1p?8Qs%>g~>~y7cism1#H;=7~+U@#q2(?bw$Ff7;W7>Dx$WzcD6nV<;xsc%T zv9I5u@&gRmQ4sSWPg{Wg(*?utg-0rH_!{_ZEGowJxv7)M#M+@1#a@_e3qW_TG`gT{ zK#GTTjMPA}&09u+U! zbwzkS(=1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + From b78e3aa17bb8b80fc8beff1a082183e84f60561a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 1 Sep 2016 13:39:01 +0200 Subject: [PATCH 192/714] Switch target test setup to Django 1.10 --- .travis.yml | 8 ++++---- tox.ini | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2ae6bf20..2c86b524 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,15 +10,15 @@ install: matrix: include: - python: "2.7" - env: TOXENV=py27-django19-alchemy10-mongoengine010 + env: TOXENV=py27-django110-alchemy10-mongoengine010 - python: "3.4" - env: TOXENV=py34-django19-alchemy10-mongoengine010 + env: TOXENV=py34-django110-alchemy10-mongoengine010 - python: "3.5" - env: TOXENV=py35-django19-alchemy10-mongoengine010 + env: TOXENV=py35-django110-alchemy10-mongoengine010 # Pypy - python: "pypy" - env: TOXENV=py27-django19-alchemy10-mongoengine010 + env: TOXENV=py27-django110-alchemy10-mongoengine010 # Linting - python: "3.5" diff --git a/tox.ini b/tox.ini index 24e7fb83..90ad6e10 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35}-django{17,18,19}-alchemy10-mongoengine010 + py{27,34,35}-django{17,18,19,110}-alchemy10-mongoengine010 examples lint @@ -12,7 +12,8 @@ deps = django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 - django{17,18,19}: Pillow + django110: Django>=1.10,<1.11 + django{17,18,19,110}: Pillow alchemy10: SQLAlchemy>=1.0,<1.1 mongoengine010: mongoengine>=0.10,<0.11 From b83fa89610a0c8de51c388d8016afb3ae3a87824 Mon Sep 17 00:00:00 2001 From: Tom Date: Wed, 14 Sep 2016 16:15:26 -0400 Subject: [PATCH 193/714] Create ImageField files from a callable Allow dynamic creation of ``factory.django.ImageField`` fields by passing a function that returns an image file object. --- docs/orms.rst | 1 + factory/django.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index fb3543de..6019d417 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -143,6 +143,7 @@ Extra fields and keep its filename :param file from_file: Use the contents of the provided file object; use its filename if available + :param func from_func: Use function that returns a file object :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``) diff --git a/factory/django.py b/factory/django.py index caa64a65..a7f6c780 100644 --- a/factory/django.py +++ b/factory/django.py @@ -205,9 +205,10 @@ def _make_data(self, params): def _make_content(self, params): path = '' - if params.get('from_path') and params.get('from_file'): + _content_params = [params.get('from_path'), params.get('from_file'), params.get('from_func')] + if len([p for p in _content_params if p]) > 1: raise ValueError( - "At most one argument from 'from_file' and 'from_path' should " + "At most one argument from 'from_file', 'from_path', and 'from_func' should " "be non-empty when calling factory.django.FileField." ) @@ -221,6 +222,11 @@ def _make_content(self, params): content = django_files.File(f) path = content.name + elif params.get('from_func'): + func = params['from_func'] + content = django_files.File(func()) + path = content.name + else: data = self._make_data(params) content = django_files.base.ContentFile(data) From dba31e3866cdefde1d7f37fc7f0553a12feeab9b Mon Sep 17 00:00:00 2001 From: Flavio Curella Date: Fri, 16 Sep 2016 15:26:58 -0500 Subject: [PATCH 194/714] Update Faker requirement --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3c2a76fe..c7c07fa9 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ def get_version(package_name): packages=['factory'], license='MIT', install_requires=[ - 'fake-factory>=0.5.0', + 'Faker>=0.7.0', ], setup_requires=[ 'setuptools>=0.8', From 02421885feabbcaf2740f414c1dff1e7d7033d64 Mon Sep 17 00:00:00 2001 From: Tom Date: Mon, 19 Sep 2016 16:37:14 -0400 Subject: [PATCH 195/714] Updated docs for FileField Explain what the parameter ``from_func`` does. --- docs/orms.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/orms.rst b/docs/orms.rst index 6019d417..73646827 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -111,6 +111,7 @@ Extra fields and keep its filename :param file from_file: Use the contents of the provided file object; use its filename if available, unless ``filename`` is also provided. + :param func from_func: Use function that returns a file object :param bytes data: Use the provided bytes as file contents :param str filename: The filename for the FileField From 9185c9faed958085692ca0c2810e9deffe4f42f1 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 20 Sep 2016 13:56:51 -0400 Subject: [PATCH 196/714] Add test for factory.django.ImageField --- tests/test_django.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_django.py b/tests/test_django.py index aca3eb25..c471f754 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -64,6 +64,7 @@ import factory import factory.django +from factory.compat import BytesIO from . import testdata from . import tools @@ -694,6 +695,21 @@ def test_no_file(self): self.assertIsNone(o.pk) self.assertFalse(o.animage) + def _img_test_func(self): + img = Image.new('RGB', (32,32), 'blue') + img_io = BytesIO() + img.save(img_io, format='JPEG') + img_io.seek(0) + return img_io + + def test_with_func(self): + o = WithImageFactory.build(animage__from_func=self._img_test_func) + self.assertIsNone(o.pk) + i = Image.open(o.animage.file) + self.assertEqual('JPEG', i.format) + self.assertEqual(32, i.width) + self.assertEqual(32, i.height) + @unittest.skipIf(django is None, "Django not installed.") class PreventSignalsTestCase(unittest.TestCase): From 39053f868b7ba22e63a87bf31b8feb67cc755b2b Mon Sep 17 00:00:00 2001 From: Janusz Skonieczny Date: Tue, 11 Oct 2016 10:07:29 +0200 Subject: [PATCH 197/714] Faker website url (#323) * Faker website url * https://faker.readthedocs.io/ --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index eb0aa459..c6fa2808 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -5,7 +5,7 @@ Fuzzy attributes .. note:: Now that FactoryBoy includes the :class:`factory.Faker` class, most of these built-in fuzzers are deprecated in favor of their - `Faker `_ equivalents. Further + `Faker `_ equivalents. Further discussion here: ``_ From 5733f417455d34ab333436e1b8d12f3c222602ce Mon Sep 17 00:00:00 2001 From: Amit Shah Date: Tue, 11 Oct 2016 09:19:22 +0100 Subject: [PATCH 198/714] Update fake-factory=>Faker in requirements.txt to match setup.py (#324) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bd2a4a63..ca80dfc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -fake-factory>=0.5.0 +Faker>=0.7.0 From 97e61dec7b156b832eaf60c517a5dcaf94d628a7 Mon Sep 17 00:00:00 2001 From: Chris Seto Date: Fri, 14 Oct 2016 14:52:28 -0400 Subject: [PATCH 199/714] Update reference.rst (#325) --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 13d70dec..fc1dbb8e 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1780,7 +1780,7 @@ Thus, by default, our users will have a password set to ``'defaultpassword'``. True If the :class:`PostGenerationMethodCall` declaration contained no -arguments or one argument, an overriding the value can be passed +arguments or one argument, an overriding value can be passed directly to the method through a keyword argument matching the attribute name. For example we can override the default password specified in the declaration above by simply passing in the desired password as a keyword argument to the From 9e61a37362206102a70acb8a9cff0c6f4aa2e1ed Mon Sep 17 00:00:00 2001 From: Issa Jubril Date: Thu, 20 Oct 2016 22:29:11 +0100 Subject: [PATCH 200/714] Update Readme.rst (#326) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4926559a..f9672f80 100644 --- a/README.rst +++ b/README.rst @@ -61,7 +61,7 @@ while only declaring the test-specific fields: ) # etc. -factory_boy is designed to work well with various ORMs (Django, Mogo, SQLAlchemy), +factory_boy is designed to work well with various ORMs (Django, Mongo, SQLAlchemy), and can easily be extended for other libraries. Its main features include: From 16a9f14fb53bcd6c29171ed2aaf28a1b979e958f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 Dec 2016 01:22:29 +0100 Subject: [PATCH 201/714] Adjust MANIFEST.in Recent pip/setuptools updates require wildcards to be actual wildcards, so we'll need to exclude '*.pyc' instead of '.pyc'. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 786a034f..5f83b75a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,6 @@ prune docs prune examples prune tests -global-exclude .py[cod] __pycache__ +global-exclude *.py[cod] __pycache__ exclude Makefile tox.ini .flake8 From 8e4a59cdcab331f09eaa421e1f9345b9d2876b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 Dec 2016 01:16:17 +0100 Subject: [PATCH 202/714] Release version 2.8.0 --- docs/changelog.rst | 9 +++++++-- factory/__init__.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 88062cc2..3f4c0aac 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,15 +1,20 @@ ChangeLog ========= -.. _v2.7.1: +.. _v2.8.0: -2.7.1 (2016-07-10) +2.8.0 (2016-12-17) ------------------ *New:* - :issue:`240`: Call post-generation declarations in the order they were declared, thanks to `Oleg Pidsadnyi `_. + - :issue:`309`: Provide new options for SQLAlchemy session persistence + +*Bugfix:* + + - :issue:`334`: Adjust for the package change in ``faker`` .. _v2.7.0: diff --git a/factory/__init__.py b/factory/__init__.py index 4dd75f70..c33bacf0 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -89,7 +89,7 @@ from . import mongoengine -__version__ = '2.7.1' +__version__ = '2.8.0' __author__ = 'Raphaël Barrois ' From bae1e09f3006310d8d0138ab0cdc5e7ce7917825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 Dec 2016 01:42:23 +0100 Subject: [PATCH 203/714] Fix handling of README.rst * Don't include sphinx-only directives * Import it in the setup.py' long_description --- README.rst | 21 --------------------- docs/index.rst | 25 ++++++++++++++++++++++++- setup.py | 1 + 3 files changed, 25 insertions(+), 22 deletions(-) mode change 120000 => 100644 docs/index.rst diff --git a/README.rst b/README.rst index f9672f80..633c2284 100644 --- a/README.rst +++ b/README.rst @@ -379,24 +379,3 @@ To avoid running ``mongoengine`` tests (e.g no mongo server installed), run: $ make SKIP_MONGOENGINE=1 test - -Contents, indices and tables ----------------------------- - -.. toctree:: - :maxdepth: 2 - - introduction - reference - orms - recipes - fuzzy - examples - internals - changelog - ideas - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/index.rst b/docs/index.rst deleted file mode 120000 index 89a01069..00000000 --- a/docs/index.rst +++ /dev/null @@ -1 +0,0 @@ -../README.rst \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..0d9e4374 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,24 @@ +.. include:: ../README.rst + + + +Contents, indices and tables +---------------------------- + +.. toctree:: + :maxdepth: 2 + + introduction + reference + orms + recipes + fuzzy + examples + internals + changelog + ideas + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/setup.py b/setup.py index c7c07fa9..a219669d 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ def get_version(package_name): name='factory_boy', version=get_version(PACKAGE), description="A versatile test fixtures replacement based on thoughtbot's factory_girl for Ruby.", + long_description=codecs.open(os.path.join(root_dir, 'README.rst'), 'r', 'utf-8').read(), author='Mark Sandstrom', author_email='mark@deliciouslynerdy.com', maintainer='Raphaël Barrois', From 110317e8438d571deca8c6a910d13390fd91d394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 Dec 2016 01:44:15 +0100 Subject: [PATCH 204/714] Release version 2.8.1 --- docs/changelog.rst | 10 ++++++++++ factory/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3f4c0aac..ccfa89d2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,16 @@ ChangeLog ========= +.. _v2.8.1: + +2.8.1 (2016-12-17) +------------------ + +*Bugfix:* + + - Fix packaging issues. + + .. _v2.8.0: 2.8.0 (2016-12-17) diff --git a/factory/__init__.py b/factory/__init__.py index c33bacf0..09bbab78 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -89,7 +89,7 @@ from . import mongoengine -__version__ = '2.8.0' +__version__ = '2.8.1' __author__ = 'Raphaël Barrois ' From 02c5e086bda6cb34d4e94e844c5a9ceba74cc105 Mon Sep 17 00:00:00 2001 From: Leonardo Lazzaro Date: Sat, 7 Jan 2017 02:23:36 -0300 Subject: [PATCH 205/714] Raise an exception if no SQLAlchemy session was set (#339) * Check if the session was provided. Fix for #281 * Check if the session was provided. Fix for #281 * use double quotes --- factory/alchemy.py | 2 ++ tests/test_alchemy.py | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index f1c8ce61..f3e4a282 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -91,6 +91,8 @@ def _create(cls, model_class, *args, **kwargs): session_persistence = SESSION_PERSISTENCE_FLUSH obj = model_class(*args, **kwargs) + if session is None: + raise RuntimeError("No session provided.") session.add(obj) if session_persistence == SESSION_PERSISTENCE_FLUSH: session.flush() diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 62b2e517..ea39c90f 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -65,6 +65,14 @@ class Meta: id = factory.Sequence(lambda n: 'foo%d' % n) +class NoSessionFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_session = None + + id = factory.Sequence(lambda n: n) + + @unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyPkSequenceTestCase(unittest.TestCase): @@ -149,7 +157,6 @@ class Meta: self.mock_session.commit.assert_not_called() self.mock_session.flush.assert_not_called() - def test_type_error(self): with self.assertRaises(TypeError): class BadPersistenceFactory(StandardFactory): @@ -216,3 +223,17 @@ def test_force_pk(self): NonIntegerPkFactory.reset_sequence() nonint2 = NonIntegerPkFactory.create() self.assertEqual('foo0', nonint2.id) + + +@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") +class SQLAlchemyNoSessionTestCase(unittest.TestCase): + + def test_create_raises_exception_when_no_session_was_set(self): + with self.assertRaises(RuntimeError): + NoSessionFactory.create() + + def test_build_does_not_raises_exception_when_no_session_was_set(self): + inst0 = NoSessionFactory.build() + inst1 = NoSessionFactory.build() + self.assertEqual(inst0.id, 0) + self.assertEqual(inst1.id, 1) From a0c72ee4c064ddd1ad93498f39980a26d9925f80 Mon Sep 17 00:00:00 2001 From: Luke GB Date: Sat, 7 Jan 2017 05:25:53 +0000 Subject: [PATCH 206/714] Use readthedocs version string in page titles (#337) This is primarily a concession to ensure that Google displays more useful version information. --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index dcf2064c..2cbb1bb7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -127,6 +127,10 @@ def get_version(*module_dir_components): # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None +if 'READTHEDOCS_VERSION' in os.environ: + # Use the readthedocs version string in preference to our known version. + html_title = u"{} {} documentation".format( + project, os.environ['READTHEDOCS_VERSION']) # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None From 447a9fe29761cab9f190fd9a68f44e68428dfd24 Mon Sep 17 00:00:00 2001 From: David Baumgold Date: Tue, 10 Jan 2017 15:19:22 -0500 Subject: [PATCH 207/714] Rename `fake-factory` references to `Faker` library (#341) `fake-factory` has been renamed to `Faker` --- docs/reference.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index fc1dbb8e..388f5212 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -670,8 +670,8 @@ Faker In order to easily define realistic-looking factories, use the :class:`Faker` attribute declaration. - This is a wrapper around `fake-factory `_; - its argument is the name of a ``fake-factory`` provider: + This is a wrapper around `faker `_; + its argument is the name of a ``faker`` provider: .. code-block:: python @@ -721,7 +721,7 @@ Faker .. classmethod:: add_provider(cls, locale=None) - Some projects may need to fake fields beyond those provided by ``fake-factory``; + Some projects may need to fake fields beyond those provided by ``faker``; in such cases, use :meth:`factory.Faker.add_provider` to declare additional providers for those fields: From a6e7790b857f27143d412007315ed22b1c82b43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sean=20L=C3=B6fgren?= Date: Sun, 15 Jan 2017 22:14:52 -0600 Subject: [PATCH 208/714] Rename `fake-factory` reference in README (#343) --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 633c2284..858f369c 100644 --- a/README.rst +++ b/README.rst @@ -187,7 +187,7 @@ Realistic, random values """""""""""""""""""""""" Demos look better with random yet realistic values; and those realistic values can also help discover bugs. -For this, factory_boy relies on the excellent `fake-factory `_ library: +For this, factory_boy relies on the excellent `faker `_ library: .. code-block:: python From 7b7e5c3a33ea8b6529183b695c3d8fc6eda06e0e Mon Sep 17 00:00:00 2001 From: Puneeth Chaganti Date: Fri, 10 Feb 2017 13:05:34 +0530 Subject: [PATCH 209/714] [#331] Warn users about setting end_date if seed is set (#346) Closes #331 --- factory/fuzzy.py | 16 ++++++++++++++++ tests/test_fuzzy.py | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 99df591f..905d9d52 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -29,12 +29,21 @@ import random import string import datetime +import warnings from . import compat from . import declarations _random = random.Random() +_random.state_set = False + + +random_seed_warning = ( + "Setting a specific random seed for {} can still have varying results " + "unless you also set a specific end date. For details and potential solutions " + "see https://github.com/FactoryBoy/factory_boy/issues/331" +) def get_random_state(): @@ -44,6 +53,7 @@ def get_random_state(): def set_random_state(state): """Force-set the state of factory.fuzzy's random generator.""" + _random.state_set = True return _random.setstate(state) @@ -190,6 +200,9 @@ class FuzzyDate(BaseFuzzyAttribute): def __init__(self, start_date, end_date=None, **kwargs): super(FuzzyDate, self).__init__(**kwargs) if end_date is None: + if _random.state_set: + cls_name = self.__class__.__name__ + warnings.warn(random_seed_warning.format(cls_name)) end_date = datetime.date.today() if start_date > end_date: @@ -226,6 +239,9 @@ def __init__(self, start_dt, end_dt=None, super(BaseFuzzyDateTime, self).__init__(**kwargs) if end_dt is None: + if _random.state_set: + cls_name = self.__class__.__name__ + warnings.warn(random_seed_warning.format(cls_name)) end_dt = self._now() self._check_bounds(start_dt, end_dt) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 4c3873a9..18bbe94d 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -23,6 +23,7 @@ import datetime import decimal +import warnings from factory import compat from factory import fuzzy @@ -548,6 +549,13 @@ def test_seeding(self): value2 = fuzz.evaluate(sequence=1, obj=None, create=False) self.assertEqual(value, value2) + def test_seeding_warning(self): + with warnings.catch_warnings(record=True) as w: + fuzz = fuzzy.FuzzyDate(datetime.date(2013, 1, 1)) + fuzz.evaluate(None, None, None) + self.assertEqual(1, len(w)) + self.assertIn('factory_boy/issues/331', str(w[-1].message)) + def test_reset_state(self): fuzz = fuzzy.FuzzyInteger(1, 1000) From 325b8d7b95fb337b9115fb55c575b8679d0bceb0 Mon Sep 17 00:00:00 2001 From: QuantumGhost Date: Sat, 18 Feb 2017 01:23:01 +0800 Subject: [PATCH 210/714] Add documentation for `sqlalchemy_session_persistence` (#349) --- docs/orms.rst | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 73646827..7a09348a 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -319,10 +319,28 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. - .. attribute:: force_flush + .. attribute:: sqlalchemy_session_persistence + + Control the action taken by sqlalchemy session at the end of a create call. + + Valid values are: + + * ``None``: do nothing + * ``'flush'``: perform a session :meth:`~sqlalchemy.orm.session.Session.flush` + * ``'commit'``: perform a session :meth:`~sqlalchemy.orm.session.Session.commit` + + The default value is ``None``. + + If ``force_flush`` is set to ``True``, it overrides this option. - Force a session flush() at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + .. attribute:: force_flush + Force a session ``flush()`` at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. + + .. note:: + + This option is deprecated. Use ``sqlalchemy_session_persistence`` instead. + A (very) simple example: .. code-block:: python From af7b4b6c8eb5478fbc3e80d8e57d958b5f974043 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Fri, 17 Feb 2017 09:26:42 -0800 Subject: [PATCH 211/714] Use SQLAlchemy 'latest' rather than 0.9 for intersphinx --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2cbb1bb7..d9b98d6c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -251,7 +251,7 @@ def get_version(*module_dir_components): 'http://docs.djangoproject.com/en/dev/_objects/', ), 'sqlalchemy': ( - 'http://docs.sqlalchemy.org/en/rel_0_9/', - 'http://docs.sqlalchemy.org/en/rel_0_9/objects.inv', + 'http://docs.sqlalchemy.org/en/latest/', + 'http://docs.sqlalchemy.org/en/latest/objects.inv', ), } From 65565768ebb78fe2f0c5bc665a92b0b66615e45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 25 Feb 2017 19:15:29 +0100 Subject: [PATCH 212/714] Clarify iterator recycling. When using @factory.iterator, the values from the underlying generator will be saved in memory, and reused once the initial generator has reached its end. This means that, if the generator relies on external information (e.g datetime.now()), generated values won't be recomputed when that external information is changed. To avoid this, users should use the ``factory.Iterator(func, cycle=False)`` pattern, and design the target function for never ending. Related to issue #344. --- docs/reference.rst | 10 ++++++++++ tests/test_using.py | 17 +++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 388f5212..94677a35 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1344,6 +1344,7 @@ When a value is passed in for the argument, the iterator will *not* be advanced: >>> UserFactory().lang 'fr' + .. _iterator-getter: Getter @@ -1389,6 +1390,15 @@ use the :func:`iterator` decorator: yield line +.. warning:: Values from the underlying iterator are *kept* in memory; once the + initial iterator has been emptied, saved values are used instead of + executing the function instead. + + Use ``factory.Iterator(my_func, cycle=False)`` to disable value + recycling. + + + Resetting ~~~~~~~~~ diff --git a/tests/test_using.py b/tests/test_using.py index 0f7f3040..2bd679a1 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -21,6 +21,7 @@ """Tests using factory.""" +import datetime import functools import os import sys @@ -1681,6 +1682,22 @@ class Meta: obj = TestObjectFactory() self.assertEqual(1, obj.one) + def test_iterator_time_manipulation(self): + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + @factory.iterator + def one(): + now = datetime.datetime.now() + yield now + datetime.timedelta(hours=1) + yield now + datetime.timedelta(hours=2) + + obj1, obj2, obj3 = TestObjectFactory.create_batch(3) + # Timers should be t+1H, t+2H, t+1H, t+2H, etc. + self.assertEqual(datetime.timedelta(hours=1), obj2.one - obj1.one) + self.assertEqual(obj1.one, obj3.one) + class BetterFakeModelManager(object): def __init__(self, keys, instance): From 4aaa21e6520c686085782b41028631eab771bea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 15:17:42 +0200 Subject: [PATCH 213/714] Simplify copyright headers. --- LICENSE | 1 + examples/flask_alchemy/demoapp.py | 20 +------------------- factory/__init__.py | 21 +-------------------- factory/alchemy.py | 20 +------------------- factory/base.py | 21 +-------------------- factory/compat.py | 21 +-------------------- factory/containers.py | 21 +-------------------- factory/declarations.py | 21 +-------------------- factory/django.py | 21 +-------------------- factory/errors.py | 20 +------------------- factory/faker.py | 21 +-------------------- factory/fuzzy.py | 21 +-------------------- factory/helpers.py | 21 +-------------------- factory/mogo.py | 21 +-------------------- factory/mongoengine.py | 21 +-------------------- factory/utils.py | 21 +-------------------- tests/__init__.py | 2 +- tests/alchemyapp/models.py | 20 +------------------- tests/compat.py | 20 +------------------- tests/cyclic/bar.py | 20 +------------------- tests/cyclic/foo.py | 20 +------------------- tests/cyclic/self_ref.py | 20 +------------------- tests/djapp/models.py | 20 +------------------- tests/djapp/settings.py | 21 ++------------------- tests/test_alchemy.py | 20 +------------------- tests/test_base.py | 21 +-------------------- tests/test_containers.py | 21 +-------------------- tests/test_declarations.py | 21 +-------------------- tests/test_django.py | 21 ++------------------- tests/test_faker.py | 21 +-------------------- tests/test_fuzzy.py | 21 +-------------------- tests/test_helpers.py | 21 +-------------------- tests/test_mongoengine.py | 20 +------------------- tests/test_using.py | 21 ++------------------- tests/test_utils.py | 21 +-------------------- tests/testdata/__init__.py | 20 +------------------- tests/tools.py | 21 +-------------------- tests/utils.py | 21 +-------------------- 38 files changed, 41 insertions(+), 706 deletions(-) diff --git a/LICENSE b/LICENSE index d009218a..a9cfab38 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ Copyright (c) 2010 Mark Sandstrom Copyright (c) 2011-2015 Raphaël Barrois +Copyright (c) The FactoryBoy project Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py index 4ab42b0c..83b2e8e6 100644 --- a/examples/flask_alchemy/demoapp.py +++ b/examples/flask_alchemy/demoapp.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from flask import Flask from flask.ext.sqlalchemy import SQLAlchemy diff --git a/factory/__init__.py b/factory/__init__.py index 09bbab78..99b58d58 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from .base import ( Factory, diff --git a/factory/alchemy.py b/factory/alchemy.py index f3e4a282..d3e7ba66 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -1,23 +1,5 @@ # -*- 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. +# Copyright: See the LICENSE file. from __future__ import unicode_literals diff --git a/factory/base.py b/factory/base.py index 212f9d66..26cced3c 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from __future__ import unicode_literals diff --git a/factory/compat.py b/factory/compat.py index 77f967fe..051c7dfe 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Compatibility tools""" diff --git a/factory/containers.py b/factory/containers.py index ef749ba4..8e4c2e7e 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from __future__ import unicode_literals diff --git a/factory/declarations.py b/factory/declarations.py index 92059757..4ed5203d 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from __future__ import unicode_literals diff --git a/factory/django.py b/factory/django.py index a7f6c780..8eef6837 100644 --- a/factory/django.py +++ b/factory/django.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """factory_boy extensions for use with the Django framework.""" diff --git a/factory/errors.py b/factory/errors.py index cd4bed8b..aecdadf1 100644 --- a/factory/errors.py +++ b/factory/errors.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. class FactoryError(Exception): diff --git a/factory/faker.py b/factory/faker.py index 5b8ff7db..edc90762 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Additional declarations for "faker" attributes. diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 905d9d52..27d9f69c 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Additional declarations for "fuzzy" attribute definitions.""" diff --git a/factory/helpers.py b/factory/helpers.py index 6a0462a2..15e84711 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Simple wrappers around Factory class definition.""" diff --git a/factory/mogo.py b/factory/mogo.py index 284dc556..9c167a79 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """factory_boy extensions for use with the mogo library (pymongo wrapper).""" diff --git a/factory/mongoengine.py b/factory/mongoengine.py index c6559ebc..ce430bf7 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """factory_boy extensions for use with the mongoengine library (pymongo wrapper).""" diff --git a/factory/utils.py b/factory/utils.py index 26363512..e38c8622 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from __future__ import unicode_literals diff --git a/tests/__init__.py b/tests/__init__.py index b2c772d4..a21e445c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois +# Copyright: See the LICENSE file. # factory.django needs a configured Django. from .test_django import * diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index e0193d46..88708fd5 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -1,23 +1,5 @@ # -*- 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. +# Copyright: See the LICENSE file. """Helpers for testing SQLAlchemy apps.""" diff --git a/tests/compat.py b/tests/compat.py index 167c1851..364d0513 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Compatibility tools for tests""" diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index b4f8e0c8..4cd7c79d 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Helper to test circular factory dependencies.""" diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index 62e58c04..a9e7c958 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Helper to test circular factory dependencies.""" diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py index d98b3abc..0384bcc2 100644 --- a/tests/cyclic/self_ref.py +++ b/tests/cyclic/self_ref.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Helper to test circular factory dependencies.""" diff --git a/tests/djapp/models.py b/tests/djapp/models.py index b7cad0d7..3a925e39 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. """Helpers for testing django apps.""" diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index 1ef16d5e..fb91d2cf 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -1,23 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. + """Settings for factory_boy/Django tests.""" import os diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index ea39c90f..1598e313 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2015 Romain Command& -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# 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. +# Copyright: See the LICENSE file. """Tests for factory_boy/SQLAlchemy interactions.""" diff --git a/tests/test_base.py b/tests/test_base.py index a3b3704c..b9d65ba2 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import warnings diff --git a/tests/test_containers.py b/tests/test_containers.py index 1b87b2ec..8b2e5746 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from factory import base from factory import containers diff --git a/tests/test_declarations.py b/tests/test_declarations.py index cd8c6bd0..9c124780 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import datetime import itertools diff --git a/tests/test_django.py b/tests/test_django.py index c471f754..4350ac25 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1,23 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. + """Tests for factory_boy/Django interactions.""" import os diff --git a/tests/test_faker.py b/tests/test_faker.py index 99e54afd..8b08eb87 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import unittest diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 18bbe94d..7cd100e1 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import datetime diff --git a/tests/test_helpers.py b/tests/test_helpers.py index bee66caf..b81613d8 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import logging diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 148d2744..56cf2481 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -1,23 +1,5 @@ # -*- 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. +# Copyright: See the LICENSE file. """Tests for factory_boy/MongoEngine interactions.""" diff --git a/tests/test_using.py b/tests/test_using.py index 2bd679a1..57b68b59 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1,23 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. + """Tests using factory.""" diff --git a/tests/test_utils.py b/tests/test_utils.py index 77598e16..05187fa1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. from __future__ import unicode_literals diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py index b534998a..9a9e6845 100644 --- a/tests/testdata/__init__.py +++ b/tests/testdata/__init__.py @@ -1,23 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import os.path diff --git a/tests/tools.py b/tests/tools.py index 47f705c1..42598530 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import functools diff --git a/tests/utils.py b/tests/utils.py index 7a31ed2a..5b7ed9a7 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,24 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011-2015 Raphaël Barrois -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. +# Copyright: See the LICENSE file. import datetime From 79dacef0d1938e91ff094cb857e51e2d0544d808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 15:19:01 +0200 Subject: [PATCH 214/714] Add credits. Note: I wasn't able to fetch a contributor license agreement from all past contributors yet. --- CREDITS | 108 +++++++++++++++++++++++++++++++++++++++++++++++ docs/credits.rst | 1 + docs/index.rst | 1 + 3 files changed, 110 insertions(+) create mode 100644 CREDITS create mode 120000 docs/credits.rst diff --git a/CREDITS b/CREDITS new file mode 100644 index 00000000..3405f01f --- /dev/null +++ b/CREDITS @@ -0,0 +1,108 @@ +Credits +======= + + +Maintainers +----------- + +The ``factory_boy`` project is operated and maintained by: + +* Jeff Widman (https://github.com/jeffwidman) +* Raphaël Barrois (https://github.com/rbarrois) + + +.. _contributors: + +Contributors +------------ + +The project was initially created by Mark Sandstrom . + + +The project has received contributions from (in alphabetical order): + +* Adam Chainz +* Alejandro +* Alexey Kotlyarov +* Amit Shah +* Andrey Voronov +* Branko Majic +* Carl Meyer +* Chris Lasher +* Chris Seto +* Christoph Sieghart +* David Baumgold +* Eduard Iskandarov +* Flavio Curella +* George Hickman +* Hervé Cauwelier +* Ilya Baryshev +* Ilya Pirogov +* Ionuț Arțăriși +* Issa Jubril +* Ivan Miric +* Janusz Skonieczny +* Jeff Widman (https://github.com/jeffwidman) +* Jon Dufresne +* Jonathan Tushman +* Joshua Carp +* Leonardo Lazzaro +* Luke GB +* Marc Abramowitz +* Mark Sandstrom +* Michael Joseph +* Mikhail Korobov +* Oleg Pidsadnyi +* Omer +* Pauly Fenwar +* Peter Marsh +* Puneeth Chaganti +* QuantumGhost +* Raphaël Barrois (https://github.com/rbarrois) +* Rich Rauenzahn +* Richard Moch +* Rob Zyskowski +* Robrecht De Rouck +* Samuel Paccoud +* Saul Shanabrook +* Sean Löfgren +* Tom +* alex-netquity +* anentropic +* minimumserious +* mluszczyk +* nkryptic +* obiwanus +* tsouvarev +* yamaneko + + + +Contributor license agreement +----------------------------- + +.. note:: This agreement is required to allow redistribution of submitted contributions. + See http://oss-watch.ac.uk/resources/cla for an explanation. + +Any contributor proposing updates to the code or documentation of this project *MUST* +add its name to the list in the :ref:`contributors` section, thereby "signing" the +following contributor license agreement: + +They accept and agree to the following terms for their present end future contributions +submitted to the ``factory_boy`` project: + +* They represent that they are legally entitled to grant this license, and that their + contributions are their original creation + +* They grant the ``factory_boy`` project a perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable copyright license to reproduce, + prepare derivative works of, publicly display, sublicense and distribute their contributions + and such derivative works. + +* They are not expected to provide support for their contributions, except to the extent they + desire to provide support. + + +.. note:: The above agreement is inspired by the Apache Contributor License Agreement. + +.. vim:set ft=rst: diff --git a/docs/credits.rst b/docs/credits.rst new file mode 120000 index 00000000..c7a76417 --- /dev/null +++ b/docs/credits.rst @@ -0,0 +1 @@ +../CREDITS \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 0d9e4374..7d9562a9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Contents, indices and tables examples internals changelog + credits ideas * :ref:`genindex` From e426ab973b3dbcc5302a32b457519ee13574dcf4 Mon Sep 17 00:00:00 2001 From: Zahim Anass Date: Thu, 6 Apr 2017 12:34:45 +0200 Subject: [PATCH 215/714] Use the same random seed from fuzzy in faker. #closes 275 - Functions such as `get_random_state` are both used by `factory.fuzzy` and `factory.faker`, it makes more sense to have them moved to a separate module : `random.py` - Adding depracation warnings for random state functions moved from `factory.fuzzy` to `factory.random`. - Updating changelog. --- .gitignore | 1 + CREDITS | 1 + docs/changelog.rst | 13 +++++++++++ factory/faker.py | 5 +++- factory/fuzzy.py | 56 ++++++++++++++++++++++++++------------------- factory/random.py | 24 +++++++++++++++++++ tests/test_faker.py | 3 ++- tests/test_fuzzy.py | 37 +++++++++++++++--------------- tests/test_using.py | 22 ++++++++++++++++++ 9 files changed, 118 insertions(+), 44 deletions(-) create mode 100644 factory/random.py diff --git a/.gitignore b/.gitignore index 5437c432..bed0ddc2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .*.swp *.pyc *.pyo +.idea/ # Build-related files docs/_build/ diff --git a/CREDITS b/CREDITS index 3405f01f..47e1801a 100644 --- a/CREDITS +++ b/CREDITS @@ -25,6 +25,7 @@ The project has received contributions from (in alphabetical order): * Alejandro * Alexey Kotlyarov * Amit Shah +* Anas Zahim (https://github.com/kamotos) * Andrey Voronov * Branko Majic * Carl Meyer diff --git a/docs/changelog.rst b/docs/changelog.rst index ccfa89d2..7df5ea7e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,19 @@ ChangeLog ========= +master +------ + +*New:* + + - :issue:`275`: `factory.fuzzy` and `factory.faker` now use the same random seed. + +*Deprecation:* + + - `factory.fuzzy.get_random_state` is deprecated, `factory.random.get_random_state` should be used instead. + - `factory.fuzzy.set_random_state` is deprecated, `factory.random.set_random_state` should be used instead. + - `factory.fuzzy.reseed_random` is deprecated, `factory.random.reseed_random` should be used instead. + .. _v2.8.1: 2.8.1 (2016-12-17) diff --git a/factory/faker.py b/factory/faker.py index edc90762..feb7fa29 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -22,6 +22,7 @@ class Meta: import faker import faker.config +from .random import get_random_state from . import declarations @@ -73,8 +74,10 @@ def _get_faker(cls, locale=None): locale = cls._DEFAULT_LOCALE if locale not in cls._FAKER_REGISTRY: - cls._FAKER_REGISTRY[locale] = faker.Faker(locale=locale) + subfaker = faker.Faker(locale=locale) + cls._FAKER_REGISTRY[locale] = subfaker + cls._FAKER_REGISTRY[locale].random.setstate(get_random_state()) return cls._FAKER_REGISTRY[locale] @classmethod diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 27d9f69c..122c546f 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -6,19 +6,14 @@ from __future__ import unicode_literals +import datetime import decimal -import random import string -import datetime import warnings from . import compat from . import declarations - - -_random = random.Random() -_random.state_set = False - +from . import random random_seed_warning = ( "Setting a specific random seed for {} can still have varying results " @@ -28,20 +23,33 @@ def get_random_state(): - """Retrieve the state of factory.fuzzy's random generator.""" - return _random.getstate() + warnings.warn( + "`factory.fuzzy.get_random_state` is deprecated. " + "You should use `factory.random.get_random_state` instead", + DeprecationWarning, + stacklevel=2 + ) + return random.get_random_state() def set_random_state(state): - """Force-set the state of factory.fuzzy's random generator.""" - _random.state_set = True - return _random.setstate(state) + warnings.warn( + "`factory.fuzzy.set_random_state` is deprecated. " + "You should use `factory.random.set_random_state` instead", + DeprecationWarning, + stacklevel=2 + ) + return random.set_random_state(state) def reseed_random(seed): - """Reseed factory.fuzzy's random generator.""" - r = random.Random(seed) - set_random_state(r.getstate()) + warnings.warn( + "`factory.fuzzy.set_random_state` is deprecated. " + "You should use `factory.random.reseed_random` instead", + DeprecationWarning, + stacklevel=2 + ) + random.reseed_random(seed) class BaseFuzzyAttribute(declarations.OrderedDeclaration): @@ -98,7 +106,7 @@ def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters, self.chars = tuple(chars) # Unroll iterators def fuzz(self): - chars = [_random.choice(self.chars) for _i in range(self.length)] + chars = [random.randgen.choice(self.chars) for _i in range(self.length)] return self.prefix + ''.join(chars) + self.suffix @@ -118,7 +126,7 @@ def __init__(self, choices, **kwargs): def fuzz(self): if self.choices is None: self.choices = list(self.choices_generator) - return _random.choice(self.choices) + return random.randgen.choice(self.choices) class FuzzyInteger(BaseFuzzyAttribute): @@ -136,7 +144,7 @@ def __init__(self, low, high=None, step=1, **kwargs): super(FuzzyInteger, self).__init__(**kwargs) def fuzz(self): - return _random.randrange(self.low, self.high + 1, self.step) + return random.randgen.randrange(self.low, self.high + 1, self.step) class FuzzyDecimal(BaseFuzzyAttribute): @@ -154,7 +162,7 @@ def __init__(self, low, high=None, precision=2, **kwargs): super(FuzzyDecimal, self).__init__(**kwargs) def fuzz(self): - base = decimal.Decimal(str(_random.uniform(self.low, self.high))) + base = decimal.Decimal(str(random.randgen.uniform(self.low, self.high))) return base.quantize(decimal.Decimal(10) ** -self.precision) @@ -172,7 +180,7 @@ def __init__(self, low, high=None, **kwargs): super(FuzzyFloat, self).__init__(**kwargs) def fuzz(self): - return _random.uniform(self.low, self.high) + return random.randgen.uniform(self.low, self.high) class FuzzyDate(BaseFuzzyAttribute): @@ -181,7 +189,7 @@ class FuzzyDate(BaseFuzzyAttribute): def __init__(self, start_date, end_date=None, **kwargs): super(FuzzyDate, self).__init__(**kwargs) if end_date is None: - if _random.state_set: + if random.randgen.state_set: cls_name = self.__class__.__name__ warnings.warn(random_seed_warning.format(cls_name)) end_date = datetime.date.today() @@ -195,7 +203,7 @@ def __init__(self, start_date, end_date=None, **kwargs): self.end_date = end_date.toordinal() def fuzz(self): - return datetime.date.fromordinal(_random.randint(self.start_date, self.end_date)) + return datetime.date.fromordinal(random.randgen.randint(self.start_date, self.end_date)) class BaseFuzzyDateTime(BaseFuzzyAttribute): @@ -220,7 +228,7 @@ def __init__(self, start_dt, end_dt=None, super(BaseFuzzyDateTime, self).__init__(**kwargs) if end_dt is None: - if _random.state_set: + if random.randgen.state_set: cls_name = self.__class__.__name__ warnings.warn(random_seed_warning.format(cls_name)) end_dt = self._now() @@ -241,7 +249,7 @@ def fuzz(self): delta = self.end_dt - self.start_dt microseconds = delta.microseconds + 1000000 * (delta.seconds + (delta.days * 86400)) - offset = _random.randint(0, microseconds) + offset = random.randgen.randint(0, microseconds) result = self.start_dt + datetime.timedelta(microseconds=offset) if self.force_year is not None: diff --git a/factory/random.py b/factory/random.py new file mode 100644 index 00000000..88a5d9e8 --- /dev/null +++ b/factory/random.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import + +import random + +randgen = random.Random() + +randgen.state_set = False + + +def get_random_state(): + """Retrieve the state of factory.fuzzy's random generator.""" + return randgen.getstate() + + +def set_random_state(state): + """Force-set the state of factory.fuzzy's random generator.""" + randgen.state_set = True + return randgen.setstate(state) + + +def reseed_random(seed): + """Reseed factory.fuzzy's random generator.""" + r = random.Random(seed) + set_random_state(r.getstate()) diff --git a/tests/test_faker.py b/tests/test_faker.py index 8b08eb87..14f9fa63 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright: See the LICENSE file. - +import random import unittest import faker.providers @@ -12,6 +12,7 @@ class MockFaker(object): def __init__(self, expected): self.expected = expected + self.random = random.Random() def format(self, provider, **kwargs): return self.expected[provider] diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 7cd100e1..9fa7f6a6 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -8,6 +8,7 @@ from factory import compat from factory import fuzzy +from factory import random from .compat import mock, unittest from . import utils @@ -37,7 +38,7 @@ def test_mock(self): d = fuzzy.FuzzyChoice(options) - with mock.patch('factory.fuzzy._random.choice', fake_choice): + with mock.patch('factory.random.randgen.choice', fake_choice): res = d.evaluate(2, None, False) self.assertEqual(6, res) @@ -93,7 +94,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyInteger(2, 8) - with mock.patch('factory.fuzzy._random.randrange', fake_randrange): + with mock.patch('factory.random.randgen.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((2 + 8 + 1) * 1, res) @@ -103,7 +104,7 @@ def test_biased_high_only(self): fuzz = fuzzy.FuzzyInteger(8) - with mock.patch('factory.fuzzy._random.randrange', fake_randrange): + with mock.patch('factory.random.randgen.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((0 + 8 + 1) * 1, res) @@ -113,7 +114,7 @@ def test_biased_with_step(self): fuzz = fuzzy.FuzzyInteger(5, 8, 3) - with mock.patch('factory.fuzzy._random.randrange', fake_randrange): + with mock.patch('factory.random.randgen.randrange', fake_randrange): res = fuzz.evaluate(2, None, False) self.assertEqual((5 + 8 + 1) * 3, res) @@ -146,7 +147,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyDecimal(2.0, 8.0) - with mock.patch('factory.fuzzy._random.uniform', fake_uniform): + with mock.patch('factory.random.randgen.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('10.0'), res) @@ -156,7 +157,7 @@ def test_biased_high_only(self): fuzz = fuzzy.FuzzyDecimal(8.0) - with mock.patch('factory.fuzzy._random.uniform', fake_uniform): + with mock.patch('factory.random.randgen.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('8.0'), res) @@ -166,7 +167,7 @@ def test_precision(self): fuzz = fuzzy.FuzzyDecimal(8.0, precision=3) - with mock.patch('factory.fuzzy._random.uniform', fake_uniform): + with mock.patch('factory.random.randgen.uniform', fake_uniform): res = fuzz.evaluate(2, None, False) self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) @@ -227,7 +228,7 @@ def test_biased(self): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31) - with mock.patch('factory.fuzzy._random.randint', fake_randint): + with mock.patch('factory.random.randgen.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.date(2013, 1, 16), res) @@ -238,7 +239,7 @@ def test_biased_partial(self): fuzz = fuzzy.FuzzyDate(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('factory.fuzzy._random.randint', fake_randint): + with mock.patch('factory.random.randgen.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.date(2013, 1, 2), res) @@ -345,7 +346,7 @@ def test_biased(self): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31) - with mock.patch('factory.fuzzy._random.randint', fake_randint): + with mock.patch('factory.random.randgen.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 16), res) @@ -356,7 +357,7 @@ def test_biased_partial(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('factory.fuzzy._random.randint', fake_randint): + with mock.patch('factory.random.randgen.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2), res) @@ -463,7 +464,7 @@ def test_biased(self): fake_randint = lambda low, high: (low + high) // 2 fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31) - with mock.patch('factory.fuzzy._random.randint', fake_randint): + with mock.patch('factory.random.randgen.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res) @@ -474,7 +475,7 @@ def test_biased_partial(self): fuzz = fuzzy.FuzzyDateTime(self.jan1) fake_randint = lambda low, high: (low + high) // 2 - with mock.patch('factory.fuzzy._random.randint', fake_randint): + with mock.patch('factory.random.randgen.randint', fake_randint): res = fuzz.evaluate(2, None, False) self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res) @@ -499,7 +500,7 @@ def test_mock(self): chars = ['a', 'b', 'c'] fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4) - with mock.patch('factory.fuzzy._random.choice', fake_choice): + with mock.patch('factory.random.randgen.choice', fake_choice): res = fuzz.evaluate(2, None, False) self.assertEqual('preaaaapost', res) @@ -523,10 +524,10 @@ class FuzzyRandomTestCase(unittest.TestCase): def test_seeding(self): fuzz = fuzzy.FuzzyInteger(1, 1000) - fuzzy.reseed_random(42) + random.reseed_random(42) value = fuzz.evaluate(sequence=1, obj=None, create=False) - fuzzy.reseed_random(42) + random.reseed_random(42) value2 = fuzz.evaluate(sequence=1, obj=None, create=False) self.assertEqual(value, value2) @@ -540,9 +541,9 @@ def test_seeding_warning(self): def test_reset_state(self): fuzz = fuzzy.FuzzyInteger(1, 1000) - state = fuzzy.get_random_state() + state = random.get_random_state() value = fuzz.evaluate(sequence=1, obj=None, create=False) - fuzzy.set_random_state(state) + random.set_random_state(state) value2 = fuzz.evaluate(sequence=1, obj=None, create=False) self.assertEqual(value, value2) diff --git a/tests/test_using.py b/tests/test_using.py index 57b68b59..ad11006c 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2205,6 +2205,28 @@ def test_example(self): self.assertIsNone(b.foo.bar.foo.bar) +class RepeatableRandomSeedFakerTests(unittest.TestCase): + def test_same_seed_is_used_between_fuzzy_and_faker_generators(self): + class StudentFactory(factory.Factory): + one = factory.fuzzy.FuzzyDate(datetime.date(1950, 1, 1), ) + two = factory.Faker('name') + three = factory.Faker('name', locale='it') + + class Meta: + model = TestObject + + seed = "seed1" + factory.random.reseed_random(seed) + students_1 = (StudentFactory(), StudentFactory()) + + factory.random.reseed_random(seed) + students_2 = (StudentFactory(), StudentFactory()) + + self.assertEqual(students_1[0].one, students_2[0].one) + self.assertEqual(students_1[0].two, students_2[0].two) + self.assertEqual(students_1[0].three, students_2[0].three) + + class SelfReferentialTests(unittest.TestCase): def test_no_parent(self): from .cyclic import self_ref From 1c53f211cb97ffd9a55b9530dbd9d1f2e28d7ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 15:54:04 +0200 Subject: [PATCH 216/714] Fix MANIFEST.in. --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 5f83b75a..8156ca2f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include ChangeLog LICENSE README.rst +include ChangeLog CREDITS LICENSE README.rst include requirements*.txt graft factory From 23dbafaf018494965afa512bfb84602913360bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 17:09:58 +0200 Subject: [PATCH 217/714] Refactor example testing. Use a per-project runtests.sh file. --- examples/Makefile | 2 +- examples/flask_alchemy/runtests.sh | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100755 examples/flask_alchemy/runtests.sh diff --git a/examples/Makefile b/examples/Makefile index 6064a9bc..0beb2c9f 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -6,4 +6,4 @@ test: $(TEST_TARGETS) $(TEST_TARGETS): runtest-%: - cd $* && PYTHONPATH=../.. python -m unittest + cd $* && ./runtests.sh diff --git a/examples/flask_alchemy/runtests.sh b/examples/flask_alchemy/runtests.sh new file mode 100755 index 00000000..f1a6b5d8 --- /dev/null +++ b/examples/flask_alchemy/runtests.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +cd $(dirname $0) +for f in test_*.py; do + python "$f"; +done From c35c17e91880c29bf1082b7488b4a848e64d23be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20B=C3=A4chtold?= Date: Fri, 7 Apr 2017 11:31:18 +0200 Subject: [PATCH 218/714] Add support for Python 3.6. In fact, this makes all the tests be run under Python 3.6. If the tests succeed, we assume there is support for Python 3.6 Closes https://github.com/FactoryBoy/factory_boy/issues/358 --- .travis.yml | 6 ++++-- CREDITS | 1 + README.rst | 2 +- setup.py | 1 + tox.ini | 2 +- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c86b524..0abcf912 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,15 +15,17 @@ matrix: env: TOXENV=py34-django110-alchemy10-mongoengine010 - python: "3.5" env: TOXENV=py35-django110-alchemy10-mongoengine010 + - python: "3.6" + env: TOXENV=py36-django110-alchemy10-mongoengine010 # Pypy - python: "pypy" env: TOXENV=py27-django110-alchemy10-mongoengine010 # Linting - - python: "3.5" + - python: "3.6" env: TOXENV=examples - - python: "3.5" + - python: "3.6" env: TOXENV=lint services: diff --git a/CREDITS b/CREDITS index 47e1801a..f058fa28 100644 --- a/CREDITS +++ b/CREDITS @@ -51,6 +51,7 @@ The project has received contributions from (in alphabetical order): * Luke GB * Marc Abramowitz * Mark Sandstrom +* Martin Bächtold (https://github.com/mbaechtold) * Michael Joseph * Mikhail Korobov * Oleg Pidsadnyi diff --git a/README.rst b/README.rst index 858f369c..5e17d8cf 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.2 to 3.5, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.7, 3.2 to 3.6, as well as PyPy; it requires only the standard Python library. Download diff --git a/setup.py b/setup.py index a219669d..04aa183a 100755 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ def get_version(package_name): "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tox.ini b/tox.ini index 90ad6e10..973fc444 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35}-django{17,18,19,110}-alchemy10-mongoengine010 + py{27,34,35,36}-django{17,18,19,110}-alchemy10-mongoengine010 examples lint From b0db4d0e5e60e7059caa9c4161fb8cfe4734241a Mon Sep 17 00:00:00 2001 From: Demur Nodia Date: Fri, 7 Apr 2017 14:17:34 +0200 Subject: [PATCH 219/714] Django examples (#356) * Add generic fk example * add runtests script for django_demo * Hook django_demo requirements in examples --- CREDITS | 1 + examples/Makefile | 2 +- examples/django_demo/django_demo/__init__.py | 0 examples/django_demo/django_demo/settings.py | 121 ++++++++++++++++++ examples/django_demo/django_demo/urls.py | 21 +++ examples/django_demo/django_demo/wsgi.py | 16 +++ .../generic_foreignkey/__init__.py | 0 .../django_demo/generic_foreignkey/apps.py | 7 + .../generic_foreignkey/factories.py | 43 +++++++ .../migrations/0001_initial.py | 27 ++++ .../generic_foreignkey/migrations/__init__.py | 0 .../django_demo/generic_foreignkey/models.py | 17 +++ .../django_demo/generic_foreignkey/tests.py | 28 ++++ examples/django_demo/manage.py | 22 ++++ examples/django_demo/requirements.txt | 1 + examples/django_demo/runtests.sh | 4 + examples/requirements.txt | 1 + 17 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 examples/django_demo/django_demo/__init__.py create mode 100644 examples/django_demo/django_demo/settings.py create mode 100644 examples/django_demo/django_demo/urls.py create mode 100644 examples/django_demo/django_demo/wsgi.py create mode 100644 examples/django_demo/generic_foreignkey/__init__.py create mode 100644 examples/django_demo/generic_foreignkey/apps.py create mode 100644 examples/django_demo/generic_foreignkey/factories.py create mode 100644 examples/django_demo/generic_foreignkey/migrations/0001_initial.py create mode 100644 examples/django_demo/generic_foreignkey/migrations/__init__.py create mode 100644 examples/django_demo/generic_foreignkey/models.py create mode 100644 examples/django_demo/generic_foreignkey/tests.py create mode 100755 examples/django_demo/manage.py create mode 100644 examples/django_demo/requirements.txt create mode 100755 examples/django_demo/runtests.sh diff --git a/CREDITS b/CREDITS index f058fa28..07be3f58 100644 --- a/CREDITS +++ b/CREDITS @@ -33,6 +33,7 @@ The project has received contributions from (in alphabetical order): * Chris Seto * Christoph Sieghart * David Baumgold +* Demur Nodia (https://github.com/demonno) * Eduard Iskandarov * Flavio Curella * George Hickman diff --git a/examples/Makefile b/examples/Makefile index 0beb2c9f..c99a25a4 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -1,4 +1,4 @@ -EXAMPLES = flask_alchemy +EXAMPLES = django_demo flask_alchemy TEST_TARGETS = $(addprefix runtest-,$(EXAMPLES)) diff --git a/examples/django_demo/django_demo/__init__.py b/examples/django_demo/django_demo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/django_demo/django_demo/settings.py b/examples/django_demo/django_demo/settings.py new file mode 100644 index 00000000..5135faf9 --- /dev/null +++ b/examples/django_demo/django_demo/settings.py @@ -0,0 +1,121 @@ +""" +Django settings for django_demo project. + +Generated by 'django-admin startproject' using Django 1.10. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.10/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'kh)1s3@93ju6f6$qx!758f6h^(_3d0brqzoxubo@xsn3*%2wgu' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'generic_foreignkey' +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'django_demo.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'django_demo.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.10/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.10/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/examples/django_demo/django_demo/urls.py b/examples/django_demo/django_demo/urls.py new file mode 100644 index 00000000..2bf9b41a --- /dev/null +++ b/examples/django_demo/django_demo/urls.py @@ -0,0 +1,21 @@ +"""django_demo URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/1.10/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.conf.urls import url, include + 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) +""" +from django.conf.urls import url +from django.contrib import admin + +urlpatterns = [ + url(r'^admin/', admin.site.urls), +] diff --git a/examples/django_demo/django_demo/wsgi.py b/examples/django_demo/django_demo/wsgi.py new file mode 100644 index 00000000..3a5bea10 --- /dev/null +++ b/examples/django_demo/django_demo/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for django_demo project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_demo.settings") + +application = get_wsgi_application() diff --git a/examples/django_demo/generic_foreignkey/__init__.py b/examples/django_demo/generic_foreignkey/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/django_demo/generic_foreignkey/apps.py b/examples/django_demo/generic_foreignkey/apps.py new file mode 100644 index 00000000..64526770 --- /dev/null +++ b/examples/django_demo/generic_foreignkey/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class GenericForenikeyConfig(AppConfig): + name = 'generic_foreignkey' diff --git a/examples/django_demo/generic_foreignkey/factories.py b/examples/django_demo/generic_foreignkey/factories.py new file mode 100644 index 00000000..d39da0b8 --- /dev/null +++ b/examples/django_demo/generic_foreignkey/factories.py @@ -0,0 +1,43 @@ +import factory +from django.contrib.auth.models import User, Group +from django.contrib.contenttypes.models import ContentType + +from .models import TaggedItem + + +class UserFactory(factory.DjangoModelFactory): + first_name = 'Adam' + + class Meta: + model = User + + +class GroupFactory(factory.DjangoModelFactory): + name = 'group' + + class Meta: + model = Group + + +class TaggedItemFactory(factory.DjangoModelFactory): + object_id = factory.SelfAttribute('content_object.id') + content_type = factory.LazyAttribute( + lambda o: ContentType.objects.get_for_model(o.content_object)) + + class Meta: + exclude = ['content_object'] + abstract = True + + +class TaggedUserFactory(TaggedItemFactory): + content_object = factory.SubFactory(UserFactory) + + class Meta: + model = TaggedItem + + +class TaggedGroupFactory(TaggedItemFactory): + content_object = factory.SubFactory(GroupFactory) + + class Meta: + model = TaggedItem diff --git a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py new file mode 100644 index 00000000..dbf0b0b7 --- /dev/null +++ b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2017-04-06 14:33 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='TaggedItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tag', models.SlugField()), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + ] diff --git a/examples/django_demo/generic_foreignkey/migrations/__init__.py b/examples/django_demo/generic_foreignkey/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/django_demo/generic_foreignkey/models.py b/examples/django_demo/generic_foreignkey/models.py new file mode 100644 index 00000000..33a8d481 --- /dev/null +++ b/examples/django_demo/generic_foreignkey/models.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +from django.db import models +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey + + +class TaggedItem(models.Model): + """Example GemericForeinKey model from django docs""" + tag = models.SlugField() + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + content_object = GenericForeignKey('content_type', 'object_id') + + def __str__(self): # __unicode__ on Python 2 + return self.tag + diff --git a/examples/django_demo/generic_foreignkey/tests.py b/examples/django_demo/generic_foreignkey/tests.py new file mode 100644 index 00000000..430200ad --- /dev/null +++ b/examples/django_demo/generic_foreignkey/tests.py @@ -0,0 +1,28 @@ +from django.contrib.auth.models import User, Group +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from .factories import UserFactory, GroupFactory, TaggedUserFactory, TaggedGroupFactory + + +class GenericFactoryTest(TestCase): + + def test_user_factory(self): + user = UserFactory() + self.assertEqual(user.first_name, 'Adam') + + def test_group_factory(self): + group = GroupFactory() + self.assertEqual(group.name, 'group') + + def test_generic_user(self): + model = TaggedUserFactory(tag='user') + self.assertEqual(model.tag, 'user') + self.assertTrue(isinstance(model.content_object, User)) + self.assertEqual(model.content_type, ContentType.objects.get_for_model(model.content_object)) + + def test_generic_group(self): + model = TaggedGroupFactory(tag='group') + self.assertEqual(model.tag, 'group') + self.assertTrue(isinstance(model.content_object, Group)) + self.assertEqual(model.content_type, ContentType.objects.get_for_model(model.content_object)) diff --git a/examples/django_demo/manage.py b/examples/django_demo/manage.py new file mode 100755 index 00000000..bc7ced9a --- /dev/null +++ b/examples/django_demo/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_demo.settings") + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + raise + execute_from_command_line(sys.argv) diff --git a/examples/django_demo/requirements.txt b/examples/django_demo/requirements.txt new file mode 100644 index 00000000..94a0e834 --- /dev/null +++ b/examples/django_demo/requirements.txt @@ -0,0 +1 @@ +Django diff --git a/examples/django_demo/runtests.sh b/examples/django_demo/runtests.sh new file mode 100755 index 00000000..0fd70cdb --- /dev/null +++ b/examples/django_demo/runtests.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd $(dirname $0) +python manage.py test; diff --git a/examples/requirements.txt b/examples/requirements.txt index 5e11ca53..ee2e75a0 100644 --- a/examples/requirements.txt +++ b/examples/requirements.txt @@ -1 +1,2 @@ -r flask_alchemy/requirements.txt +-r django_demo/requirements.txt From 66652f116b5c53263fe84c3dfce28151263ad007 Mon Sep 17 00:00:00 2001 From: Zahim Anass Date: Fri, 7 Apr 2017 14:55:23 +0200 Subject: [PATCH 220/714] Adding django 1.11 to tox.ini and .travis.yml --- .travis.yml | 10 +++++----- tox.ini | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0abcf912..58f13fab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,17 +10,17 @@ install: matrix: include: - python: "2.7" - env: TOXENV=py27-django110-alchemy10-mongoengine010 + env: TOXENV=py27-django111-alchemy10-mongoengine010 - python: "3.4" - env: TOXENV=py34-django110-alchemy10-mongoengine010 + env: TOXENV=py34-django111-alchemy10-mongoengine010 - python: "3.5" - env: TOXENV=py35-django110-alchemy10-mongoengine010 + env: TOXENV=py35-django111-alchemy10-mongoengine010 - python: "3.6" - env: TOXENV=py36-django110-alchemy10-mongoengine010 + env: TOXENV=py36-django111-alchemy10-mongoengine010 # Pypy - python: "pypy" - env: TOXENV=py27-django110-alchemy10-mongoengine010 + env: TOXENV=py27-django111-alchemy10-mongoengine010 # Linting - python: "3.6" diff --git a/tox.ini b/tox.ini index 973fc444..88029e3c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36}-django{17,18,19,110}-alchemy10-mongoengine010 + py{27,34,35,36}-django{17,18,19,110,111}-alchemy10-mongoengine010 examples lint @@ -13,7 +13,8 @@ deps = django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 - django{17,18,19,110}: Pillow + django111: Django>=1.11,<1.12 + django{17,18,19,110,111}: Pillow alchemy10: SQLAlchemy>=1.0,<1.1 mongoengine010: mongoengine>=0.10,<0.11 From 607e678126fa85a6ff1d132903c6fafc3734ef58 Mon Sep 17 00:00:00 2001 From: Demur Nodia Date: Fri, 7 Apr 2017 15:19:41 +0200 Subject: [PATCH 221/714] Add generic fk in recepes --- docs/recipes.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index fe18f501..d7817223 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -341,7 +341,7 @@ This time, we want the company owner to live in a country neighboring the countr country = factory.SubFactory(CountryFactory) owner = factory.SubFactory(UserFactory, country=factory.LazyAttribute(lambda o: get_random_neighbor(o.factory_parent.country))) - + Custom manager methods ---------------------- @@ -489,3 +489,14 @@ In order to get a dict, we'll just have to swap the model; the easiest way is to >>> factory.build(dict, FACTORY_CLASS=UserFactory) {'first_name': "Agent 001", 'username': 'john_doe'} + +Django models with `GenericForeignKeys` +-------------------------------------- + +For model which uses `GenericForeignKey `_ + +.. literalinclude:: ../examples/django_demo/generic_foreignkey/models.py + +We can create factories like this: + +.. literalinclude:: ../examples/django_demo/generic_foreignkey/factories.py From 445f40b5e21654e7ba88cea0fca3035cc8fd0276 Mon Sep 17 00:00:00 2001 From: Zahim Anass Date: Fri, 7 Apr 2017 16:04:26 +0200 Subject: [PATCH 222/714] Drop support for Django<1.8 + Adapt to Django deprecation warnings : - Versions lower than Django 1.8 are no more maintained. - `on_cascade` will become required starting from django 2.0 on OneToOneFields. Adding it to one of the django app examples to avoid deprecation warnings. --- tests/djapp/models.py | 5 ++++- tox.ini | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 3a925e39..26674096 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -61,7 +61,10 @@ class PointedModel(models.Model): class PointingModel(models.Model): foo = models.CharField(max_length=20) - pointed = models.OneToOneField(PointedModel, related_name='pointer', null=True) + pointed = models.OneToOneField( + PointedModel, related_name='pointer', null=True, + on_delete=models.CASCADE + ) WITHFILE_UPLOAD_TO = 'django' diff --git a/tox.ini b/tox.ini index 88029e3c..607999d1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36}-django{17,18,19,110,111}-alchemy10-mongoengine010 + py{27,34,35,36}-django{18,19,110,111}-alchemy10-mongoengine010 examples lint @@ -9,12 +9,11 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = -rrequirements_test.txt - django17: Django>=1.7,<1.8 django18: Django>=1.8,<1.9 django19: Django>=1.9,<1.10 django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 - django{17,18,19,110,111}: Pillow + django{18,19,110,111}: Pillow alchemy10: SQLAlchemy>=1.0,<1.1 mongoengine010: mongoengine>=0.10,<0.11 From 0ae90cca4ba69c9d3931f2e2b9cfb2fed77c14a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 15 Jan 2017 21:42:26 +0100 Subject: [PATCH 223/714] tests: switch to public APIs. Declarations were tested using their private APIs; switching to the public API allows to test for complex refactors, and ensures that documentation is accurate. This commit replaces call to the (internal) `evaluate` function with a simple helper that declares a factory as needed. --- tests/test_declarations.py | 196 ++++++++++++++++++++----------------- tests/test_fuzzy.py | 106 ++++++++++---------- tests/test_using.py | 6 +- tests/tools.py | 9 -- tests/utils.py | 21 ++++ 5 files changed, 182 insertions(+), 156 deletions(-) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 9c124780..389a925c 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -9,13 +9,13 @@ from factory import helpers from .compat import mock, unittest -from . import tools +from . import utils class OrderedDeclarationTestCase(unittest.TestCase): def test_errors(self): - decl = declarations.OrderedDeclaration() - self.assertRaises(NotImplementedError, decl.evaluate, None, {}, False) + with self.assertRaises(NotImplementedError): + utils.evaluate_declaration(declarations.OrderedDeclaration()) class DigTestCase(unittest.TestCase): @@ -77,55 +77,64 @@ def test_grandparent(self): class IteratorTestCase(unittest.TestCase): def test_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, utils.evaluate_declaration(it, force_sequence=0)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=2)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=3)) def test_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) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) + self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) 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)) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=2)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=3)) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=4)) it.reset() - self.assertEqual(1, it.evaluate(5, None, False)) - self.assertEqual(2, it.evaluate(6, None, False)) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=5)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=6)) 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) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) + self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) 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) + self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) + self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) def test_getter(self): it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1]) - self.assertEqual(2, it.evaluate(0, None, False)) - self.assertEqual(3, it.evaluate(1, None, False)) - self.assertEqual(2, it.evaluate(2, None, False)) - self.assertEqual(3, it.evaluate(3, None, False)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=0)) + self.assertEqual(3, utils.evaluate_declaration(it, force_sequence=1)) + self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=2)) + self.assertEqual(3, utils.evaluate_declaration(it, force_sequence=3)) class PostGenerationDeclarationTestCase(unittest.TestCase): - def test_extract_no_prefix(self): - decl = declarations.PostGenerationDeclaration() + def test_post_generation(self): + call_params = [] + def foo(*args, **kwargs): + call_params.append(args) + call_params.append(kwargs) - context = decl.extract('foo', - {'foo': 13, 'foo__bar': 42}) - self.assertTrue(context.did_extract) - self.assertEqual(context.value, 13) - self.assertEqual(context.extra, {'bar': 42}) + helpers.build( + dict, + foo=declarations.PostGeneration(foo), + foo__bar=42, + blah=42, + blah__baz=1, + ) + + self.assertEqual(2, len(call_params)) + self.assertEqual(3, len(call_params[0])) # instance, created, context + self.assertEqual({'bar': 42}, call_params[1]) def test_decorator_simple(self): call_params = [] @@ -134,16 +143,16 @@ def foo(*args, **kwargs): call_params.append(args) call_params.append(kwargs) - context = foo.extract('foo', - {'foo': 13, 'foo__bar': 42, 'blah': 42, 'blah__baz': 1}) - self.assertTrue(context.did_extract) - self.assertEqual(13, context.value) - self.assertEqual({'bar': 42}, context.extra) + helpers.build( + dict, + foo=foo, + foo__bar=42, + blah=42, + blah__baz=1, + ) - # No value returned. - foo.call(None, False, context) self.assertEqual(2, len(call_params)) - self.assertEqual((None, False, 13), call_params[0]) + self.assertEqual(3, len(call_params[0])) # instance, created, context self.assertEqual({'bar': 42}, call_params[1]) @@ -189,77 +198,82 @@ def test_cache(self): class PostGenerationMethodCallTestCase(unittest.TestCase): - def setUp(self): - self.obj = mock.MagicMock() - - def ctx(self, value=None, force_value=False, extra=None): - return declarations.ExtractionContext( - value, - bool(value) or force_value, - extra, - ) + def build(self, declaration, **params): + f = helpers.make_factory(mock.MagicMock, post=declaration) + return f(**params) def test_simplest_setup_and_call(self): - decl = declarations.PostGenerationMethodCall('method') - decl.call(self.obj, False, self.ctx()) - self.obj.method.assert_called_once_with() + obj = self.build( + declarations.PostGenerationMethodCall('method'), + ) + obj.method.assert_called_once_with() def test_call_with_method_args(self): - decl = declarations.PostGenerationMethodCall( - 'method', 'data') - decl.call(self.obj, False, self.ctx()) - self.obj.method.assert_called_once_with('data') + obj = self.build( + declarations.PostGenerationMethodCall( 'method', 'data'), + ) + obj.method.assert_called_once_with('data') def test_call_with_passed_extracted_string(self): - decl = declarations.PostGenerationMethodCall( - 'method') - decl.call(self.obj, False, self.ctx('data')) - self.obj.method.assert_called_once_with('data') + obj = self.build( + declarations.PostGenerationMethodCall('method'), + post='data', + ) + obj.method.assert_called_once_with('data') def test_call_with_passed_extracted_int(self): - decl = declarations.PostGenerationMethodCall('method') - decl.call(self.obj, False, self.ctx(1)) - self.obj.method.assert_called_once_with(1) + obj = self.build( + declarations.PostGenerationMethodCall('method'), + post=1, + ) + obj.method.assert_called_once_with(1) def test_call_with_passed_extracted_iterable(self): - decl = declarations.PostGenerationMethodCall('method') - decl.call(self.obj, False, self.ctx((1, 2, 3))) - self.obj.method.assert_called_once_with((1, 2, 3)) + obj = self.build( + declarations.PostGenerationMethodCall('method'), + post=(1, 2, 3), + ) + obj.method.assert_called_once_with((1, 2, 3)) def test_call_with_method_kwargs(self): - decl = declarations.PostGenerationMethodCall( - 'method', data='data') - decl.call(self.obj, False, self.ctx()) - self.obj.method.assert_called_once_with(data='data') + obj = self.build( + declarations.PostGenerationMethodCall('method', data='data'), + ) + obj.method.assert_called_once_with(data='data') def test_call_with_passed_kwargs(self): - decl = declarations.PostGenerationMethodCall('method') - decl.call(self.obj, False, self.ctx(extra={'data': 'other'})) - self.obj.method.assert_called_once_with(data='other') + obj = self.build( + declarations.PostGenerationMethodCall('method'), + post__data='other', + ) + obj.method.assert_called_once_with(data='other') def test_multi_call_with_multi_method_args(self): - decl = declarations.PostGenerationMethodCall( - 'method', 'arg1', 'arg2') - decl.call(self.obj, False, self.ctx()) - self.obj.method.assert_called_once_with('arg1', 'arg2') + obj = self.build( + declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), + ) + obj.method.assert_called_once_with('arg1', 'arg2') def test_multi_call_with_passed_multiple_args(self): - decl = declarations.PostGenerationMethodCall( - 'method', 'arg1', 'arg2') - decl.call(self.obj, False, self.ctx(('param1', 'param2', 'param3'))) - self.obj.method.assert_called_once_with('param1', 'param2', 'param3') + obj = self.build( + declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), + post=('param1', 'param2', 'param3'), + ) + 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, self.ctx((('param1', 'param2'),))) - self.obj.method.assert_called_once_with(('param1', 'param2')) + obj = self.build( + declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), + post=(('param1', 'param2'),), + ) + obj.method.assert_called_once_with(('param1', 'param2')) def test_multi_call_with_kwargs(self): - decl = declarations.PostGenerationMethodCall( - 'method', 'arg1', 'arg2') - decl.call(self.obj, False, self.ctx(extra={'x': 2})) - self.obj.method.assert_called_once_with('arg1', 'arg2', x=2) + obj = self.build( + declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), + post__x=2, + ) + obj.method.assert_called_once_with('arg1', 'arg2', x=2) class PostGenerationOrdering(unittest.TestCase): diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 9fa7f6a6..133e4ddd 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -18,10 +18,10 @@ class FuzzyAttributeTestCase(unittest.TestCase): def test_simple_call(self): d = fuzzy.FuzzyAttribute(lambda: 10) - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertEqual(10, res) - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertEqual(10, res) @@ -29,7 +29,7 @@ class FuzzyChoiceTestCase(unittest.TestCase): def test_unbiased(self): options = [1, 2, 3] d = fuzzy.FuzzyChoice(options) - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertIn(res, options) def test_mock(self): @@ -39,7 +39,7 @@ def test_mock(self): d = fuzzy.FuzzyChoice(options) with mock.patch('factory.random.randgen.choice', fake_choice): - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertEqual(6, res) @@ -50,11 +50,11 @@ def options(): d = fuzzy.FuzzyChoice(options()) - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertIn(res, [0, 1, 2]) # And repeat - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertIn(res, [0, 1, 2]) def test_lazy_generator(self): @@ -71,7 +71,7 @@ def __iter__(self): d = fuzzy.FuzzyChoice(opts) self.assertFalse(opts.unrolled) - res = d.evaluate(2, None, False) + res = utils.evaluate_declaration(d) self.assertIn(res, [1, 2, 3]) self.assertTrue(opts.unrolled) @@ -81,12 +81,12 @@ def test_definition(self): """Tests all ways of defining a FuzzyInteger.""" fuzz = fuzzy.FuzzyInteger(2, 3) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertIn(res, [2, 3]) fuzz = fuzzy.FuzzyInteger(4) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertIn(res, [0, 1, 2, 3, 4]) def test_biased(self): @@ -95,7 +95,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyInteger(2, 8) with mock.patch('factory.random.randgen.randrange', fake_randrange): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual((2 + 8 + 1) * 1, res) @@ -105,7 +105,7 @@ def test_biased_high_only(self): fuzz = fuzzy.FuzzyInteger(8) with mock.patch('factory.random.randgen.randrange', fake_randrange): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual((0 + 8 + 1) * 1, res) @@ -115,7 +115,7 @@ def test_biased_with_step(self): fuzz = fuzzy.FuzzyInteger(5, 8, 3) with mock.patch('factory.random.randgen.randrange', fake_randrange): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual((5 + 8 + 1) * 3, res) @@ -125,19 +125,19 @@ 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) + res = utils.evaluate_declaration(fuzz) self.assertTrue(decimal.Decimal('2.0') <= res <= decimal.Decimal('3.0'), "value %d is not between 2.0 and 3.0" % res) fuzz = fuzzy.FuzzyDecimal(4.0) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertTrue(decimal.Decimal('0.0') <= res <= decimal.Decimal('4.0'), "value %d is not between 0.0 and 4.0" % res) fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertTrue(decimal.Decimal('0.54') <= res <= decimal.Decimal('4.0'), "value %d is not between 0.54 and 4.0" % res) self.assertTrue(res.as_tuple().exponent, -5) @@ -148,7 +148,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyDecimal(2.0, 8.0) with mock.patch('factory.random.randgen.uniform', fake_uniform): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(decimal.Decimal('10.0'), res) @@ -158,7 +158,7 @@ def test_biased_high_only(self): fuzz = fuzzy.FuzzyDecimal(8.0) with mock.patch('factory.random.randgen.uniform', fake_uniform): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(decimal.Decimal('8.0'), res) @@ -168,7 +168,7 @@ def test_precision(self): fuzz = fuzzy.FuzzyDecimal(8.0, precision=3) with mock.patch('factory.random.randgen.uniform', fake_uniform): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) @@ -181,7 +181,7 @@ def test_no_approximation(self): old_traps = decimal_context.traps[decimal.FloatOperation] try: decimal_context.traps[decimal.FloatOperation] = True - fuzz.evaluate(2, None, None) + utils.evaluate_declaration(fuzz) finally: decimal_context.traps[decimal.FloatOperation] = old_traps @@ -199,7 +199,7 @@ def test_accurate_definition(self): fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertLessEqual(self.jan1, res) self.assertLessEqual(res, self.jan31) @@ -209,7 +209,7 @@ def test_partial_definition(self): fuzz = fuzzy.FuzzyDate(self.jan1) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertLessEqual(self.jan1, res) self.assertLessEqual(res, self.jan3) @@ -229,7 +229,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyDate(self.jan1, self.jan31) with mock.patch('factory.random.randgen.randint', fake_randint): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(datetime.date(2013, 1, 16), res) @@ -240,7 +240,7 @@ def test_biased_partial(self): fake_randint = lambda low, high: (low + high) // 2 with mock.patch('factory.random.randgen.randint', fake_randint): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(datetime.date(2013, 1, 2), res) @@ -258,7 +258,7 @@ def test_accurate_definition(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertLessEqual(self.jan1, res) self.assertLessEqual(res, self.jan31) @@ -268,7 +268,7 @@ def test_partial_definition(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertLessEqual(self.jan1, res) self.assertLessEqual(res, self.jan3) @@ -286,49 +286,49 @@ 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) self.assertEqual(4, res.microsecond) def test_invalid_definition(self): @@ -347,7 +347,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31) with mock.patch('factory.random.randgen.randint', fake_randint): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(datetime.datetime(2013, 1, 16), res) @@ -358,7 +358,7 @@ def test_biased_partial(self): fake_randint = lambda low, high: (low + high) // 2 with mock.patch('factory.random.randgen.randint', fake_randint): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(datetime.datetime(2013, 1, 2), res) @@ -376,7 +376,7 @@ def test_accurate_definition(self): fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertLessEqual(self.jan1, res) self.assertLessEqual(res, self.jan31) @@ -386,7 +386,7 @@ def test_partial_definition(self): fuzz = fuzzy.FuzzyDateTime(self.jan1) for _i in range(20): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertLessEqual(self.jan1, res) self.assertLessEqual(res, self.jan3) @@ -413,49 +413,49 @@ 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) 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) + res = utils.evaluate_declaration(fuzz) self.assertEqual(4, res.microsecond) def test_biased(self): @@ -465,7 +465,7 @@ def test_biased(self): fuzz = fuzzy.FuzzyDateTime(self.jan1, self.jan31) with mock.patch('factory.random.randgen.randint', fake_randint): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res) @@ -476,7 +476,7 @@ def test_biased_partial(self): fake_randint = lambda low, high: (low + high) // 2 with mock.patch('factory.random.randgen.randint', fake_randint): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res) @@ -486,7 +486,7 @@ 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) + res = utils.evaluate_declaration(fuzz) self.assertEqual('pre', res[:3]) self.assertEqual('post', res[-4:]) @@ -501,7 +501,7 @@ def test_mock(self): chars = ['a', 'b', 'c'] fuzz = fuzzy.FuzzyText(prefix='pre', suffix='post', chars=chars, length=4) with mock.patch('factory.random.randgen.choice', fake_choice): - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual('preaaaapost', res) @@ -512,7 +512,7 @@ def options(): yield 'c' fuzz = fuzzy.FuzzyText(chars=options(), length=12) - res = fuzz.evaluate(2, None, False) + res = utils.evaluate_declaration(fuzz) self.assertEqual(12, len(res)) @@ -525,16 +525,16 @@ def test_seeding(self): fuzz = fuzzy.FuzzyInteger(1, 1000) random.reseed_random(42) - value = fuzz.evaluate(sequence=1, obj=None, create=False) + value = utils.evaluate_declaration(fuzz) random.reseed_random(42) - value2 = fuzz.evaluate(sequence=1, obj=None, create=False) + value2 = utils.evaluate_declaration(fuzz) self.assertEqual(value, value2) def test_seeding_warning(self): with warnings.catch_warnings(record=True) as w: fuzz = fuzzy.FuzzyDate(datetime.date(2013, 1, 1)) - fuzz.evaluate(None, None, None) + utils.evaluate_declaration(fuzz) self.assertEqual(1, len(w)) self.assertIn('factory_boy/issues/331', str(w[-1].message)) @@ -542,8 +542,8 @@ def test_reset_state(self): fuzz = fuzzy.FuzzyInteger(1, 1000) state = random.get_random_state() - value = fuzz.evaluate(sequence=1, obj=None, create=False) + value = utils.evaluate_declaration(fuzz) random.set_random_state(state) - value2 = fuzz.evaluate(sequence=1, obj=None, create=False) + value2 = utils.evaluate_declaration(fuzz) self.assertEqual(value, value2) diff --git a/tests/test_using.py b/tests/test_using.py index ad11006c..9e96f17b 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -14,7 +14,7 @@ from factory import errors from .compat import is_python2, unittest -from . import tools +from . import utils class TestObject(object): @@ -1591,7 +1591,7 @@ class Meta: self.assertEqual(i + 10, obj.one) @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+") - @tools.disable_warnings + @utils.disable_warnings def test_iterator_list_comprehension_scope_bleeding(self): class TestObjectFactory(factory.Factory): class Meta: @@ -1603,7 +1603,7 @@ class Meta: self.assertRaises(TypeError, TestObjectFactory.build) - @tools.disable_warnings + @utils.disable_warnings def test_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): class Meta: diff --git a/tests/tools.py b/tests/tools.py index 42598530..111e3848 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -6,12 +6,3 @@ import warnings -def disable_warnings(fun): - @functools.wraps(fun) - def decorated(*args, **kwargs): - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - return fun(*args, **kwargs) - return decorated - - diff --git a/tests/utils.py b/tests/utils.py index 5b7ed9a7..18f35dc3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,11 +2,24 @@ # Copyright: See the LICENSE file. import datetime +import functools +import warnings + +import factory from .compat import mock from . import alter_time +def disable_warnings(fun): + @functools.wraps(fun) + def decorated(*args, **kwargs): + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + return fun(*args, **kwargs) + return decorated + + class MultiModulePatcher(object): """An abstract context processor for patching multiple modules.""" @@ -47,3 +60,11 @@ def __init__(self, target_dt, *target_modules, **kwargs): def _build_patcher(self, target_module): module_datetime = getattr(target_module, 'datetime') return alter_time.mock_datetime_now(self.target_dt, module_datetime) + + +def evaluate_declaration(declaration, force_sequence=None): + kwargs = {'attr': declaration} + if force_sequence is not None: + kwargs['__sequence'] = force_sequence + + return factory.build(dict, **kwargs)['attr'] From bcb3969f8d1310baa1ff8fe6d21097a65e17f74e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 15 Jan 2017 21:45:59 +0100 Subject: [PATCH 224/714] tests: Remove the 'tools' module. Merge its features into the 'utils' module. --- tests/test_django.py | 1 - tests/tools.py | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 tests/tools.py diff --git a/tests/test_django.py b/tests/test_django.py index 4350ac25..66426817 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -50,7 +50,6 @@ from factory.compat import BytesIO from . import testdata -from . import tools test_state = {} diff --git a/tests/tools.py b/tests/tools.py deleted file mode 100644 index 111e3848..00000000 --- a/tests/tools.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: See the LICENSE file. - - -import functools -import warnings - - From 8556298ffcb86348e6aee465b5deaf34afb1ecc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 15 Jan 2017 21:57:39 +0100 Subject: [PATCH 225/714] tests: Use meaningful field names. Improve a few tests by using descriptive field names instead of resorting to `one`, `two`, ... --- tests/test_using.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_using.py b/tests/test_using.py index 9e96f17b..a4abc6b8 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1529,19 +1529,19 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: model = TestModel - one = 3 - two = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=True) + sample_int = 3 + container_len = factory.ContainerAttribute(lambda obj, containers: len(containers or []), strict=True) class TestModel2Factory(FakeModelFactory): class Meta: model = TestModel2 - one = 1 - two = factory.SubFactory(TestModelFactory, one=1) + sample_int = 1 + descendant = factory.SubFactory(TestModelFactory, sample_int=1) obj = TestModel2Factory.build() - self.assertEqual(1, obj.one) - self.assertEqual(1, obj.two.one) - self.assertEqual(1, obj.two.two) + self.assertEqual(1, obj.sample_int) + self.assertEqual(1, obj.descendant.sample_int) + self.assertEqual(1, obj.descendant.container_len) self.assertRaises(TypeError, TestModelFactory.build) @@ -1552,10 +1552,10 @@ class TestModel2(FakeModel): class TestModelFactory(FakeModelFactory): class Meta: model = TestModel - one = 3 + sample_int = 3 @factory.container_attribute - def two(self, containers): + def container_len(self, containers): if containers: return len(containers) return 42 @@ -1563,17 +1563,17 @@ def two(self, containers): class TestModel2Factory(FakeModelFactory): class Meta: model = TestModel2 - one = 1 - two = factory.SubFactory(TestModelFactory, one=1) + sample_int = 1 + descendant = factory.SubFactory(TestModelFactory, sample_int=1) obj = TestModel2Factory.build() - self.assertEqual(1, obj.one) - self.assertEqual(1, obj.two.one) - self.assertEqual(1, obj.two.two) + self.assertEqual(1, obj.sample_int) + self.assertEqual(1, obj.descendant.sample_int) + self.assertEqual(1, obj.descendant.container_len) obj = TestModelFactory() - self.assertEqual(3, obj.one) - self.assertEqual(42, obj.two) + self.assertEqual(3, obj.sample_int) + self.assertEqual(42, obj.container_len) class IteratorTestCase(unittest.TestCase): @@ -2115,7 +2115,7 @@ def __init__(self, obj=None, one=None, two=None): obj.related = self self.one = one self.two = two - self.three = obj + self.related = obj class TestRelatedObjectFactory(factory.Factory): class Meta: From 11b6a887182ca97be5eb062c1ef85e5a1d12ed89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 16:03:31 +0200 Subject: [PATCH 226/714] [refactor] Move get_model_class to FactoryOptions. As part of the simplification of the Factory class. --- factory/base.py | 27 ++++++++++----------------- factory/django.py | 7 +++++++ tests/test_django.py | 4 +--- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/factory/base.py b/factory/base.py index 26cced3c..6f512a80 100644 --- a/factory/base.py +++ b/factory/base.py @@ -206,7 +206,7 @@ def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=N self._fill_from_meta(meta=meta, base_meta=base_meta) - self.model = self.factory._load_model_class(self.model) + self.model = self.get_model_class() if self.model is None: self.abstract = True @@ -289,6 +289,14 @@ def _compute_parameter_dependencies(self, parameters): % (self.factory, ', '.join(cyclic))) return deps + def get_model_class(self): + """Extension point for loading model classes. + + This can be overridden in framework-specific subclasses to hook into + existing model repositories, for instance. + """ + return self.model + def __str__(self): return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__) @@ -457,21 +465,6 @@ def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" return kwargs - @classmethod - def _load_model_class(cls, class_definition): - """Extension point for loading model classes. - - This can be overridden in framework-specific subclasses to hook into - existing model repositories, for instance. - """ - return class_definition - - @classmethod - def _get_model_class(cls): - """Retrieve the actual, associated model class.""" - definition = cls._meta.model - return cls._load_model_class(definition) - @classmethod def _prepare(cls, create, **kwargs): """Prepare an object for this factory. @@ -480,7 +473,7 @@ def _prepare(cls, create, **kwargs): create: bool, whether to create or to build the object **kwargs: arguments to pass to the creation function """ - model_class = cls._get_model_class() + model_class = cls._meta.get_model_class() kwargs = cls._rename_fields(**kwargs) kwargs = cls._adjust_kwargs(**kwargs) diff --git a/factory/django.py b/factory/django.py index 8eef6837..8e05db96 100644 --- a/factory/django.py +++ b/factory/django.py @@ -87,6 +87,13 @@ def _get_counter_reference(self): return self.factory return counter_reference + def get_model_class(self): + if is_string(self.model) and '.' in self.model: + app, model_name = self.model.split('.', 1) + self.model = get_model(app, model_name) + + return self.model + class DjangoModelFactory(base.Factory): """Factory for Django models. diff --git a/tests/test_django.py b/tests/test_django.py index 66426817..245b3a47 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -274,7 +274,7 @@ class ExampleFactory(factory.DjangoModelFactory): class Meta: model = 'djapp.StandardModel' - self.assertEqual(models.StandardModel, ExampleFactory._get_model_class()) + self.assertEqual(models.StandardModel, ExampleFactory._meta.get_model_class()) def test_building(self): class ExampleFactory(factory.DjangoModelFactory): @@ -314,8 +314,6 @@ class Example2Factory(ExampleFactory): class Meta: model = 'djapp.StandardSon' - self.assertEqual(models.StandardSon, Example2Factory._get_model_class()) - e1 = ExampleFactory.build() e2 = Example2Factory.build() e3 = ExampleFactory.build() From a1bcb322faffc9fdd57e290cb2a795a21eb7e0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 16:17:26 +0200 Subject: [PATCH 227/714] [refactor] Move containers.StubObject to base. Reducing the scope of the "containers" module. --- factory/base.py | 14 +++++++++----- factory/containers.py | 5 ----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/factory/base.py b/factory/base.py index 6f512a80..cd1f43f0 100644 --- a/factory/base.py +++ b/factory/base.py @@ -615,10 +615,7 @@ def stub(cls, **kwargs): This will return an object whose attributes are those defined in this factory's declarations or in the extra kwargs. """ - stub_object = containers.StubObject() - for name, value in cls.attributes(create=False, extra=kwargs).items(): - setattr(stub_object, name, value) - return stub_object + return StubObject(**cls.attributes(create=False, extra=kwargs)) @classmethod def stub_batch(cls, size, **kwargs): @@ -715,11 +712,18 @@ def simple_generate_batch(cls, create, size, **kwargs): Factory.AssociatedClassError = errors.AssociatedClassError +class StubObject(object): + """A generic container.""" + def __init__(self, **kwargs): + for field, value in kwargs.items(): + setattr(self, field, value) + + class StubFactory(Factory): class Meta: strategy = STUB_STRATEGY - model = containers.StubObject + model = StubObject @classmethod def build(cls, **kwargs): diff --git a/factory/containers.py b/factory/containers.py index 8e4c2e7e..8ff83456 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -303,8 +303,3 @@ def build(self, create, force_sequence=None): wrapped_attrs, containers=self._containers, model_class=self.factory, log_ctx=self._log_ctx) return stub.__fill__() - - -class StubObject(object): - """A generic container.""" - pass From 070d5890b77ec77afb5d852352233101481e6117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 16:23:16 +0200 Subject: [PATCH 228/714] [refactor] Improve Parameter/BaseDeclaration naming. Don't use ComplexParameter/OrderedDeclaration anymore. --- factory/base.py | 4 ++-- factory/containers.py | 4 ++-- factory/declarations.py | 42 ++++++++++++++++++++++++++--------------- factory/faker.py | 2 +- factory/fuzzy.py | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/factory/base.py b/factory/base.py index cd1f43f0..34c51d7d 100644 --- a/factory/base.py +++ b/factory/base.py @@ -253,7 +253,7 @@ def _is_declaration(self, name, value): """ if isinstance(value, (classmethod, staticmethod)): return False - elif isinstance(value, declarations.OrderedDeclaration): + elif isinstance(value, declarations.BaseDeclaration): return True elif isinstance(value, declarations.PostGenerationDeclaration): return False @@ -272,7 +272,7 @@ def _compute_parameter_dependencies(self, parameters): deps = collections.defaultdict(set) for name, parameter in parameters.items(): - if isinstance(parameter, declarations.ComplexParameter): + if isinstance(parameter, declarations.Parameter): field_revdeps = parameter.get_revdeps(parameters) if not field_revdeps: continue diff --git a/factory/containers.py b/factory/containers.py index 8ff83456..a8b56464 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -152,7 +152,7 @@ def resolve_one(self, name): def compute(self, name): """Actually compute the value for a given name.""" value = self.parameters[name] - if isinstance(value, declarations.ComplexParameter): + if isinstance(value, declarations.Parameter): overrides = value.compute(name, self.declaration_stack.current()) else: overrides = {name: value} @@ -290,7 +290,7 @@ def build(self, create, force_sequence=None): # OrderedDeclaration. wrapped_attrs = {} for k, v in self._declarations.items(): - if isinstance(v, declarations.OrderedDeclaration): + if isinstance(v, declarations.BaseDeclaration): v = DeclarationWrapper( v, sequence=sequence, diff --git a/factory/declarations.py b/factory/declarations.py index 4ed5203d..2314ba5e 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -13,13 +13,19 @@ logger = logging.getLogger('factory.generate') -class OrderedDeclaration(object): +class BaseDeclaration(object): """A factory declaration. Ordered declarations mark an attribute as needing lazy evaluation. - This allows them to refer to attributes defined by other OrderedDeclarations + This allows them to refer to attributes defined by other BaseDeclarations in the same factory. """ + creation_counter = 0 + + def __init__(self, **kwargs): + super(BaseDeclaration, self).__init__(**kwargs) + self.creation_counter = BaseDeclaration.creation_counter + BaseDeclaration.creation_counter += 1 def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate this declaration. @@ -39,8 +45,14 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): raise NotImplementedError('This is an abstract method') -class LazyFunction(OrderedDeclaration): - """Simplest OrderedDeclaration computed by calling the given function. +class OrderedDeclaration(BaseDeclaration): + """Compatibility""" + + # FIXME(rbarrois) + + +class LazyFunction(BaseDeclaration): + """Simplest BaseDeclaration computed by calling the given function. Attributes: function (function): a function without arguments and @@ -56,8 +68,8 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function() -class LazyAttribute(OrderedDeclaration): - """Specific OrderedDeclaration computed using a lambda. +class LazyAttribute(BaseDeclaration): + """Specific BaseDeclaration computed using a lambda. Attributes: function (function): a function, expecting the current LazyStub and @@ -106,8 +118,8 @@ def deepgetattr(obj, name, default=_UNSPECIFIED): return default -class SelfAttribute(OrderedDeclaration): - """Specific OrderedDeclaration copying values from other fields. +class SelfAttribute(BaseDeclaration): + """Specific BaseDeclaration copying values from other fields. If the field name starts with two dots or more, the lookup will be anchored in the related 'parent'. @@ -146,7 +158,7 @@ def __repr__(self): ) -class Iterator(OrderedDeclaration): +class Iterator(BaseDeclaration): """Fill this value using the values returned by an iterator. Warning: the iterator should not end ! @@ -183,8 +195,8 @@ def reset(self): self.iterator.reset() -class Sequence(OrderedDeclaration): - """Specific OrderedDeclaration to use for 'sequenced' fields. +class Sequence(BaseDeclaration): + """Specific BaseDeclaration to use for 'sequenced' fields. These fields are typically used to generate increasing unique values. @@ -220,7 +232,7 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(obj, self.type(sequence)) -class ContainerAttribute(OrderedDeclaration): +class ContainerAttribute(BaseDeclaration): """Variant of LazyAttribute, also receives the containers of the object. Attributes: @@ -252,7 +264,7 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(obj, containers) -class ParameteredAttribute(OrderedDeclaration): +class ParameteredAttribute(BaseDeclaration): """Base class for attributes expecting parameters. Attributes: @@ -431,7 +443,7 @@ def generate(self, sequence, obj, create, params): # ========== -class ComplexParameter(object): +class Parameter(object): """A complex parameter, to be used in a Factory.Params section. Must implement: @@ -456,7 +468,7 @@ def get_revdeps(self, parameters): return [] -class Trait(ComplexParameter): +class Trait(Parameter): """The simplest complex parameter, it enables a bunch of new declarations based on a boolean flag.""" def __init__(self, **overrides): self.overrides = overrides diff --git a/factory/faker.py b/factory/faker.py index feb7fa29..457cbe57 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -26,7 +26,7 @@ class Meta: from . import declarations -class Faker(declarations.OrderedDeclaration): +class Faker(declarations.BaseDeclaration): """Wrapper for 'faker' values. Args: diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 122c546f..094c438a 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -52,7 +52,7 @@ def reseed_random(seed): random.reseed_random(seed) -class BaseFuzzyAttribute(declarations.OrderedDeclaration): +class BaseFuzzyAttribute(declarations.BaseDeclaration): """Base class for fuzzy attributes. Custom fuzzers should override the `fuzz()` method. From e682014640db7d4225f1ac24dd573c83921fe1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 23:44:00 +0200 Subject: [PATCH 229/714] [refactor] Move counter/sequence to factory._meta As part of the global simplification of the Factory class. --- factory/base.py | 101 +++++++++++++++++---------------------- factory/containers.py | 2 +- tests/test_base.py | 2 +- tests/test_containers.py | 8 ++-- 4 files changed, 51 insertions(+), 62 deletions(-) diff --git a/factory/base.py b/factory/base.py index 34c51d7d..550b3c62 100644 --- a/factory/base.py +++ b/factory/base.py @@ -153,6 +153,9 @@ def __init__(self): self.parameters = {} self.parameters_dependencies = {} + self._counter = None + self.counter_reference = None + @property def sorted_postgen_declarations(self): """Get sorted postgen declaration items.""" @@ -239,9 +242,47 @@ def _get_counter_reference(self): and self.base_factory is not None and self.base_factory._meta.model is not None and issubclass(self.model, self.base_factory._meta.model)): - return self.base_factory + return self.base_factory._meta.counter_reference else: - return self.factory + return self + + def _initialize_counter(self): + """Initialize our counter pointer. + + If we're the top-level factory, instantiate a new counter + Otherwise, point to the top-level factory's counter. + """ + if self._counter is not None: + return + + if self.counter_reference is self: + self._counter = _Counter(seq=self.factory._setup_next_sequence()) + else: + self.counter_reference._initialize_counter() + self._counter = self.counter_reference._counter + + def next_sequence(self): + """Retrieve a new sequence ID. + + This will call, in order: + - next_sequence from the base factory, if provided + - _setup_next_sequence, if this is the 'toplevel' factory and the + sequence counter wasn't initialized yet; then increase it. + """ + self._initialize_counter() + return self._counter.next() + + def reset_sequence(self, value=None, force=False): + self._initialize_counter() + + if self.counter_reference is not self and not force: + raise ValueError( + "Can't reset a sequence on decendant factory %r; reset sequence on %r or use `force=True`." + % (self.factory, self.counter_reference.factory)) + + if value is None: + value = self.counter_reference.factory._setup_next_sequence() + self._counter.reset(value) def _is_declaration(self, name, value): """Determines if a class attribute is a field value declaration. @@ -315,9 +356,8 @@ class _Counter(object): seq (int): the next value """ - def __init__(self, seq, for_class): + def __init__(self, seq): self.seq = seq - self.for_class = for_class def next(self): value = self.seq @@ -327,10 +367,6 @@ def next(self): 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.""" @@ -358,20 +394,7 @@ def reset_sequence(cls, value=None, force=False): force (bool): whether to force-reset parent sequence counters in a factory inheritance chain. """ - if cls._meta.counter_reference is not cls: - if force: - cls._meta.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(force=True)." - ) - else: - cls._setup_counter() - if value is None: - value = cls._setup_next_sequence() - cls._counter.reset(value) + cls._meta.reset_sequence(value, force=force) @classmethod def _setup_next_sequence(cls): @@ -382,40 +405,6 @@ def _setup_next_sequence(cls): """ 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("%s: Setting up next sequence (%d)", cls, first_seq) - - @classmethod - def _generate_next_sequence(cls): - """Retrieve a new sequence ID. - - This will call, in order: - - _generate_next_sequence from the base factory, if provided - - _setup_next_sequence, if this is the 'toplevel' factory and the - sequence counter wasn't initialized yet; then increase it. - """ - - # Rely upon our parents - if cls._meta.counter_reference is not cls: - logger.debug("%r: reusing sequence from %r", cls, cls._meta.base_factory) - return cls._meta.base_factory._generate_next_sequence() - - # Make sure _counter is initialized - cls._setup_counter() - - # Pick current value, then increase class counter for the next call. - return cls._counter.next() - @classmethod def attributes(cls, create=False, extra=None): """Build a dict of attribute values, respecting declaration order. diff --git a/factory/containers.py b/factory/containers.py index a8b56464..6ec21a5c 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -282,7 +282,7 @@ def build(self, create, force_sequence=None): """ # Setup factory sequence. if force_sequence is None: - sequence = self.factory._generate_next_sequence() + sequence = self.factory._meta.next_sequence() else: sequence = force_sequence diff --git a/tests/test_base.py b/tests/test_base.py index b9d65ba2..90dcc8bd 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -113,7 +113,7 @@ class AbstractFactory(base.Factory): self.assertEqual({}, AbstractFactory._meta.postgen_declarations) self.assertEqual(AbstractFactory, AbstractFactory._meta.factory) self.assertEqual(base.Factory, AbstractFactory._meta.base_factory) - self.assertEqual(AbstractFactory, AbstractFactory._meta.counter_reference) + self.assertEqual(AbstractFactory._meta, AbstractFactory._meta.counter_reference) def test_declaration_collecting(self): lazy = declarations.LazyFunction(int) diff --git a/tests/test_containers.py b/tests/test_containers.py index 8b2e5746..581b16d1 100644 --- a/tests/test_containers.py +++ b/tests/test_containers.py @@ -113,13 +113,13 @@ class Meta: parameters = {} parameters_dependencies = {} - class FakeFactory(object): - _meta = Meta - @classmethod - def _generate_next_sequence(cls): + def next_sequence(cls): return 1 + class FakeFactory(object): + _meta = Meta + return FakeFactory def test_empty(self): From e44766245e70fa102a7e1f85d3bd66c3646d271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 6 Apr 2017 23:58:20 +0200 Subject: [PATCH 230/714] [refactor] Move instantiation to class Meta As part of the global simplification of the Factory class. ERROR: Change the signature of the _after_postgeneration hook (name of args) ERROR: Change the signature of the _generate function (switch from a boolean 'create' to a full enum. --- factory/base.py | 105 +++++++++++++++++++++++--------------------- factory/django.py | 4 +- tests/test_base.py | 6 +-- tests/test_using.py | 1 + 4 files changed, 61 insertions(+), 55 deletions(-) diff --git a/factory/base.py b/factory/base.py index 550b3c62..0c722c2f 100644 --- a/factory/base.py +++ b/factory/base.py @@ -284,6 +284,48 @@ def reset_sequence(self, value=None, force=False): value = self.counter_reference.factory._setup_next_sequence() self._counter.reset(value) + def prepare_arguments(self, attributes): + """Convert an attributes dict to a (args, kwargs) tuple.""" + kwargs = dict(attributes) + # 1. Extension points + kwargs = self.factory._adjust_kwargs(**kwargs) + + # 2. Remove hidden objects + kwargs = { + k: v for k, v in kwargs.items() + if k not in self.exclude and k not in self.parameters + } + + # 3. Rename fields + for old_name, new_name in self.rename.items(): + kwargs[new_name] = kwargs.pop(old_name) + + # 4. Extract inline args + args = tuple( + kwargs.pop(arg_name) + for arg_name in self.inline_args + ) + + return args, kwargs + + def instantiate(self, strategy, args, kwargs): + model = self.get_model_class() + + if strategy == BUILD_STRATEGY: + return self.factory._build(model, *args, **kwargs) + elif strategy == CREATE_STRATEGY: + return self.factory._create(model, *args, **kwargs) + else: + assert strategy == STUB_STRATEGY + return StubObject(**kwargs) + + def use_postgeneration_results(self, create, instance, results): + self.factory._after_postgeneration( + instance=instance, + create=create, + results=results, + ) + def _is_declaration(self, name, value): """Determines if a class attribute is a field value declaration. @@ -443,57 +485,18 @@ def declarations(cls, extra_defs=None): decls.update(extra_defs or {}) return decls - @classmethod - def _rename_fields(cls, **kwargs): - for old_name, new_name in cls._meta.rename.items(): - kwargs[new_name] = kwargs.pop(old_name) - return kwargs - @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" return kwargs @classmethod - def _prepare(cls, create, **kwargs): - """Prepare an object for this factory. - - Args: - create: bool, whether to create or to build the object - **kwargs: arguments to pass to the creation function - """ - model_class = cls._meta.get_model_class() - kwargs = cls._rename_fields(**kwargs) - kwargs = cls._adjust_kwargs(**kwargs) - - # Remove 'hidden' arguments. - for arg in cls._meta.exclude: - del kwargs[arg] - # Remove parameters, if defined - for arg in cls._meta.parameters: - kwargs.pop(arg, None) - - # Extract *args from **kwargs - args = tuple(kwargs.pop(key) for key in cls._meta.inline_args) - - logger.debug( - "BaseFactory: Generating %s.%s(%s)", - cls.__module__, - cls.__name__, - utils.log_pprint(args, kwargs), - ) - if create: - return cls._create(model_class, *args, **kwargs) - else: - return cls._build(model_class, *args, **kwargs) - - @classmethod - def _generate(cls, create, attrs): + def _generate(cls, strategy, params): """generate the object. Args: - create (bool): whether to 'build' or 'create' the object - attrs (dict): attributes to use for generating the object + params (dict): attributes to use for generating the object + strategy: the strategy to use """ if cls._meta.abstract: raise errors.FactoryError( @@ -501,6 +504,9 @@ def _generate(cls, create, attrs): "Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract " "is either not set or False." % dict(f=cls.__name__)) + create = bool(strategy == CREATE_STRATEGY) + + attrs = cls.attributes(create=create, extra=params) # Extract declarations used for post-generation postgen_attributes = {} @@ -508,7 +514,8 @@ def _generate(cls, create, attrs): postgen_attributes[name] = decl.extract(name, attrs) # Generate the object - obj = cls._prepare(create, **attrs) + args, kwargs = cls._meta.prepare_arguments(attrs) + obj = cls._meta.instantiate(strategy, args, kwargs) # Handle post-generation attributes results = {} @@ -516,12 +523,12 @@ def _generate(cls, create, attrs): extraction_context = postgen_attributes[name] results[name] = decl.call(obj, create, extraction_context) - cls._after_postgeneration(obj, create, results) + cls._meta.use_postgeneration_results(create, obj, results) return obj @classmethod - def _after_postgeneration(cls, obj, create, results=None): + def _after_postgeneration(cls, instance, create, results=None): """Hook called after post-generation declarations have been handled. Args: @@ -564,8 +571,7 @@ def _create(cls, model_class, *args, **kwargs): @classmethod def build(cls, **kwargs): """Build an instance of the associated class, with overriden attrs.""" - attrs = cls.attributes(create=False, extra=kwargs) - return cls._generate(False, attrs) + return cls._generate(BUILD_STRATEGY, kwargs) @classmethod def build_batch(cls, size, **kwargs): @@ -582,8 +588,7 @@ def build_batch(cls, size, **kwargs): @classmethod def create(cls, **kwargs): """Create an instance of the associated class, with overriden attrs.""" - attrs = cls.attributes(create=True, extra=kwargs) - return cls._generate(True, attrs) + return cls._generate(CREATE_STRATEGY, kwargs) @classmethod def create_batch(cls, size, **kwargs): @@ -604,7 +609,7 @@ def stub(cls, **kwargs): This will return an object whose attributes are those defined in this factory's declarations or in the extra kwargs. """ - return StubObject(**cls.attributes(create=False, extra=kwargs)) + return cls._generate(STUB_STRATEGY, kwargs) @classmethod def stub_batch(cls, size, **kwargs): diff --git a/factory/django.py b/factory/django.py index 8e05db96..7535549c 100644 --- a/factory/django.py +++ b/factory/django.py @@ -169,11 +169,11 @@ def _create(cls, model_class, *args, **kwargs): return manager.create(*args, **kwargs) @classmethod - def _after_postgeneration(cls, obj, create, results=None): + def _after_postgeneration(cls, instance, 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() + instance.save() class FileField(declarations.ParameteredAttribute): diff --git a/tests/test_base.py b/tests/test_base.py index 90dcc8bd..94233a49 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -472,9 +472,9 @@ class Meta: model = TestModel @classmethod - def _prepare(cls, create, **kwargs): - kwargs['four'] = 4 - return super(TestModelFactory, cls)._prepare(create, **kwargs) + def _generate(cls, create, attrs): + attrs['four'] = 4 + return super(TestModelFactory, cls)._generate(create, attrs) b = TestModelFactory.build(one=1) self.assertEqual(1, b.one) diff --git a/tests/test_using.py b/tests/test_using.py index a4abc6b8..eca812e4 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1902,6 +1902,7 @@ def incr_one(self, _create, _increment): self.assertEqual(3, obj.one) self.assertFalse(hasattr(obj, 'incr_one')) + @unittest.expectedFailure # Broken API in refactor def test_post_generation_hook(self): class TestObjectFactory(factory.Factory): class Meta: From c079d87ddb8e0eeabc66762d7f8f5bb81a143ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 00:08:53 +0200 Subject: [PATCH 231/714] [refactor] Convert Trait into Maybe declaration Also, convert a scalar parameter into a SimpleParameter. FIXME: Document the 'Maybe' parameter. --- factory/__init__.py | 1 + factory/base.py | 9 +++--- factory/containers.py | 2 +- factory/declarations.py | 63 +++++++++++++++++++++++++++++++++++++---- 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index 99b58d58..b2014de3 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -34,6 +34,7 @@ SubFactory, Dict, List, + Maybe, PostGeneration, PostGenerationMethodCall, RelatedFactory, diff --git a/factory/base.py b/factory/base.py index 0c722c2f..c58d9e22 100644 --- a/factory/base.py +++ b/factory/base.py @@ -231,9 +231,10 @@ def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=N if params is not None: for k, v in vars(params).items(): if not k.startswith('_'): - self.parameters[k] = v + self.parameters[k] = declarations.SimpleParameter.wrap(v) + + self._check_parameter_dependencies(self.parameters) - self.parameters_dependencies = self._compute_parameter_dependencies(self.parameters) def _get_counter_reference(self): """Identify which factory should be used for a shared counter.""" @@ -346,7 +347,7 @@ def _is_postgen_declaration(self, name, value): """Captures instances of PostGenerationDeclaration.""" return isinstance(value, declarations.PostGenerationDeclaration) - def _compute_parameter_dependencies(self, parameters): + def _check_parameter_dependencies(self, parameters): """Find out in what order parameters should be called.""" # Warning: parameters only provide reverse dependencies; we reverse them into standard dependencies. # deep_revdeps: set of fields a field depend indirectly upon @@ -368,7 +369,7 @@ def _compute_parameter_dependencies(self, parameters): cyclic = [name for name, field_deps in deep_revdeps.items() if name in field_deps] if cyclic: raise errors.CyclicDefinitionError( - "Cyclic definition detected on %s' Params around %s" + "Cyclic definition detected on %r; Params around %s" % (self.factory, ', '.join(cyclic))) return deps diff --git a/factory/containers.py b/factory/containers.py index 6ec21a5c..c210f24b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -153,7 +153,7 @@ def compute(self, name): """Actually compute the value for a given name.""" value = self.parameters[name] if isinstance(value, declarations.Parameter): - overrides = value.compute(name, self.declaration_stack.current()) + overrides = value.as_declarations(name, self.declaration_stack.current()) else: overrides = {name: value} self.declaration_stack['overrides'].update(overrides) diff --git a/factory/declarations.py b/factory/declarations.py index 2314ba5e..283056a5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -20,6 +20,7 @@ class BaseDeclaration(object): This allows them to refer to attributes defined by other BaseDeclarations in the same factory. """ + creation_counter = 0 def __init__(self, **kwargs): @@ -443,6 +444,36 @@ def generate(self, sequence, obj, create, params): # ========== +class UNDEFINED(object): + pass + + +class Maybe(BaseDeclaration): + def __init__(self, decider, yes_declaration, no_declaration=None): + self.decider = decider + self.yes = yes_declaration + self.no = no_declaration + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + decider = getattr(obj, self.decider, None) + target = self.yes if decider else self.no + + if isinstance(target, BaseDeclaration): + return target.evaluate( + sequence=sequence, + obj=obj, + create=create, + extra=extra, + containers=containers, + ) + else: + # Flat value + return target + + def __repr__(self): + return 'Maybe(%r, yes=%r, no=%r)' % (self.decider, self.yes, self.no) + + class Parameter(object): """A complex parameter, to be used in a Factory.Params section. @@ -451,7 +482,7 @@ class Parameter(object): - Optionally, a get_revdeps() function (to compute other parameters it may alter) """ - def compute(self, field_name, declarations): + def as_declarations(self, field_name, declarations): """Compute the overrides for this parameter. Args: @@ -468,16 +499,36 @@ def get_revdeps(self, parameters): return [] +class SimpleParameter(Parameter): + def __init__(self, value): + self.value = value + + def as_declarations(self, field_name, declarations): + return { + field_name: self.value, + } + + @classmethod + def wrap(cls, value): + if not isinstance(value, Parameter): + return cls(value) + return value + + class Trait(Parameter): """The simplest complex parameter, it enables a bunch of new declarations based on a boolean flag.""" def __init__(self, **overrides): self.overrides = overrides - def compute(self, field_name, declarations): - if declarations.get(field_name): - return self.overrides - else: - return {} + def as_declarations(self, field_name, declarations): + overrides = {} + for maybe_field, new_value in self.overrides.items(): + overrides[maybe_field] = Maybe( + decider=field_name, + yes_declaration=new_value, + no_declaration=declarations.get(maybe_field, None), + ) + return overrides def get_revdeps(self, parameters): """This might alter fields it's injecting.""" From 6f202077a5c8156fe96f8a028f883c14962f5b95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 01:42:48 +0200 Subject: [PATCH 232/714] [refactor] Add BuildStep, remove AttributeBuilder - Rename pre/post declarations - A BuildStep holds the whole building context - Centralize declaration parsing logic ERROR: Changed argument names in PostGeneration (public API) WARNING: Renamed _meta.pre_declarations/post_declarations WARNING: No longer possible to override a RelatedFactory value from a passed-in param (failing test, but no docs) --- docs/changelog.rst | 4 +- docs/internals.rst | 88 +++++++++ docs/reference.rst | 4 +- factory/base.py | 97 ++++------ factory/builder.py | 334 +++++++++++++++++++++++++++++++++++ factory/containers.py | 305 -------------------------------- factory/declarations.py | 165 ++++++++--------- factory/django.py | 19 +- factory/errors.py | 8 + factory/faker.py | 2 +- factory/fuzzy.py | 2 +- tests/__init__.py | 2 +- tests/test_base.py | 40 +++-- tests/test_containers.py | 214 ---------------------- tests/test_declarations.py | 8 +- tests/test_docs_internals.py | 133 ++++++++++++++ tests/test_using.py | 4 +- 17 files changed, 723 insertions(+), 706 deletions(-) create mode 100644 factory/builder.py delete mode 100644 factory/containers.py delete mode 100644 tests/test_containers.py create mode 100644 tests/test_docs_internals.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 7df5ea7e..8cfa6bb5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -292,8 +292,8 @@ This takes care of all ``FACTORY_FOR`` occurences; the files containing other at - 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 + - Add a :attr:`~factory.builder.Resolver.factory_parent` attribute to the + :class:`~factory.builder.Resolver` 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`) diff --git a/docs/internals.rst b/docs/internals.rst index a7402ff2..6cbf3cec 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -1,2 +1,90 @@ Internals ========= + +.. currentmodule:: factory + +Behind the scenes: steps performed when parsing a factory declaration, and when calling it. + + +This section will be based on the following factory declaration: + +.. literalinclude:: ../tests/test_docs_internals.py + :pyobject: UserFactory + + +Parsing, Step 1: Metaclass and type declaration +----------------------------------------------- + +1. Python parses the declaration and calls (thanks to the metaclass declaration): + + .. code-block:: python + + factory.base.BaseFactory.__new__( + 'UserFactory', + (factory.Factory,), + attributes, + ) + +2. That metaclass removes :attr:`~Factory.Meta` and :attr:`~Factory.Params` from the class attributes, + then generate the actual factory class (according to standard Python rules) +3. It initializes a :class:`FactoryOptions` object, and links it to the class + + +Parsing, Step 2: adapting the class definition +----------------------------------------------- + +1. The :class:`FactoryOptions` reads the options from the :attr:`class Meta ` declaration +2. It finds a few specific pointer (loading the model class, finding the reference + factory for the sequence counter, etc.) +3. It copies declarations and parameters from parent classes +4. It scans current class attributes (from ``vars()``) to detect pre/post declarations +5. Declarations are split among pre-declarations and post-declarations + (a raw value shadowing a post-declaration is seen as a post-declaration) + + +.. note:: A declaration for ``foo__bar`` will be converted into parameter ``bar`` + for declaration ``foo``. + + +Instantiating, Step 1: Converging entrypoints +--------------------------------------------- + +First, decide the strategy: + +- If the entrypoint is specific to a strategy (:meth:`~Factory.build`, + :meth:`~Factory.create_batch`, ...), use it +- If it is generic (:meth:`~Factory.generate`, :meth:`Factory.__call__`), + use the strategy defined at the :attr:`class Meta ` level + + +Then, we'll pass the strategy and passed-in overrides to the :meth:`~Factory._generate` method. + +.. note:: According to the project roadmap, a future version will use a :meth:`~Factory._generate_batch`` at its core instead. + +A factory's :meth:`~Factory._generate` function actually delegates to a ``StepBuilder()`` object. +This object will carry the overall "build an object" context (strategy, depth, and possibly other). + + +Instantiating, Step 2: Preparing values +--------------------------------------- + +1. The ``StepBuilder`` merges overrides with the class-level declarations +2. The sequence counter for this instance is initialized +3. A ``Resolver`` is set up with all those declarations, and parses them in order; + it will call each value's ``evaluate()`` method, including extra parameters. +4. If needed, the ``Resolver`` might recurse (through the ``StepBuilder``, e.g when + encountering a :class:`SubFactory`. + + +Instantiating, Step 3: Building the object +------------------------------------------ + +1. The ``StepBuilder`` fetches the attributes computed by the ``Resolver``. +2. It applies renaming/adjustment rules +3. It passes them to the :meth:`FactoryOptions.instantiate` method, which + forwards to the proper methods. +4. Post-declaration are applied (in declaration order) + + +.. note:: This document discusses implementation details; there is no guarantee that the + described methods names and signatures will be kept as is. diff --git a/docs/reference.rst b/docs/reference.rst index 94677a35..b257a5d6 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -804,7 +804,7 @@ accept the object being built as sole argument, and return a value. 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 +but instead a :class:`~builder.Resolver`: a temporary container that computes the value of all declared fields. @@ -1273,7 +1273,7 @@ Obviously, this "follow parents" ability also handles overriding some attributes This feature is also available to :class:`LazyAttribute` and :class:`LazyAttributeSequence`, -through the :attr:`~containers.LazyStub.factory_parent` attribute of the passed-in object: +through the :attr:`~builder.Resolver.factory_parent` attribute of the passed-in object: .. code-block:: python diff --git a/factory/base.py b/factory/base.py index c58d9e22..8123533b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -5,8 +5,9 @@ import collections import logging +import warnings -from . import containers +from . import builder from . import declarations from . import errors from . import utils @@ -148,21 +149,21 @@ class FactoryOptions(object): def __init__(self): self.factory = None self.base_factory = None - self.declarations = {} - self.postgen_declarations = {} + self.base_declarations = {} self.parameters = {} self.parameters_dependencies = {} + self.pre_declarations = builder.DeclarationSet() + self.post_declarations = builder.DeclarationSet() self._counter = None self.counter_reference = None @property - def sorted_postgen_declarations(self): - """Get sorted postgen declaration items.""" - return sorted( - self.postgen_declarations.items(), - key=lambda item: item[1].creation_counter, - ) + def declarations(self): + base_declarations = dict(self.base_declarations) + for name, param in self.parameters.items(): + base_declarations.update(param.as_declarations(name, base_declarations)) + return base_declarations def _build_default_options(self): """"Provide the default value for all allowed fields. @@ -215,18 +216,17 @@ def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=N self.counter_reference = self._get_counter_reference() + # Scan the inheritance chain, starting from the furthest point, + # excluding the current class, to retrieve all declarations. for parent in reversed(self.factory.__mro__[1:]): if not hasattr(parent, '_meta'): continue - self.declarations.update(parent._meta.declarations) - self.postgen_declarations.update(parent._meta.postgen_declarations) + self.base_declarations.update(parent._meta.base_declarations) self.parameters.update(parent._meta.parameters) for k, v in vars(self.factory).items(): - if self._is_declaration(k, v): - self.declarations[k] = v - if self._is_postgen_declaration(k, v): - self.postgen_declarations[k] = v + if self._is_declaration(k, v) or self._is_postgen_declaration(k, v): + self.base_declarations[k] = v if params is not None: for k, v in vars(params).items(): @@ -235,6 +235,7 @@ def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=N self._check_parameter_dependencies(self.parameters) + self.pre_declarations, self.post_declarations = builder.parse_declarations(self.declarations) def _get_counter_reference(self): """Identify which factory should be used for a shared counter.""" @@ -309,21 +310,21 @@ def prepare_arguments(self, attributes): return args, kwargs - def instantiate(self, strategy, args, kwargs): + def instantiate(self, step, args, kwargs): model = self.get_model_class() - if strategy == BUILD_STRATEGY: + if step.builder.strategy == BUILD_STRATEGY: return self.factory._build(model, *args, **kwargs) - elif strategy == CREATE_STRATEGY: + elif step.builder.strategy == CREATE_STRATEGY: return self.factory._create(model, *args, **kwargs) else: - assert strategy == STUB_STRATEGY + assert step.builder.strategy == STUB_STRATEGY return StubObject(**kwargs) - def use_postgeneration_results(self, create, instance, results): + def use_postgeneration_results(self, step, instance, results): self.factory._after_postgeneration( instance=instance, - create=create, + step=step, results=results, ) @@ -459,20 +460,15 @@ def attributes(cls, create=False, extra=None): applicable; the current list of computed attributes is available to the currently processed object. """ - force_sequence = None - if extra: - force_sequence = extra.pop('__sequence', None) - log_ctx = '%s.%s' % (cls.__module__, cls.__name__) - logger.debug( - "BaseFactory: Preparing %s.%s(extra=%s)", - cls.__module__, - cls.__name__, - utils.log_repr(extra), - ) - return containers.AttributeBuilder(cls, extra, log_ctx=log_ctx).build( - create=create, - force_sequence=force_sequence, + warnings.warn( + "Usage of Factory.attributes() is deprecated.", + DeprecationWarning, + stacklevel=2, ) + declarations = cls._meta.pre_declarations.as_dict() + declarations.update(extra or {}) + from . import helpers + return helpers.make_factory(dict, **declarations) @classmethod def declarations(cls, extra_defs=None): @@ -482,7 +478,12 @@ def declarations(cls, extra_defs=None): extra_defs (dict): additional definitions to insert into the retrieved DeclarationDict. """ - decls = cls._meta.declarations.copy() + warnings.warn( + "Factory.declarations is deprecated; use Factory._meta.pre_declarations instead.", + DeprecationWarning, + stacklevel=2, + ) + decls = cls._meta.pre_declarations.as_dict() decls.update(extra_defs or {}) return decls @@ -505,31 +506,11 @@ def _generate(cls, strategy, params): "Ensure %(f)s.Meta.model is set and %(f)s.Meta.abstract " "is either not set or False." % dict(f=cls.__name__)) - create = bool(strategy == CREATE_STRATEGY) - - attrs = cls.attributes(create=create, extra=params) - # Extract declarations used for post-generation - postgen_attributes = {} - - for name, decl in cls._meta.sorted_postgen_declarations: - postgen_attributes[name] = decl.extract(name, attrs) - - # Generate the object - args, kwargs = cls._meta.prepare_arguments(attrs) - obj = cls._meta.instantiate(strategy, args, kwargs) - - # Handle post-generation attributes - results = {} - for name, decl in cls._meta.sorted_postgen_declarations: - extraction_context = postgen_attributes[name] - results[name] = decl.call(obj, create, extraction_context) - - cls._meta.use_postgeneration_results(create, obj, results) - - return obj + step = builder.StepBuilder(cls._meta, params, strategy) + return step.build() @classmethod - def _after_postgeneration(cls, instance, create, results=None): + def _after_postgeneration(cls, instance, step, results=None): """Hook called after post-generation declarations have been handled. Args: diff --git a/factory/builder.py b/factory/builder.py new file mode 100644 index 00000000..30634007 --- /dev/null +++ b/factory/builder.py @@ -0,0 +1,334 @@ +"""Build factory instances.""" + +import collections + +from . import declarations +from . import errors + + +DeclarationContext = collections.namedtuple( + 'DeclarationContext', + ['name', 'declaration', 'context'], +) + + +class DeclarationSet(object): + """A set of declarations, including the recursive parameters.""" + SPLITTER = '__' + + def __init__(self, initial=None): + self.declarations = {} + self.contexts = collections.defaultdict(dict) + self.update(initial or {}) + + @classmethod + def split(cls, entry): + if cls.SPLITTER in entry: + return entry.split(cls.SPLITTER, 1) + else: + return (entry, None) + + @classmethod + def join(cls, root, subkey): + return cls.SPLITTER.join((root, subkey)) + + def copy(self): + other = self.__class__() + other.declarations = self.declarations.copy() + other.contexts = self.contexts.copy() + return other + + def update(self, values): + deeps = collections.defaultdict(dict) + remainder = {} + for k, v in values.items(): + root, sub = self.split(k) + if sub is None: + self.declarations[root] = v + else: + self.contexts[root][sub] = v + + extra_context_keys = set(self.contexts) - set(self.declarations) + if extra_context_keys: + raise errors.InvalidDeclarationError( + "Received deep context for unknown fields: %r (known=%r)" % ( + { + self.join(root, sub): v + for root in extra_context_keys + for sub, v in self.contexts[root].items() + }, + sorted(self.declarations), + ) + ) + + def filter(self, entries): + return [ + entry for entry in entries + if self.split(entry)[0] in self.declarations + ] + + def sorted(self): + return sorted( + self.declarations, + key=lambda entry: self.declarations[entry].creation_counter, + ) + + def __contains__(self, key): + return key in self.declarations + + def __getitem__(self, key): + return DeclarationContext( + name=key, + declaration=self.declarations[key], + context=self.contexts[key], + ) + + def __iter__(self): + return iter(self.declarations) + + def _items(self): + """Extract a list of (key, value) pairs, suitable for our __init__.""" + for name in self.declarations: + yield name, self.declarations[name] + for subkey, value in self.contexts[name].items(): + yield self.join(name, subkey), value + + def values(self): + for name in self: + yield self[name] + + def as_dict(self): + return dict(self._items()) + + def __repr__(self): + return '' % self.as_dict() + + +class FakePostGenerationDeclaration(declarations.PostGenerationDeclaration): + """A fake post-generation declaration, providing simply a hardcoded value. + + Used to disable post-generation when the user has overridden a method. + """ + def __init__(self, value): + self.value = value + + def call(self, instance, step, context): + return self.value + + +def parse_declarations(decls, base_pre=None, base_post=None): + pre_declarations = base_pre.copy() if base_pre else DeclarationSet() + post_declarations = base_post.copy() if base_post else DeclarationSet() + + # Inject extra declarations, splitting between known-to-be-post and undetermined + extra_post = {} + extra_maybenonpost = {} + for k, v in decls.items(): + if isinstance(v, declarations.PostGenerationDeclaration): + if k in pre_declarations: + # Conflict: PostGenerationDeclaration with the same + # name as a BaseDeclaration + raise errors.InvalidDeclarationError( + "PostGenerationDeclaration %s=%r shadows declaration %r" + % (k, v, pre_declarations[k]) + ) + extra_post[k] = v + else: + extra_maybenonpost[k] = v + + # Start with adding new post-declarations + post_declarations.update(extra_post) + + # Fill in extra post-declaration context + post_overrides = post_declarations.filter(extra_maybenonpost) + post_declarations.update({ + # Set foo__bar as foo__foo__bar, in order to build an ExtractionContext + # later on + post_declarations.join(post_declarations.split(k)[0], k): v + for k, v in extra_maybenonpost.items() + if k in post_overrides + }) + + # Anything else is pre_declarations + pre_declarations.update({ + k: v + for k, v in extra_maybenonpost.items() + if k not in post_overrides + }) + + return pre_declarations, post_declarations + + +class BuildStep(object): + def __init__(self, builder, sequence, parent_step=None): + self.builder = builder + self.sequence = sequence + self.attributes = {} + self.parent_step = parent_step + self.stub = None + + def resolve(self, declarations): + self.stub = Resolver( + declarations=declarations, + step=self, + sequence=self.sequence, + ) + + for field_name in declarations: + self.attributes[field_name] = getattr(self.stub, field_name) + + @property + def chain(self): + if self.parent_step: + parent_chain = self.parent_step.chain + else: + parent_chain = () + return (self.stub,) + parent_chain + + def recurse(self, factory, declarations, force_sequence=None): + builder = self.builder.recurse(factory._meta, declarations) + return builder.build(parent_step=self, force_sequence=force_sequence) + + +class StepBuilder(object): + """A factory instantiation step. + + Attributes: + - parent: the parent StepBuilder, or None for the root step + - extras: the passed-in kwargs for this branch + - factory: the factory class being built + - strategy: the strategy to use + """ + def __init__(self, factory_meta, extras, strategy): + self.factory_meta = factory_meta + self.strategy = strategy + self.extras = extras + self.force_init_sequence = extras.pop('__sequence', None) + + def build(self, parent_step=None, force_sequence=None): + """Build a factory instance.""" + # TODO: Handle "batch build" natively + pre, post = parse_declarations( + self.extras, + base_pre=self.factory_meta.pre_declarations, + base_post=self.factory_meta.post_declarations, + ) + + if force_sequence is not None: + sequence = force_sequence + elif self.force_init_sequence is not None: + sequence = self.force_init_sequence + else: + sequence = self.factory_meta.next_sequence() + + step = BuildStep( + builder=self, + sequence=sequence, + parent_step=parent_step, + ) + step.resolve(pre) + + args, kwargs = self.factory_meta.prepare_arguments(step.attributes) + + instance = self.factory_meta.instantiate( + step=step, + args=args, + kwargs=kwargs, + ) + + postgen_results = {} + for declaration_name in post.sorted(): + declaration = post[declaration_name] + context = declaration.declaration.extract(declaration.name, declaration.context) + postgen_results[declaration_name] = declaration.declaration.call( + instance=instance, + step=step, + context=context, + ) + self.factory_meta.use_postgeneration_results( + instance=instance, + step=step, + results=postgen_results, + ) + return instance + + def recurse(self, factory_meta, extras): + """Recurse into a sub-factory call.""" + return self.__class__(factory_meta, extras, strategy=self.strategy) + + +class Resolver(object): + """Resolve a set of declarations. + + Attributes are set at instantiation time, values are computed lazily. + + Attributes: + __initialized (bool): whether this object's __init__ as run. If set, + setting any attribute will be prevented. + __declarations (dict): maps attribute name to their declaration + __values (dict): maps attribute name to computed value + __pending (str list): names of the attributes whose value is being + computed. This allows to detect cyclic lazy attribute definition. + __step (BuildStep): the BuildStep related to this resolver. + This allows to have the value of a field depend on the value of + another field + """ + + __initialized = False + + def __init__(self, declarations, step, sequence): + self.__declarations = declarations + self.__step = step + + self.__values = {} + self.__pending = [] + + self.__initialized = True + + @property + def factory_parent(self): + return self.__step.parent_step.stub if self.__step.parent_step else None + + def __repr__(self): + return '' % self.__step + + def __getattr__(self, name): + """Retrieve an attribute's value. + + This will compute it if needed, unless it is already on the list of + attributes being computed. + """ + if name in self.__pending: + raise errors.CyclicDefinitionError( + "Cyclic lazy attribute definition for %r; cycle found in %r." % + (name, self.__pending)) + elif name in self.__values: + return self.__values[name] + elif name in self.__declarations: + declaration = self.__declarations[name] + value = declaration.declaration + if isinstance(value, declarations.BaseDeclaration): + self.__pending.append(name) + try: + value = value.evaluate( + instance=self, + step=self.__step, + extra=declaration.context, + ) + finally: + last = self.__pending.pop() + assert name == last + + self.__values[name] = value + return value + else: + raise AttributeError( + "The parameter %r is unknown. Evaluated attributes are %r, " + "definitions are %r." % (name, self.__values, self.__declarations)) + + def __setattr__(self, name, value): + """Prevent setting attributes once __init__ is done.""" + if not self.__initialized: + return super(Resolver, self).__setattr__(name, value) + else: + raise AttributeError('Setting of object attributes is not allowed') diff --git a/factory/containers.py b/factory/containers.py deleted file mode 100644 index c210f24b..00000000 --- a/factory/containers.py +++ /dev/null @@ -1,305 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: See the LICENSE file. - -from __future__ import unicode_literals - -import logging - -from . import declarations -from . import errors -from . import utils - -logger = logging.getLogger(__name__) - - -class LazyStub(object): - """A generic container that only allows getting attributes. - - Attributes are set at instantiation time, values are computed lazily. - - Attributes: - __initialized (bool): whether this object's __init__ as run. If set, - setting any attribute will be prevented. - __attrs (dict): maps attribute name to their declaration - __values (dict): maps attribute name to computed value - __pending (str list): names of the attributes whose value is being - computed. This allows to detect cyclic lazy attribute definition. - __containers (LazyStub list): "parents" of the LazyStub being built. - This allows to have the field of a field depend on the value of - another field - __model_class (type): the model class to build. - """ - - __initialized = False - - def __init__(self, attrs, containers=(), model_class=object, log_ctx=None): - self.__attrs = attrs - self.__values = {} - self.__pending = [] - self.__containers = containers - self.__model_class = model_class - self.__log_ctx = log_ctx or '%s.%s' % (model_class.__module__, model_class.__name__) - self.factory_parent = containers[0] if containers else None - self.__initialized = True - - def __repr__(self): - return '' % (self.__model_class.__module__, self.__model_class.__name__) - - def __str__(self): - return '' % ( - self.__model_class.__name__, list(self.__attrs.keys())) - - def __fill__(self): - """Fill this LazyStub, computing values of all defined attributes. - - Retunrs: - 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): - """Retrieve an attribute's value. - - This will compute it if needed, unless it is already on the list of - attributes being computed. - """ - if name in self.__pending: - raise errors.CyclicDefinitionError( - "Cyclic lazy attribute definition for %s; cycle found in %r." % - (name, self.__pending)) - elif name in self.__values: - return self.__values[name] - elif name in self.__attrs: - val = self.__attrs[name] - if isinstance(val, LazyValue): - self.__pending.append(name) - try: - val = val.evaluate(self, self.__containers) - finally: - last = self.__pending.pop() - assert name == last - self.__values[name] = val - return val - else: - raise AttributeError( - "The parameter %s is unknown. Evaluated attributes are %r, " - "definitions are %r." % (name, self.__values, self.__attrs)) - - def __setattr__(self, name, value): - """Prevent setting attributes once __init__ is done.""" - if not self.__initialized: - return super(LazyStub, self).__setattr__(name, value) - else: - raise AttributeError('Setting of object attributes is not allowed') - - -class DeclarationStack(object): - """An ordered stack of declarations. - - This is intended to handle declaration precedence among different mutating layers. - """ - def __init__(self, ordering): - self.ordering = ordering - self.layers = dict((name, {}) for name in self.ordering) - - def __getitem__(self, key): - return self.layers[key] - - def __setitem__(self, key, value): - assert key in self.ordering - self.layers[key] = value - - def current(self): - """Retrieve the current, flattened declarations dict.""" - result = {} - for layer in self.ordering: - result.update(self.layers[layer]) - return result - - -class ParameterResolver(object): - """Resolve a factory's parameter declarations.""" - def __init__(self, parameters, deps): - self.parameters = parameters - self.deps = deps - self.declaration_stack = None - - self.resolved = set() - - def resolve_one(self, name): - """Compute one field is needed, taking dependencies into accounts.""" - if name in self.resolved: - return - - for dep in self.deps.get(name, ()): - self.resolve_one(dep) - - self.compute(name) - self.resolved.add(name) - - def compute(self, name): - """Actually compute the value for a given name.""" - value = self.parameters[name] - if isinstance(value, declarations.Parameter): - overrides = value.as_declarations(name, self.declaration_stack.current()) - else: - overrides = {name: value} - self.declaration_stack['overrides'].update(overrides) - - def resolve(self, declaration_stack): - """Resolve parameters for a given declaration stack. - - Modifies the stack in-place. - """ - self.declaration_stack = declaration_stack - for name in self.parameters: - self.resolve_one(name) - - -class LazyValue(object): - """Some kind of "lazy evaluating" object.""" - - def evaluate(self, obj, containers=()): # pragma: no cover - """Compute the value, using the given object.""" - raise NotImplementedError("This is an abstract method.") - - -class DeclarationWrapper(LazyValue): - """Lazy wrapper around an OrderedDeclaration. - - Attributes: - declaration (declarations.OrderedDeclaration): the OrderedDeclaration - being wrapped - sequence (int): the sequence counter to use when evaluatin the - declaration - """ - - def __init__(self, declaration, sequence, create, extra=None, **kwargs): - super(DeclarationWrapper, self).__init__(**kwargs) - self.declaration = declaration - self.sequence = sequence - self.create = create - self.extra = extra - - def evaluate(self, obj, containers=()): - """Lazily evaluate the attached OrderedDeclaration. - - Args: - obj (LazyStub): the object being built - containers (object list): the chain of containers of the object - being built, its immediate holder being first. - """ - return self.declaration.evaluate( - self.sequence, obj, - create=self.create, - extra=self.extra, - containers=containers, - ) - - def __repr__(self): - return '<%s for %r>' % (self.__class__.__name__, self.declaration) - - -class AttributeBuilder(object): - """Builds attributes from a factory and extra data. - - Attributes: - factory (base.Factory): the Factory for which attributes are being - built - _declarations (DeclarationDict): the attribute declarations for the factory - _subfields (dict): dict mapping an attribute name to a dict of - overridden default values for the related SubFactory. - """ - - def __init__(self, factory, extra=None, log_ctx=None, **kwargs): - super(AttributeBuilder, self).__init__(**kwargs) - - if not extra: - extra = {} - - self.factory = factory - self._containers = extra.pop('__containers', ()) - - initial_declarations = dict(factory._meta.declarations) - self._log_ctx = log_ctx - - # Parameters - # ---------- - self._declarations = self.merge_declarations(initial_declarations, extra) - - # Subfields - # --------- - - attrs_with_subfields = [ - k for k, v in initial_declarations.items() - if self.has_subfields(v) - ] - - # Extract subfields; THIS MODIFIES self._declarations. - self._subfields = utils.multi_extract_dict( - attrs_with_subfields, self._declarations) - - def has_subfields(self, value): - return isinstance(value, declarations.ParameteredAttribute) - - def merge_declarations(self, initial, extra): - """Compute the final declarations, taking into account paramter-based overrides.""" - # Precedence order: - # - Start with class-level declarations - # - Add overrides from parameters - # - Finally, use callsite-level declarations & values - declaration_stack = DeclarationStack(['initial', 'overrides', 'extra']) - declaration_stack['initial'] = initial.copy() - declaration_stack['extra'] = extra.copy() - - # Actually compute the final stack - resolver = ParameterResolver( - parameters=self.factory._meta.parameters, - deps=self.factory._meta.parameters_dependencies, - ) - resolver.resolve(declaration_stack) - return declaration_stack.current() - - def build(self, create, force_sequence=None): - """Build a dictionary of attributes. - - Args: - create (bool): whether to 'build' or 'create' the subfactories. - force_sequence (int or None): if set to an int, use this value for - the sequence counter; don't advance the related counter. - """ - # Setup factory sequence. - if force_sequence is None: - sequence = self.factory._meta.next_sequence() - else: - sequence = force_sequence - - # Parse attribute declarations, wrapping SubFactory and - # OrderedDeclaration. - wrapped_attrs = {} - for k, v in self._declarations.items(): - if isinstance(v, declarations.BaseDeclaration): - v = DeclarationWrapper( - v, - sequence=sequence, - create=create, - extra=self._subfields.get(k, {}), - ) - wrapped_attrs[k] = v - - stub = LazyStub( - wrapped_attrs, containers=self._containers, - model_class=self.factory, log_ctx=self._log_ctx) - return stub.__fill__() diff --git a/factory/declarations.py b/factory/declarations.py index 283056a5..c18ed8c9 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals +import collections import itertools import logging @@ -28,20 +29,15 @@ def __init__(self, **kwargs): self.creation_counter = BaseDeclaration.creation_counter BaseDeclaration.creation_counter += 1 - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): """Evaluate this declaration. Args: - sequence (int): the current sequence counter to use when filling - the current instance - obj (containers.LazyStub): The object holding currently computed + instance (builder.Resolver): The object holding currently computed attributes - containers (list of containers.LazyStub): The chain of SubFactory - which led to building this object. - create (bool): whether the model class should be 'built' or - 'created' - extra (DeclarationDict or None): extracted key/value extracted from - the attribute prefix + step: a factory.builder.BuildStep + extra (dict): additional, call-time added kwargs + for the step. """ raise NotImplementedError('This is an abstract method') @@ -64,8 +60,8 @@ def __init__(self, function, *args, **kwargs): super(LazyFunction, self).__init__(*args, **kwargs) self.function = function - def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyFunction: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(obj)) + def evaluate(self, instance, step, extra): + logger.debug("LazyFunction: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(step)) return self.function() @@ -81,9 +77,9 @@ def __init__(self, function, *args, **kwargs): super(LazyAttribute, self).__init__(*args, **kwargs) self.function = function - def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("LazyAttribute: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(obj)) - return self.function(obj) + def evaluate(self, instance, step, extra): + logger.debug("LazyAttribute: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(instance)) + return self.function(instance) class _UNSPECIFIED(object): @@ -141,12 +137,12 @@ def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs): self.attribute_name = attribute_name self.default = default - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): if self.depth > 1: # Fetching from a parent - target = containers[self.depth - 2] + target = step.chain[self.depth - 1] else: - target = obj + target = instance logger.debug("SelfAttribute: Picking attribute %r on %s", self.attribute_name, utils.log_repr(target)) return deepgetattr(target, self.attribute_name, self.default) @@ -179,7 +175,7 @@ def __init__(self, iterator, cycle=True, getter=None): else: self.iterator_builder = lambda: utils.ResetableIterator(iterator) - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): # Begin unrolling as late as possible. # This helps with ResetableIterator(MyModel.objects.all()) if self.iterator is None: @@ -212,9 +208,9 @@ def __init__(self, function, type=int): self.function = function self.type = type - def evaluate(self, sequence, obj, create, extra=None, containers=()): - logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, sequence) - return self.function(self.type(sequence)) + def evaluate(self, instance, step, extra): + logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, step.sequence) + return self.function(self.type(step.sequence)) class LazyAttributeSequence(Sequence): @@ -226,11 +222,11 @@ class LazyAttributeSequence(Sequence): type (function): A function converting an integer into the expected kind of counter for the 'function' attribute. """ - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): logger.debug( "LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%s", - self.function, sequence, utils.log_repr(obj)) - return self.function(obj, self.type(sequence)) + self.function, step.sequence, utils.log_repr(instance)) + return self.function(instance, self.type(step.sequence)) class ContainerAttribute(BaseDeclaration): @@ -247,7 +243,7 @@ def __init__(self, function, strict=True, *args, **kwargs): self.function = function self.strict = strict - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): """Evaluate the current ContainerAttribute. Args: @@ -257,12 +253,14 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): being evaluated in a chain, each item being a future field of next one. """ - if self.strict and not containers: + # Strip the current instance from the chain + chain = step.chain[1:] + if self.strict and not chain: raise TypeError( "A ContainerAttribute in 'strict' mode can only be used " "within a SubFactory.") - return self.function(obj, containers) + return self.function(instance, chain) class ParameteredAttribute(BaseDeclaration): @@ -293,7 +291,7 @@ def _prepare_containers(self, obj, containers=()): return containers - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): """Evaluate the current definition and fill its attributes. Uses attributes definition in the following order: @@ -301,23 +299,19 @@ def evaluate(self, sequence, obj, create, extra=None, containers=()): - additional values defined when instantiating the containing factory Args: - create (bool): whether the parent factory is being 'built' or - 'created' - extra (containers.DeclarationDict): extra values that should - override the defaults - containers (list of LazyStub): List of LazyStub for the chain of - factories being evaluated, the calling stub being first. + instance (builder.Resolver): The object holding currently computed + attributes + step: a factory.builder.BuildStep + extra (dict): additional, call-time added kwargs + for the step. """ defaults = dict(self.defaults) if extra: defaults.update(extra) - if self.CONTAINERS_FIELD: - containers = self._prepare_containers(obj, containers) - defaults[self.CONTAINERS_FIELD] = containers - return self.generate(sequence, obj, create, defaults) + return self.generate(step, defaults) - def generate(self, sequence, obj, create, params): # pragma: no cover + def generate(self, step, params): """Actually generate the related attribute. Args: @@ -378,6 +372,7 @@ class SubFactory(ParameteredAttribute): """ EXTEND_CONTAINERS = True + FORCE_SEQUENCE = False def __init__(self, factory, **kwargs): super(SubFactory, self).__init__(**kwargs) @@ -387,58 +382,43 @@ def get_factory(self): """Retrieve the wrapped factory.Factory subclass.""" return self.factory_wrapper.get() - def generate(self, sequence, obj, create, params): + def generate(self, step, params): """Evaluate the current definition and fill its attributes. Args: - create (bool): whether the subfactory should call 'build' or - 'create' - params (containers.DeclarationDict): extra values that should - override the wrapped factory's defaults + step: a factory.builder.BuildStep + params (dict): additional, call-time added kwargs + for the step. """ subfactory = self.get_factory() logger.debug( "SubFactory: Instantiating %s.%s(%s), create=%r", subfactory.__module__, subfactory.__name__, utils.log_pprint(kwargs=params), - create, + step, ) - return subfactory.simple_generate(create, **params) + force_sequence = step.sequence if self.FORCE_SEQUENCE else None + return step.recurse(subfactory, params, force_sequence=force_sequence) class Dict(SubFactory): """Fill a dict with usual declarations.""" + FORCE_SEQUENCE = True + def __init__(self, params, dict_factory='factory.DictFactory'): super(Dict, self).__init__(dict_factory, **dict(params)) - 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) - class List(SubFactory): """Fill a list with standard declarations.""" + FORCE_SEQUENCE = True + def __init__(self, params, list_factory='factory.ListFactory'): params = dict((str(i), v) for i, v in enumerate(params)) super(List, self).__init__(list_factory, **params) - 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) - # Parameters # ========== @@ -454,17 +434,15 @@ def __init__(self, decider, yes_declaration, no_declaration=None): self.yes = yes_declaration self.no = no_declaration - def evaluate(self, sequence, obj, create, extra=None, containers=()): - decider = getattr(obj, self.decider, None) + def evaluate(self, instance, step, extra): + decider = getattr(instance, self.decider, None) target = self.yes if decider else self.no if isinstance(target, BaseDeclaration): return target.evaluate( - sequence=sequence, - obj=obj, - create=create, + instance=instance, + step=step, extra=extra, - containers=containers, ) else: # Flat value @@ -588,13 +566,13 @@ def extract(self, name, attrs): kwargs = utils.extract_dict(name, attrs) return ExtractionContext(extracted, did_extract, kwargs, name) - def call(self, obj, create, extraction_context): # pragma: no cover + def call(self, instance, step, context): # pragma: no cover """Call this hook; no return value is expected. Args: obj (object): the newly generated object create (bool): whether the object was 'built' or 'created' - extraction_context: An ExtractionContext containing values + context: An ExtractionContext containing values extracted from the containing factory's declaration """ raise NotImplementedError() @@ -606,19 +584,18 @@ def __init__(self, function): super(PostGeneration, self).__init__() self.function = function - def call(self, obj, create, extraction_context): + def call(self, instance, step, context): logger.debug( "PostGeneration: Calling %s.%s(%s)", self.function.__module__, self.function.__name__, utils.log_pprint( - (obj, create, extraction_context.value), - extraction_context.extra, + (instance, step), + context, ), ) return self.function( - obj, create, - extraction_context.value, **extraction_context.extra) + instance, step, context.value, **context.extra) class RelatedFactory(PostGenerationDeclaration): @@ -642,30 +619,30 @@ def get_factory(self): """Retrieve the wrapped factory.Factory subclass.""" return self.factory_wrapper.get() - def call(self, obj, create, extraction_context): + def call(self, instance, step, context): factory = self.get_factory() - if extraction_context.did_extract: + if context.did_extract: # The user passed in a custom value logger.debug( "RelatedFactory: Using provided %s instead of generating %s.%s.", - utils.log_repr(extraction_context.value), + utils.log_repr(context.value), factory.__module__, factory.__name__, ) - return extraction_context.value + return context.value passed_kwargs = dict(self.defaults) - passed_kwargs.update(extraction_context.extra) + passed_kwargs.update(context.extra) if self.name: - passed_kwargs[self.name] = obj + passed_kwargs[self.name] = instance logger.debug( "RelatedFactory: Generating %s.%s(%s)", factory.__module__, factory.__name__, - utils.log_pprint((create,), passed_kwargs), + utils.log_pprint((step,), passed_kwargs), ) - return factory.simple_generate(create, **passed_kwargs) + return step.recurse(factory, passed_kwargs) class PostGenerationMethodCall(PostGenerationDeclaration): @@ -687,22 +664,22 @@ def __init__(self, method_name, *args, **kwargs): self.method_args = args self.method_kwargs = kwargs - def call(self, obj, create, extraction_context): - if not extraction_context.did_extract: + def call(self, instance, step, context): + if not context.did_extract: passed_args = self.method_args elif len(self.method_args) <= 1: # Max one argument expected - passed_args = (extraction_context.value,) + passed_args = (context.value,) else: - passed_args = tuple(extraction_context.value) + passed_args = tuple(context.value) passed_kwargs = dict(self.method_kwargs) - passed_kwargs.update(extraction_context.extra) - method = getattr(obj, self.method_name) + passed_kwargs.update(context.extra) + method = getattr(instance, self.method_name) logger.debug( "PostGenerationMethodCall: Calling %s.%s(%s)", - utils.log_repr(obj), + utils.log_repr(instance), self.method_name, utils.log_pprint(passed_args, passed_kwargs), ) diff --git a/factory/django.py b/factory/django.py index 7535549c..0860bbbb 100644 --- a/factory/django.py +++ b/factory/django.py @@ -155,8 +155,8 @@ def _get_or_create(cls, model_class, *args, **kwargs): key_fields[field] = kwargs.pop(field) key_fields['defaults'] = kwargs - obj, _created = manager.get_or_create(*args, **key_fields) - return obj + instance, _created = manager.get_or_create(*args, **key_fields) + return instance @classmethod def _create(cls, model_class, *args, **kwargs): @@ -169,22 +169,21 @@ def _create(cls, model_class, *args, **kwargs): return manager.create(*args, **kwargs) @classmethod - def _after_postgeneration(cls, instance, create, results=None): + def _after_postgeneration(cls, instance, step, results=None): """Save again the instance if creating and at least one hook ran.""" - if create and results: + if step.builder.strategy == base.CREATE_STRATEGY and results: # Some post-generation hooks ran, and may have modified us. instance.save() -class FileField(declarations.ParameteredAttribute): +class FileField(declarations.Dict): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' - EXTEND_CONTAINERS = True def __init__(self, **defaults): require_django() - super(FileField, self).__init__(**defaults) + super(FileField, self).__init__(defaults) def _make_data(self, params): """Create data for the field.""" @@ -227,11 +226,9 @@ def _make_content(self, params): filename = params.get('filename', default_filename) return filename, content - def generate(self, sequence, obj, create, params): + def generate(self, step, params): """Fill in the field.""" - - params.setdefault('__sequence', sequence) - params = base.DictFactory.simple_generate(create, **params) + params = super(FileField, self).generate(step, params) filename, content = self._make_content(params) return django_files.File(content.file, filename) diff --git a/factory/errors.py b/factory/errors.py index aecdadf1..63d75487 100644 --- a/factory/errors.py +++ b/factory/errors.py @@ -20,3 +20,11 @@ class UnsupportedStrategy(FactoryError): class CyclicDefinitionError(FactoryError): """Raised when a cyclical declaration occurs.""" + + +class InvalidDeclarationError(FactoryError): + """Raised when a sub-declaration has no related declaration. + + This means that the user declared 'foo__bar' without adding a declaration + at 'foo'. + """ diff --git a/factory/faker.py b/factory/faker.py index 457cbe57..064863da 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -52,7 +52,7 @@ def generate(self, extra_kwargs): subfaker = self._get_faker(self.locale) return subfaker.format(self.provider, **kwargs) - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): return self.generate(extra or {}) _FAKER_REGISTRY = {} diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 094c438a..70742a5d 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -61,7 +61,7 @@ class BaseFuzzyAttribute(declarations.BaseDeclaration): def fuzz(self): # pragma: no cover raise NotImplementedError() - def evaluate(self, sequence, obj, create, extra=None, containers=()): + def evaluate(self, instance, step, extra): return self.fuzz() diff --git a/tests/__init__.py b/tests/__init__.py index a21e445c..f90d2d27 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -5,8 +5,8 @@ from .test_django import * from .test_base import * -from .test_containers import * from .test_declarations import * +from .test_docs_internals import * from .test_faker import * from .test_fuzzy import * from .test_helpers import * diff --git a/tests/test_base.py b/tests/test_base.py index 94233a49..a9f1acf9 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -109,8 +109,8 @@ class AbstractFactory(base.Factory): self.assertEqual(base.CREATE_STRATEGY, AbstractFactory._meta.strategy) # Non-declarative attributes - self.assertEqual({}, AbstractFactory._meta.declarations) - self.assertEqual({}, AbstractFactory._meta.postgen_declarations) + self.assertEqual({}, AbstractFactory._meta.pre_declarations.as_dict()) + self.assertEqual({}, AbstractFactory._meta.post_declarations.as_dict()) self.assertEqual(AbstractFactory, AbstractFactory._meta.factory) self.assertEqual(base.Factory, AbstractFactory._meta.base_factory) self.assertEqual(AbstractFactory._meta, AbstractFactory._meta.counter_reference) @@ -133,8 +133,14 @@ class AbstractFactory(base.Factory): self.assertEqual(postgen, AbstractFactory.z) # And are available in class Meta - self.assertEqual({'x': 1, 'y': lazy, 'y2': lazy2}, AbstractFactory._meta.declarations) - self.assertEqual({'z': postgen}, AbstractFactory._meta.postgen_declarations) + self.assertEqual( + {'x': 1, 'y': lazy, 'y2': lazy2}, + AbstractFactory._meta.pre_declarations.as_dict(), + ) + self.assertEqual( + {'z': postgen}, + AbstractFactory._meta.post_declarations.as_dict(), + ) def test_inherited_declaration_collecting(self): lazy = declarations.LazyFunction(int) @@ -159,8 +165,14 @@ class OtherFactory(AbstractFactory): self.assertEqual(postgen, OtherFactory.z) # And are available in class Meta - self.assertEqual({'x': 1, 'y': lazy, 'a': lazy2}, OtherFactory._meta.declarations) - self.assertEqual({'z': postgen, 'b': postgen2}, OtherFactory._meta.postgen_declarations) + self.assertEqual( + {'x': 1, 'y': lazy, 'a': lazy2}, + OtherFactory._meta.pre_declarations.as_dict(), + ) + self.assertEqual( + {'z': postgen, 'b': postgen2}, + OtherFactory._meta.post_declarations.as_dict(), + ) def test_inherited_declaration_shadowing(self): lazy = declarations.LazyFunction(int) @@ -183,8 +195,14 @@ class OtherFactory(AbstractFactory): self.assertEqual(postgen2, OtherFactory.z) # And are available in class Meta - self.assertEqual({'x': 1, 'y': lazy2}, OtherFactory._meta.declarations) - self.assertEqual({'z': postgen2}, OtherFactory._meta.postgen_declarations) + self.assertEqual( + {'x': 1, 'y': lazy2}, + OtherFactory._meta.pre_declarations.as_dict(), + ) + self.assertEqual( + {'z': postgen2}, + OtherFactory._meta.post_declarations.as_dict(), + ) class DeclarationParsingTests(unittest.TestCase): @@ -504,7 +522,7 @@ class Meta: foo = declarations.PostGenerationDeclaration() - self.assertIn('foo', TestObjectFactory._meta.postgen_declarations) + self.assertIn('foo', TestObjectFactory._meta.post_declarations.as_dict()) def test_classlevel_extraction(self): class TestObjectFactory(base.Factory): @@ -514,8 +532,8 @@ class Meta: foo = declarations.PostGenerationDeclaration() foo__bar = 42 - self.assertIn('foo', TestObjectFactory._meta.postgen_declarations) - self.assertIn('foo__bar', TestObjectFactory._meta.declarations) + self.assertIn('foo', TestObjectFactory._meta.post_declarations.as_dict()) + self.assertIn('foo__foo__bar', TestObjectFactory._meta.post_declarations.as_dict()) diff --git a/tests/test_containers.py b/tests/test_containers.py deleted file mode 100644 index 581b16d1..00000000 --- a/tests/test_containers.py +++ /dev/null @@ -1,214 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: See the LICENSE file. - -from factory import base -from factory import containers -from factory import declarations -from factory import errors - -from .compat import unittest - -class LazyStubTestCase(unittest.TestCase): - def test_basic(self): - stub = containers.LazyStub({'one': 1, 'two': 2}) - - self.assertEqual({'one': 1, 'two': 2}, stub.__fill__()) - - def test_setting_values(self): - stub = containers.LazyStub({'one': 1, 'two': 2}) - - self.assertRaises(AttributeError, setattr, stub, 'one', 1) - - def test_reading_value(self): - stub = containers.LazyStub({'one': 1, 'two': 2}) - self.assertEqual(1, stub.one) - - self.assertRaises(AttributeError, getattr, stub, 'three') - - def test_accessing_container(self): - class LazyAttr(containers.LazyValue): - def __init__(self, obj_attr, container_attr): - self.obj_attr = obj_attr - self.container_attr = container_attr - - def evaluate(self, obj, containers=()): - if containers: - add = getattr(containers[0], self.container_attr) - else: - add = 0 - return getattr(obj, self.obj_attr) + add - - class DummyContainer(object): - three = 3 - - stub = containers.LazyStub({'one': LazyAttr('two', 'three'), 'two': 2, 'three': 42}, - containers=(DummyContainer(),)) - - self.assertEqual(5, stub.one) - - stub = containers.LazyStub({'one': LazyAttr('two', 'three'), 'two': 2, 'three': 42}, - 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) - - class LazyAttr(containers.LazyValue): - def __init__(self, attrname): - self.attrname = attrname - - def evaluate(self, obj, container=None): - return 1 + getattr(obj, self.attrname) - - def test_cyclic_definition(self): - stub = containers.LazyStub({ - 'one': self.LazyAttr('two'), - 'two': self.LazyAttr('one'), - }) - - self.assertRaises(errors.CyclicDefinitionError, getattr, stub, 'one') - - def test_cyclic_definition_rescue(self): - class LazyAttrDefault(self.LazyAttr): - def __init__(self, attname, defvalue): - super(LazyAttrDefault, self).__init__(attname) - self.defvalue = defvalue - def evaluate(self, obj, container=None): - try: - return super(LazyAttrDefault, self).evaluate(obj, container) - except errors.CyclicDefinitionError: - return self.defvalue - - stub = containers.LazyStub({ - 'one': LazyAttrDefault('two', 10), - 'two': self.LazyAttr('one'), - }) - - self.assertEqual(10, stub.one) - self.assertEqual(11, stub.two) - - def test_representation(self): - class RandomObj(object): - pass - - stub = containers.LazyStub({'one': 1, 'two': 2}, model_class=RandomObj) - self.assertIn('RandomObj', repr(stub)) - self.assertIn('RandomObj', str(stub)) - self.assertIn('one', str(stub)) - - - -class AttributeBuilderTestCase(unittest.TestCase): - - def make_fake_factory(self, decls): - class Meta: - declarations = decls - parameters = {} - parameters_dependencies = {} - - @classmethod - def next_sequence(cls): - return 1 - - class FakeFactory(object): - _meta = Meta - - return FakeFactory - - def test_empty(self): - """Tests building attributes from an empty definition.""" - - FakeFactory = self.make_fake_factory({}) - ab = containers.AttributeBuilder(FakeFactory) - - self.assertEqual({}, ab.build(create=False)) - - def test_factory_defined(self): - FakeFactory = self.make_fake_factory({'one': 1}) - ab = containers.AttributeBuilder(FakeFactory) - - self.assertEqual({'one': 1}, ab.build(create=False)) - - def test_extended(self): - FakeFactory = self.make_fake_factory({'one': 1}) - ab = containers.AttributeBuilder(FakeFactory, {'two': 2}) - self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False)) - - def test_overridden(self): - FakeFactory = self.make_fake_factory({'one': 1}) - ab = containers.AttributeBuilder(FakeFactory, {'one': 2}) - self.assertEqual({'one': 2}, ab.build(create=False)) - - def test_factory_defined_sequence(self): - seq = declarations.Sequence(lambda n: 'xx%d' % n) - FakeFactory = self.make_fake_factory({'one': seq}) - - ab = containers.AttributeBuilder(FakeFactory) - self.assertEqual({'one': 'xx1'}, ab.build(create=False)) - - def test_additionnal_sequence(self): - seq = declarations.Sequence(lambda n: 'xx%d' % n) - FakeFactory = self.make_fake_factory({'one': 1}) - - ab = containers.AttributeBuilder(FakeFactory, extra={'two': seq}) - self.assertEqual({'one': 1, 'two': 'xx1'}, ab.build(create=False)) - - def test_replaced_sequence(self): - seq = declarations.Sequence(lambda n: 'xx%d' % n) - seq2 = declarations.Sequence(lambda n: 'yy%d' % n) - FakeFactory = self.make_fake_factory({'one': seq}) - - ab = containers.AttributeBuilder(FakeFactory, extra={'one': seq2}) - self.assertEqual({'one': 'yy1'}, ab.build(create=False)) - - def test_lazy_function(self): - lf = declarations.LazyFunction(int) - FakeFactory = self.make_fake_factory({'one': 1, 'two': lf}) - - ab = containers.AttributeBuilder(FakeFactory) - self.assertEqual({'one': 1, 'two': 0}, ab.build(create=False)) - - ab = containers.AttributeBuilder(FakeFactory, {'one': 4}) - self.assertEqual({'one': 4, 'two': 0}, ab.build(create=False)) - - ab = containers.AttributeBuilder(FakeFactory, {'one': 4, 'three': lf}) - self.assertEqual({'one': 4, 'two': 0, 'three': 0}, ab.build(create=False)) - - def test_lazy_attribute(self): - la = declarations.LazyAttribute(lambda a: a.one * 2) - FakeFactory = self.make_fake_factory({'one': 1, 'two': la}) - - ab = containers.AttributeBuilder(FakeFactory) - self.assertEqual({'one': 1, 'two': 2}, ab.build(create=False)) - - ab = containers.AttributeBuilder(FakeFactory, {'one': 4}) - self.assertEqual({'one': 4, 'two': 8}, ab.build(create=False)) - - ab = containers.AttributeBuilder(FakeFactory, {'one': 4, 'three': la}) - self.assertEqual({'one': 4, 'two': 8, 'three': 8}, ab.build(create=False)) - - def test_subfields(self): - class FakeInnerFactory(object): - pass - - sf = declarations.SubFactory(FakeInnerFactory) - FakeFactory = self.make_fake_factory({'one': sf, 'two': 2}) - - ab = containers.AttributeBuilder(FakeFactory, {'one__blah': 1, 'two__bar': 2}) - self.assertTrue(ab.has_subfields(sf)) - self.assertEqual(['one'], list(ab._subfields.keys())) - self.assertEqual(2, ab._declarations['two__bar']) - - def test_sub_factory(self): - pass - - -if __name__ == '__main__': # pragma: no cover - unittest.main() diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 389a925c..3294818a 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -133,7 +133,7 @@ def foo(*args, **kwargs): ) self.assertEqual(2, len(call_params)) - self.assertEqual(3, len(call_params[0])) # instance, created, context + self.assertEqual(3, len(call_params[0])) # instance, step, context.value self.assertEqual({'bar': 42}, call_params[1]) def test_decorator_simple(self): @@ -152,7 +152,7 @@ def foo(*args, **kwargs): ) self.assertEqual(2, len(call_params)) - self.assertEqual(3, len(call_params[0])) # instance, created, context + self.assertEqual(3, len(call_params[0])) # instance, step, context.value self.assertEqual({'bar': 42}, call_params[1]) @@ -304,9 +304,7 @@ def zz(*args, **kwargs): def aa(*args, **kwargs): postgen_results.append('aa') - postgen_names = [ - k for k, v in Ordered._meta.sorted_postgen_declarations - ] + postgen_names = Ordered._meta.post_declarations.sorted() self.assertEqual(postgen_names, ['a', 'z', 'a1', 'zz', 'aa']) # Test generation happens in desired order diff --git a/tests/test_docs_internals.py b/tests/test_docs_internals.py new file mode 100644 index 00000000..b329cae1 --- /dev/null +++ b/tests/test_docs_internals.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2011-2015 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +"""Tests for the docs/internals module.""" + +import datetime + +import factory +import factory.fuzzy +from factory.compat import UTC + +from .compat import unittest + + + +class User(object): + def __init__(self, + username, + full_name, + is_active=True, + is_superuser=False, + is_staff=False, + creation_date=None, + deactivation_date=None, + ): + self.username = username + self.full_name = full_name + self.is_active = is_active + self.is_superuser = is_superuser + self.is_staff = is_staff + self.creation_date = creation_date + self.deactivation_date = deactivation_date + self.logs = [] + + def log(self, action, timestamp): + UserLog(user=self, action=action, timestamp=timestamp) + + +class UserLog(object): + + ACTIONS = ['create', 'update', 'disable'] + + def __init__(self, user, action, timestamp): + self.user = user + self.action = action + self.timestamp = timestamp + + user.logs.append(self) + + +class UserLogFactory(factory.Factory): + class Meta: + model = UserLog + + user = factory.SubFactory('test_docs_internals.UserFactory') + timestamp = factory.fuzzy.FuzzyDateTime( + datetime.datetime(2000, 1, 1, tzinfo=UTC), + ) + action = factory.Iterator(UserLog.ACTIONS) + + +class UserFactory(factory.Factory): + class Meta: + model = User + + class Params: + # Allow us to quickly enable staff/superuser flags + superuser = factory.Trait( + is_superuser=True, + is_staff=True, + ) + # Meta parameter handling all 'enabled'-related fields + enabled = True + + # Classic fields + username = factory.Faker('user_name') + full_name = factory.Faker('name') + creation_date = factory.fuzzy.FuzzyDateTime( + datetime.datetime(2000, 1, 1, tzinfo=UTC), + datetime.datetime(2015, 12, 31, 20, tzinfo=UTC) + ) + + # Conditional flags + is_active = factory.SelfAttribute('enabled') + deactivation_date = factory.Maybe( + 'enabled', + None, + factory.fuzzy.FuzzyDateTime( +# factory.SelfAttribute('creation_date'), + datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=10), + datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=1), + ), + ) + + # Related logs + creation_log = factory.RelatedFactory( + UserLogFactory, 'user', + action='create', timestamp=factory.SelfAttribute('user.creation_date'), + ) + + +class DocsInternalsTests(unittest.TestCase): + def test_simple_usage(self): + user = UserFactory() + + # Default user should be active, not super + self.assertTrue(user.is_active) + self.assertFalse(user.is_superuser) + self.assertFalse(user.is_staff) + + # We should have one log + self.assertEqual(1, len(user.logs)) + # And it should be a 'create' action linked to the user's creation_date + self.assertEqual('create', user.logs[0].action) + self.assertEqual(user, user.logs[0].user) + self.assertEqual(user.creation_date, user.logs[0].timestamp) diff --git a/tests/test_using.py b/tests/test_using.py index eca812e4..5162ff8b 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1922,7 +1922,7 @@ def _after_postgeneration(cls, obj, create, results): obj = TestObjectFactory.build() self.assertEqual(2, obj.one) - self.assertFalse(obj.create) + self.assertEqual(factory.BUILD_STRATEGY, obj.step.builder.strategy) self.assertEqual({'incr_one': 42}, obj.results) def test_post_generation_extraction(self): @@ -1944,6 +1944,7 @@ def incr_one(self, _create, increment=1): self.assertEqual(4, obj.one) self.assertFalse(hasattr(obj, 'incr_one')) + @unittest.expectedFailure # Broken API in refactor def test_post_generation_extraction_lambda(self): def my_lambda(obj, create, extracted, **kwargs): @@ -2110,6 +2111,7 @@ class Meta: self.assertEqual(1, obj.related.one) self.assertEqual(4, obj.related.two) + @unittest.expectedFailure # Broken undocumented hack in refactor def test_parameterized_related_factory(self): class TestRelatedObject(object): def __init__(self, obj=None, one=None, two=None): From 8dadbe20e845ae7e311edf2cefc4ce9e24c25370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 14:15:26 +0200 Subject: [PATCH 233/714] [refactor] Handle post-gen context in builder Move the extraction into the builder, instead of offloading it to declarations. This ensures that internal mechanisms are kept consistent, and live in a single place. --- factory/builder.py | 29 ++++++++++++++++++-------- factory/declarations.py | 45 +++-------------------------------------- tests/test_base.py | 2 +- 3 files changed, 25 insertions(+), 51 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index 30634007..34b12696 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -6,12 +6,18 @@ from . import errors -DeclarationContext = collections.namedtuple( - 'DeclarationContext', +DeclarationWithContext = collections.namedtuple( + 'DeclarationWithContext', ['name', 'declaration', 'context'], ) +PostGenerationContext = collections.namedtuple( + 'PostGenerationContext', + ['value_provided', 'value', 'extra'], +) + + class DeclarationSet(object): """A set of declarations, including the recursive parameters.""" SPLITTER = '__' @@ -77,7 +83,7 @@ def __contains__(self, key): return key in self.declarations def __getitem__(self, key): - return DeclarationContext( + return DeclarationWithContext( name=key, declaration=self.declarations[key], context=self.contexts[key], @@ -133,6 +139,11 @@ def parse_declarations(decls, base_pre=None, base_post=None): % (k, v, pre_declarations[k]) ) extra_post[k] = v + elif k in post_declarations: + # Passing in a scalar value to a PostGenerationDeclaration + # Set it as `key__` + magic_key = post_declarations.join(k, '') + extra_post[magic_key] = v else: extra_maybenonpost[k] = v @@ -142,9 +153,7 @@ def parse_declarations(decls, base_pre=None, base_post=None): # Fill in extra post-declaration context post_overrides = post_declarations.filter(extra_maybenonpost) post_declarations.update({ - # Set foo__bar as foo__foo__bar, in order to build an ExtractionContext - # later on - post_declarations.join(post_declarations.split(k)[0], k): v + k: v for k, v in extra_maybenonpost.items() if k in post_overrides }) @@ -239,11 +248,15 @@ def build(self, parent_step=None, force_sequence=None): postgen_results = {} for declaration_name in post.sorted(): declaration = post[declaration_name] - context = declaration.declaration.extract(declaration.name, declaration.context) + postgen_context = PostGenerationContext( + value_provided='' in declaration.context, + value=declaration.context.get(''), + extra={k: v for k, v in declaration.context.items() if k != ''}, + ) postgen_results[declaration_name] = declaration.declaration.call( instance=instance, step=step, - context=context, + context=postgen_context, ) self.factory_meta.use_postgeneration_results( instance=instance, diff --git a/factory/declarations.py b/factory/declarations.py index c18ed8c9..0a73105e 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -517,22 +517,6 @@ def get_revdeps(self, parameters): # =============== -class ExtractionContext(object): - """Private class holding all required context from extraction to postgen.""" - def __init__(self, value=None, did_extract=False, extra=None, for_field=''): - self.value = value - self.did_extract = did_extract - self.extra = extra or {} - self.for_field = for_field - - def __repr__(self): - return 'ExtractionContext(%r, %r, %r)' % ( - self.value, - self.did_extract, - self.extra, - ) - - class PostGenerationDeclaration(object): """Declarations to be called once the model object has been generated.""" @@ -543,36 +527,13 @@ def __init__(self, *args, **kwargs): self.creation_counter = PostGenerationDeclaration.creation_counter PostGenerationDeclaration.creation_counter += 1 - def extract(self, name, attrs): - """Extract relevant attributes from a dict. - - Args: - name (str): the name at which this PostGenerationDeclaration was - defined in the declarations - attrs (dict): the attribute dict from which values should be - extracted - - Returns: - (object, dict): a tuple containing the attribute at 'name' (if - provided) and a dict of extracted attributes - """ - try: - extracted = attrs.pop(name) - did_extract = True - except KeyError: - extracted = None - did_extract = False - - kwargs = utils.extract_dict(name, attrs) - return ExtractionContext(extracted, did_extract, kwargs, name) - def call(self, instance, step, context): # pragma: no cover """Call this hook; no return value is expected. Args: obj (object): the newly generated object create (bool): whether the object was 'built' or 'created' - context: An ExtractionContext containing values + context: a builder.PostGenerationContext containing values extracted from the containing factory's declaration """ raise NotImplementedError() @@ -622,7 +583,7 @@ def get_factory(self): def call(self, instance, step, context): factory = self.get_factory() - if context.did_extract: + if context.value_provided: # The user passed in a custom value logger.debug( "RelatedFactory: Using provided %s instead of generating %s.%s.", @@ -665,7 +626,7 @@ def __init__(self, method_name, *args, **kwargs): self.method_kwargs = kwargs def call(self, instance, step, context): - if not context.did_extract: + if not context.value_provided: passed_args = self.method_args elif len(self.method_args) <= 1: diff --git a/tests/test_base.py b/tests/test_base.py index a9f1acf9..513bf8ec 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -533,7 +533,7 @@ class Meta: foo__bar = 42 self.assertIn('foo', TestObjectFactory._meta.post_declarations.as_dict()) - self.assertIn('foo__foo__bar', TestObjectFactory._meta.post_declarations.as_dict()) + self.assertIn('foo__bar', TestObjectFactory._meta.post_declarations.as_dict()) From a4c3e36a5f7f72d6d068e49a4ef045507e0bc1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 15:07:27 +0200 Subject: [PATCH 234/714] [refactor] Require kwargs for PostGeneration PostGenerationMethodCall could receive many positional arguments. This did not play well with the ability to override the value at build time: would the overridden value match one of the parameters? Should we pass a tuple? Instead, we force the user to use a *single* positional argument; all others should be keyword arguments. The clumsiness of those combined features makes it unlikely that anyone would have been using them. ERROR: API for PostGenerationMethodCall has changed WARNING: It is no longer possible to use a PostGenerationMethodCall with a method accepting only positional arguments (i.e ``*args``). --- factory/declarations.py | 31 ++++++++++++++++++++----------- tests/test_declarations.py | 4 ++++ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 0a73105e..ae8eb1d8 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -8,6 +8,7 @@ import logging from . import compat +from . import errors from . import utils @@ -606,6 +607,10 @@ def call(self, instance, step, context): return step.recurse(factory, passed_kwargs) +class NotProvided: + pass + + class PostGenerationMethodCall(PostGenerationDeclaration): """Calls a method of the generated object. @@ -621,27 +626,31 @@ class UserFactory(factory.Factory): """ def __init__(self, method_name, *args, **kwargs): super(PostGenerationMethodCall, self).__init__() + if len(args) > 1: + raise errors.InvalidDeclarationError( + "A PostGenerationMethodCall can only handle 1 positional argument; " + "please provide other parameters through keyword arguments." + ) self.method_name = method_name - self.method_args = args + self.method_arg = args[0] if args else NotProvided self.method_kwargs = kwargs def call(self, instance, step, context): if not context.value_provided: - passed_args = self.method_args - - elif len(self.method_args) <= 1: - # Max one argument expected - passed_args = (context.value,) + if self.method_arg is NotProvided: + args = tuple() + else: + args = tuple([self.method_arg]) else: - passed_args = tuple(context.value) + args = tuple([context.value]) - passed_kwargs = dict(self.method_kwargs) - passed_kwargs.update(context.extra) + kwargs = dict(self.method_kwargs) + kwargs.update(context.extra) method = getattr(instance, self.method_name) logger.debug( "PostGenerationMethodCall: Calling %s.%s(%s)", utils.log_repr(instance), self.method_name, - utils.log_pprint(passed_args, passed_kwargs), + utils.log_pprint(args, kwargs), ) - return method(*passed_args, **passed_kwargs) + return method(*args, **kwargs) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 3294818a..bea2cbae 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -248,12 +248,14 @@ def test_call_with_passed_kwargs(self): ) obj.method.assert_called_once_with(data='other') + @unittest.expectedFailure # Broken API in refactor def test_multi_call_with_multi_method_args(self): obj = self.build( declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), ) obj.method.assert_called_once_with('arg1', 'arg2') + @unittest.expectedFailure # Broken API in refactor def test_multi_call_with_passed_multiple_args(self): obj = self.build( declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), @@ -261,6 +263,7 @@ def test_multi_call_with_passed_multiple_args(self): ) obj.method.assert_called_once_with('param1', 'param2', 'param3') + @unittest.expectedFailure # Broken API in refactor def test_multi_call_with_passed_tuple(self): obj = self.build( declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), @@ -268,6 +271,7 @@ def test_multi_call_with_passed_tuple(self): ) obj.method.assert_called_once_with(('param1', 'param2')) + @unittest.expectedFailure # Broken API in refactor def test_multi_call_with_kwargs(self): obj = self.build( declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), From 3fe44f2208a85bb56ea02b095fd1a5316c799205 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 15:30:42 +0200 Subject: [PATCH 235/714] tests: Go deeper in cyclic dependency testing. --- tests/test_using.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_using.py b/tests/test_using.py index 5162ff8b..5f0916e1 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2234,8 +2234,8 @@ class SelfReferentialTests(unittest.TestCase): def test_no_parent(self): from .cyclic import self_ref - obj = self_ref.TreeElementFactory(parent=None) - self.assertIsNone(obj.parent) + obj = self_ref.TreeElementFactory(parent__parent__parent=None) + self.assertIsNone(obj.parent.parent.parent) def test_deep(self): from .cyclic import self_ref From 3c9039ea96e67875802e5f95fb8f5d55bd74909b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 15:36:38 +0200 Subject: [PATCH 236/714] Give RelatedFactory access to parent context. Previously, it was not possible to do: RelatedFactory( ProfileFactory, 'user', display_name=factory.SelfAttribute('..username'), ) Thanks to the core refactor, this is now possible. Behind the scenes, we'll pass the calling factory's context down to the related factory. --- docs/reference.rst | 10 +++++----- tests/test_using.py | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index b257a5d6..b9a3b90a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1631,7 +1631,7 @@ 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 +If a value is passed for the :class:`RelatedFactory` attribute, this disables :class:`RelatedFactory` generation: .. code-block:: pycon @@ -1649,7 +1649,8 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables .. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated. - This means that calls to :class:`factory.SelfAttribute` cannot go higher than this :class:`RelatedFactory`: + However, the build context is passed down to that factory; this means that calls to + :class:`factory.SelfAttribute` *can* go back to the calling factorry's context: .. code-block:: python @@ -1659,9 +1660,8 @@ If a value if passed for the :class:`RelatedFactory` attribute, this disables lang = 'fr' capital_city = factory.RelatedFactory(CityFactory, 'capital_of', - # factory.SelfAttribute('..lang') will crash, since the context of - # ``CountryFactory`` has already been evaluated. - main_lang=factory.SelfAttribute('capital_of.lang'), + # Would also work with SelfAttribute('capital_of.lang') + main_lang=factory.SelfAttribute('..lang'), ) diff --git a/tests/test_using.py b/tests/test_using.py index 5f0916e1..114c46aa 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2111,7 +2111,6 @@ class Meta: self.assertEqual(1, obj.related.one) self.assertEqual(4, obj.related.two) - @unittest.expectedFailure # Broken undocumented hack in refactor def test_parameterized_related_factory(self): class TestRelatedObject(object): def __init__(self, obj=None, one=None, two=None): @@ -2136,7 +2135,7 @@ class Params: one = 3 two = 2 three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') - three__two = factory.SelfAttribute('blah') + three__two = factory.SelfAttribute('..blah') obj = TestObjectFactory.build() self.assertEqual(3, obj.one) From 8b018a0576dd907de6fdf10d8674b951d9c2e77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 16:09:20 +0200 Subject: [PATCH 237/714] Fixup: Remove regression on post_generation API --- factory/base.py | 7 ++++--- factory/declarations.py | 4 +++- factory/enums.py | 7 +++++++ tests/test_using.py | 1 - 4 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 factory/enums.py diff --git a/factory/base.py b/factory/base.py index 8123533b..c9895e3f 100644 --- a/factory/base.py +++ b/factory/base.py @@ -9,15 +9,16 @@ from . import builder from . import declarations +from . import enums from . import errors from . import utils logger = logging.getLogger('factory.generate') # Strategies -BUILD_STRATEGY = 'build' -CREATE_STRATEGY = 'create' -STUB_STRATEGY = 'stub' +BUILD_STRATEGY = enums.BUILD_STRATEGY +CREATE_STRATEGY = enums.CREATE_STRATEGY +STUB_STRATEGY = enums.STUB_STRATEGY # Factory metaclasses diff --git a/factory/declarations.py b/factory/declarations.py index ae8eb1d8..f46bb2b3 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -7,6 +7,7 @@ import itertools import logging +from . import enums from . import compat from . import errors from . import utils @@ -556,8 +557,9 @@ def call(self, instance, step, context): context, ), ) + create = step.builder.strategy == enums.CREATE_STRATEGY return self.function( - instance, step, context.value, **context.extra) + instance, create, context.value, **context.extra) class RelatedFactory(PostGenerationDeclaration): diff --git a/factory/enums.py b/factory/enums.py new file mode 100644 index 00000000..387828e4 --- /dev/null +++ b/factory/enums.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright: See the LICENSE file. + +# Strategies +BUILD_STRATEGY = 'build' +CREATE_STRATEGY = 'create' +STUB_STRATEGY = 'stub' diff --git a/tests/test_using.py b/tests/test_using.py index 114c46aa..988cd61c 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1944,7 +1944,6 @@ def incr_one(self, _create, increment=1): self.assertEqual(4, obj.one) self.assertFalse(hasattr(obj, 'incr_one')) - @unittest.expectedFailure # Broken API in refactor def test_post_generation_extraction_lambda(self): def my_lambda(obj, create, extracted, **kwargs): From 52b893660e8ac74b9574fe1b9df3357aed490cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 16:11:27 +0200 Subject: [PATCH 238/714] fixup: Restore after_postgeneration API. --- factory/base.py | 8 ++++---- factory/django.py | 4 ++-- tests/test_using.py | 3 +-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/factory/base.py b/factory/base.py index c9895e3f..0a325aa9 100644 --- a/factory/base.py +++ b/factory/base.py @@ -324,8 +324,8 @@ def instantiate(self, step, args, kwargs): def use_postgeneration_results(self, step, instance, results): self.factory._after_postgeneration( - instance=instance, - step=step, + instance, + create=step.builder.strategy == enums.CREATE_STRATEGY, results=results, ) @@ -511,11 +511,11 @@ def _generate(cls, strategy, params): return step.build() @classmethod - def _after_postgeneration(cls, instance, step, results=None): + def _after_postgeneration(cls, instance, create, results=None): """Hook called after post-generation declarations have been handled. Args: - obj (object): the generated object + instance (object): the generated object create (bool): whether the strategy was 'build' or 'create' results (dict or None): result of post-generation declarations """ diff --git a/factory/django.py b/factory/django.py index 0860bbbb..0a73fe27 100644 --- a/factory/django.py +++ b/factory/django.py @@ -169,9 +169,9 @@ def _create(cls, model_class, *args, **kwargs): return manager.create(*args, **kwargs) @classmethod - def _after_postgeneration(cls, instance, step, results=None): + def _after_postgeneration(cls, instance, create, results=None): """Save again the instance if creating and at least one hook ran.""" - if step.builder.strategy == base.CREATE_STRATEGY and results: + if create and results: # Some post-generation hooks ran, and may have modified us. instance.save() diff --git a/tests/test_using.py b/tests/test_using.py index 988cd61c..38f31576 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1902,7 +1902,6 @@ def incr_one(self, _create, _increment): self.assertEqual(3, obj.one) self.assertFalse(hasattr(obj, 'incr_one')) - @unittest.expectedFailure # Broken API in refactor def test_post_generation_hook(self): class TestObjectFactory(factory.Factory): class Meta: @@ -1922,7 +1921,7 @@ def _after_postgeneration(cls, obj, create, results): obj = TestObjectFactory.build() self.assertEqual(2, obj.one) - self.assertEqual(factory.BUILD_STRATEGY, obj.step.builder.strategy) + self.assertFalse(obj.create) self.assertEqual({'incr_one': 42}, obj.results) def test_post_generation_extraction(self): From adc6299fbe64b488d952d8a42fb243bb062fa90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 16:15:34 +0200 Subject: [PATCH 239/714] [refactor] Move strategies to enums module. --- factory/__init__.py | 5 ++++- factory/base.py | 38 ++++++++++++++++---------------------- tests/test_base.py | 19 ++++++++++--------- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index b2014de3..f4ee32a4 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -9,10 +9,13 @@ ListFactory, StubFactory, + use_strategy, +) + +from .enums import ( BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, - use_strategy, ) diff --git a/factory/base.py b/factory/base.py index 0a325aa9..76fde2b1 100644 --- a/factory/base.py +++ b/factory/base.py @@ -15,12 +15,6 @@ logger = logging.getLogger('factory.generate') -# Strategies -BUILD_STRATEGY = enums.BUILD_STRATEGY -CREATE_STRATEGY = enums.CREATE_STRATEGY -STUB_STRATEGY = enums.STUB_STRATEGY - - # Factory metaclasses @@ -46,11 +40,11 @@ def __call__(cls, **kwargs): Returns an instance of the associated class. """ - if cls._meta.strategy == BUILD_STRATEGY: + if cls._meta.strategy == enums.BUILD_STRATEGY: return cls.build(**kwargs) - elif cls._meta.strategy == CREATE_STRATEGY: + elif cls._meta.strategy == enums.CREATE_STRATEGY: return cls.create(**kwargs) - elif cls._meta.strategy == STUB_STRATEGY: + elif cls._meta.strategy == enums.STUB_STRATEGY: return cls.stub(**kwargs) else: raise errors.UnknownStrategy('Unknown Meta.strategy: {0}'.format( @@ -108,7 +102,7 @@ def __str__(cls): class BaseMeta: abstract = True - strategy = CREATE_STRATEGY + strategy = enums.CREATE_STRATEGY class OptionDefault(object): @@ -175,7 +169,7 @@ def _build_default_options(self): return [ OptionDefault('model', None, inherit=True), OptionDefault('abstract', False, inherit=False), - OptionDefault('strategy', CREATE_STRATEGY, inherit=True), + OptionDefault('strategy', enums.CREATE_STRATEGY, inherit=True), OptionDefault('inline_args', (), inherit=True), OptionDefault('exclude', (), inherit=True), OptionDefault('rename', {}, inherit=True), @@ -314,12 +308,12 @@ def prepare_arguments(self, attributes): def instantiate(self, step, args, kwargs): model = self.get_model_class() - if step.builder.strategy == BUILD_STRATEGY: + if step.builder.strategy == enums.BUILD_STRATEGY: return self.factory._build(model, *args, **kwargs) - elif step.builder.strategy == CREATE_STRATEGY: + elif step.builder.strategy == enums.CREATE_STRATEGY: return self.factory._create(model, *args, **kwargs) else: - assert step.builder.strategy == STUB_STRATEGY + assert step.builder.strategy == enums.STUB_STRATEGY return StubObject(**kwargs) def use_postgeneration_results(self, step, instance, results): @@ -554,7 +548,7 @@ def _create(cls, model_class, *args, **kwargs): @classmethod def build(cls, **kwargs): """Build an instance of the associated class, with overriden attrs.""" - return cls._generate(BUILD_STRATEGY, kwargs) + return cls._generate(enums.BUILD_STRATEGY, kwargs) @classmethod def build_batch(cls, size, **kwargs): @@ -571,7 +565,7 @@ def build_batch(cls, size, **kwargs): @classmethod def create(cls, **kwargs): """Create an instance of the associated class, with overriden attrs.""" - return cls._generate(CREATE_STRATEGY, kwargs) + return cls._generate(enums.CREATE_STRATEGY, kwargs) @classmethod def create_batch(cls, size, **kwargs): @@ -592,7 +586,7 @@ def stub(cls, **kwargs): This will return an object whose attributes are those defined in this factory's declarations or in the extra kwargs. """ - return cls._generate(STUB_STRATEGY, kwargs) + return cls._generate(enums.STUB_STRATEGY, kwargs) @classmethod def stub_batch(cls, size, **kwargs): @@ -619,7 +613,7 @@ def generate(cls, strategy, **kwargs): Returns: object: the generated instance """ - assert strategy in (STUB_STRATEGY, BUILD_STRATEGY, CREATE_STRATEGY) + assert strategy in (enums.STUB_STRATEGY, enums.BUILD_STRATEGY, enums.CREATE_STRATEGY) action = getattr(cls, strategy) return action(**kwargs) @@ -637,7 +631,7 @@ def generate_batch(cls, strategy, size, **kwargs): Returns: object list: the generated instances """ - assert strategy in (STUB_STRATEGY, BUILD_STRATEGY, CREATE_STRATEGY) + assert strategy in (enums.STUB_STRATEGY, enums.BUILD_STRATEGY, enums.CREATE_STRATEGY) batch_action = getattr(cls, '%s_batch' % strategy) return batch_action(size, **kwargs) @@ -653,7 +647,7 @@ def simple_generate(cls, create, **kwargs): Returns: object: the generated instance """ - strategy = CREATE_STRATEGY if create else BUILD_STRATEGY + strategy = enums.CREATE_STRATEGY if create else enums.BUILD_STRATEGY return cls.generate(strategy, **kwargs) @classmethod @@ -669,7 +663,7 @@ def simple_generate_batch(cls, create, size, **kwargs): Returns: object list: the generated instances """ - strategy = CREATE_STRATEGY if create else BUILD_STRATEGY + strategy = enums.CREATE_STRATEGY if create else enums.BUILD_STRATEGY return cls.generate_batch(strategy, size, **kwargs) @@ -699,7 +693,7 @@ def __init__(self, **kwargs): class StubFactory(Factory): class Meta: - strategy = STUB_STRATEGY + strategy = enums.STUB_STRATEGY model = StubObject @classmethod diff --git a/tests/test_base.py b/tests/test_base.py index 513bf8ec..5e5c27b4 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -5,6 +5,7 @@ from factory import base from factory import declarations +from factory import enums from factory import errors from .compat import unittest @@ -106,7 +107,7 @@ class AbstractFactory(base.Factory): self.assertIsNone(AbstractFactory._meta.model) self.assertEqual((), AbstractFactory._meta.inline_args) self.assertEqual((), AbstractFactory._meta.exclude) - self.assertEqual(base.CREATE_STRATEGY, AbstractFactory._meta.strategy) + self.assertEqual(enums.CREATE_STRATEGY, AbstractFactory._meta.strategy) # Non-declarative attributes self.assertEqual({}, AbstractFactory._meta.pre_declarations.as_dict()) @@ -357,7 +358,7 @@ def tearDown(self): base.Factory._meta.strategy = self.default_strategy def test_build_strategy(self): - base.Factory._meta.strategy = base.BUILD_STRATEGY + base.Factory._meta.strategy = enums.BUILD_STRATEGY class TestModelFactory(base.Factory): class Meta: @@ -383,7 +384,7 @@ class Meta: self.assertTrue(test_model.id) def test_stub_strategy(self): - base.Factory._meta.strategy = base.STUB_STRATEGY + base.Factory._meta.strategy = enums.STUB_STRATEGY class TestModelFactory(base.Factory): class Meta: @@ -413,7 +414,7 @@ class Meta: one = 'one' - TestModelFactory._meta.strategy = base.CREATE_STRATEGY + TestModelFactory._meta.strategy = enums.CREATE_STRATEGY self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) @@ -424,21 +425,21 @@ class Meta: one = 'one' - TestModelFactory._meta.strategy = base.BUILD_STRATEGY + TestModelFactory._meta.strategy = enums.BUILD_STRATEGY obj = TestModelFactory() # For stubs, build() is an alias of stub(). self.assertFalse(isinstance(obj, TestModel)) def test_change_strategy(self): - @base.use_strategy(base.CREATE_STRATEGY) + @base.use_strategy(enums.CREATE_STRATEGY) class TestModelFactory(base.StubFactory): class Meta: model = TestModel one = 'one' - self.assertEqual(base.CREATE_STRATEGY, TestModelFactory._meta.strategy) + self.assertEqual(enums.CREATE_STRATEGY, TestModelFactory._meta.strategy) class FactoryCreationTestCase(unittest.TestCase): @@ -453,7 +454,7 @@ def test_stub(self): class TestFactory(base.StubFactory): pass - self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY) + self.assertEqual(TestFactory._meta.strategy, enums.STUB_STRATEGY) def test_inheritance_with_stub(self): class TestObjectFactory(base.StubFactory): @@ -465,7 +466,7 @@ class Meta: class TestFactory(TestObjectFactory): pass - self.assertEqual(TestFactory._meta.strategy, base.STUB_STRATEGY) + self.assertEqual(TestFactory._meta.strategy, enums.STUB_STRATEGY) def test_stub_and_subfactory(self): class StubA(base.StubFactory): From 9edd8107e83d8fda62f963ff329345d3a18ea38a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 16:42:18 +0200 Subject: [PATCH 240/714] Fixup: Document the Maybe option. --- docs/changelog.rst | 2 ++ docs/reference.rst | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8cfa6bb5..2a35f3da 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ master *New:* - :issue:`275`: `factory.fuzzy` and `factory.faker` now use the same random seed. + - Add :class:`factory.Maybe`, which chooses among two possible declarations based + on another field's value (powers the :class:`~factory.Trait` feature). *Deprecation:* diff --git a/docs/reference.rst b/docs/reference.rst index b9a3b90a..e19c4809 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1214,7 +1214,7 @@ That declaration takes a single argument, a dot-delimited path to the attribute .. code-block:: python - class UserFactory(factory.Factory) + class UserFactory(factory.Factory): class Meta: model = User @@ -1494,6 +1494,45 @@ with the :class:`Dict` and :class:`List` attributes: argument, if another type (tuple, set, ...) is required. +Maybe +""""" + +.. class:: Maybe(decider, yes_declaration, no_declaration) + +Sometimes, the way to build a given field depends on the value of another, +for instance of a parameter. + +In those cases, use the :class:`~factory.Maybe` declaration: +it takes the name of a "decider" boolean field, and two declarations; depending on +the value of the field whose name is held in the 'decider' parameter, it will +apply the effects of one or the other declaration: + +.. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + is_active = True + deactivation_date = factory.Maybe( + 'is_active', + yes_declaration=None, + no_declaration=factory.fuzzy.FuzzyDateTime(timezone.now() - datetime.timedelta(days=10)), + ) + +.. code-block:: pycon + + >>> u = UserFactory(is_active=True) + >>> u.deactivation_date + None + >>> u = UserFactory(is_active=False) + >>> u.deactivation_date + datetime.datetime(2017, 4, 1, 23, 21, 23, tzinfo=UTC) + +.. note:: If the condition for the decider is complex, use a :class:`LazyAttribute` + defined in the :attr:`~Factory.Params` section of your factory to + handle the computation. + Post-generation hooks """"""""""""""""""""" From 9b51fd1e56ebdda1c62b28f32f115bbaf4f69671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 16:43:46 +0200 Subject: [PATCH 241/714] [refactor] Document new PostGenerationMethodCall --- docs/changelog.rst | 5 +++++ docs/reference.rst | 43 ++++++-------------------------------- tests/test_declarations.py | 34 +++++------------------------- 3 files changed, 16 insertions(+), 66 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2a35f3da..1f965a65 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,11 +4,16 @@ ChangeLog master ------ +This version brings massive changes to the core engine, thus reducing the number of +corner cases and weird behaviourrs. + *New:* - :issue:`275`: `factory.fuzzy` and `factory.faker` now use the same random seed. - Add :class:`factory.Maybe`, which chooses among two possible declarations based on another field's value (powers the :class:`~factory.Trait` feature). + - :class:`~factory.PostGenerationMethodCall` only allows to pass one positional argument; use keyword arguments for + extra parameters. *Deprecation:* diff --git a/docs/reference.rst b/docs/reference.rst index e19c4809..1996a993 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1776,7 +1776,7 @@ A decorator is also provided, decorating a single method accepting the same PostGenerationMethodCall """""""""""""""""""""""" -.. class:: PostGenerationMethodCall(method_name, *args, **kwargs) +.. class:: PostGenerationMethodCall(method_name, *arg, **kwargs) .. OHAI_VIM* @@ -1789,9 +1789,9 @@ PostGenerationMethodCall The name of the method to call on the :attr:`~FactoryOptions.model` object - .. attribute:: args + .. attribute:: arg - The default set of unnamed arguments to pass to the method given in + The default, optional, positional argument to pass to the method given in :attr:`method_name` .. attribute:: kwargs @@ -1870,40 +1870,9 @@ factory during instantiation. 'defaultpassword') -If instead the :class:`PostGenerationMethodCall` declaration uses two or -more positional arguments, the overriding value must be an iterable. For -example, if we declared the ``password`` attribute like the following, - -.. code-block:: python - - class UserFactory(factory.Factory): - class Meta: - model = User - - username = 'user' - password = factory.PostGenerationMethodCall('set_password', '', 'sha1') - -then we must be cautious to pass in an iterable for the ``password`` -keyword argument when creating an instance from the factory: - -.. code-block:: pycon - - >>> UserFactory() # Calls user.set_password('', 'sha1') - >>> UserFactory(password=('test', 'md5')) # Calls user.set_password('test', 'md5') - - >>> # Always pass in a good iterable: - >>> UserFactory(password=('test',)) # Calls user.set_password('test') - >>> UserFactory(password='test') # Calls user.set_password('t', 'e', 's', 't') - - -.. note:: While this setup provides sane and intuitive defaults for most users, - it prevents passing more than one argument when the declaration used - zero or one. - - In such cases, users are advised to either resort to the more powerful - :class:`PostGeneration` or to add the second expected argument default - value to the :class:`PostGenerationMethodCall` declaration - (``PostGenerationMethodCall('method', 'x', 'y_that_is_the_default')``) +.. warning:: In order to keep a consistent and simple API, a :class:`PostGenerationMethodCall` + allows *at most one* positional argument; all other parameters should be passed as + keyword arguments. Keywords extracted from the factory arguments are merged into the defaults present in the :class:`PostGenerationMethodCall` declaration. diff --git a/tests/test_declarations.py b/tests/test_declarations.py index bea2cbae..870df49d 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -6,6 +6,7 @@ from factory import base from factory import declarations +from factory import errors from factory import helpers from .compat import mock, unittest @@ -248,36 +249,11 @@ def test_call_with_passed_kwargs(self): ) obj.method.assert_called_once_with(data='other') - @unittest.expectedFailure # Broken API in refactor def test_multi_call_with_multi_method_args(self): - obj = self.build( - declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), - ) - obj.method.assert_called_once_with('arg1', 'arg2') - - @unittest.expectedFailure # Broken API in refactor - def test_multi_call_with_passed_multiple_args(self): - obj = self.build( - declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), - post=('param1', 'param2', 'param3'), - ) - obj.method.assert_called_once_with('param1', 'param2', 'param3') - - @unittest.expectedFailure # Broken API in refactor - def test_multi_call_with_passed_tuple(self): - obj = self.build( - declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), - post=(('param1', 'param2'),), - ) - obj.method.assert_called_once_with(('param1', 'param2')) - - @unittest.expectedFailure # Broken API in refactor - def test_multi_call_with_kwargs(self): - obj = self.build( - declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), - post__x=2, - ) - obj.method.assert_called_once_with('arg1', 'arg2', x=2) + with self.assertRaises(errors.InvalidDeclarationError): + obj = self.build( + declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), + ) class PostGenerationOrdering(unittest.TestCase): From 95a7e7d2653ead935d345b74a1f0a790d0f991ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2017 16:53:00 +0200 Subject: [PATCH 242/714] Linting --- factory/base.py | 1 - factory/builder.py | 2 -- factory/declarations.py | 1 - 3 files changed, 4 deletions(-) diff --git a/factory/base.py b/factory/base.py index 76fde2b1..7d7a3d7b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -11,7 +11,6 @@ from . import declarations from . import enums from . import errors -from . import utils logger = logging.getLogger('factory.generate') diff --git a/factory/builder.py b/factory/builder.py index 34b12696..b181cd92 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -45,8 +45,6 @@ def copy(self): return other def update(self, values): - deeps = collections.defaultdict(dict) - remainder = {} for k, v in values.items(): root, sub = self.split(k) if sub is None: diff --git a/factory/declarations.py b/factory/declarations.py index f46bb2b3..3a98320a 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals -import collections import itertools import logging From 4feda03dae70cad274e14e495300bfee1a20e8e2 Mon Sep 17 00:00:00 2001 From: Demur Nodia Date: Fri, 7 Apr 2017 17:24:09 +0200 Subject: [PATCH 243/714] Remove 32 33 versions --- README.rst | 3 +-- setup.py | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 5e17d8cf..7d429ae6 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.2 to 3.6, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy; it requires only the standard Python library. Download @@ -378,4 +378,3 @@ To avoid running ``mongoengine`` tests (e.g no mongo server installed), run: .. code-block:: sh $ make SKIP_MONGOENGINE=1 test - diff --git a/setup.py b/setup.py index 04aa183a..f1476276 100755 --- a/setup.py +++ b/setup.py @@ -64,8 +64,6 @@ def get_version(package_name): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.2", - "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", From 4f0a44c501d3862b94cbe99e41bea564350d027d Mon Sep 17 00:00:00 2001 From: Zahim Anass Date: Fri, 7 Apr 2017 16:53:40 +0200 Subject: [PATCH 244/714] Removing flow control code managing backward compatibility with django<1.8 Related to #363 --- factory/django.py | 15 +++++---------- tests/test_django.py | 8 ++------ 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/factory/django.py b/factory/django.py index 0a73fe27..89ccc054 100644 --- a/factory/django.py +++ b/factory/django.py @@ -58,10 +58,6 @@ def _lazy_load_get_model(): if django is None: def _get_model(app, model): raise import_failure - - elif django.VERSION[:2] < (1, 7): - from django.db.models.loading import get_model as _get_model - else: from django import apps as django_apps _get_model = django_apps.apps.get_model @@ -296,12 +292,11 @@ def __exit__(self, exc_type, exc_value, traceback): receivers) signal.receivers = receivers - if django.VERSION[:2] >= (1, 6): - with signal.lock: - # Django uses some caching for its signals. - # Since we're bypassing signal.connect and signal.disconnect, - # we have to keep messing with django's internals. - signal.sender_receivers_cache.clear() + with signal.lock: + # Django uses some caching for its signals. + # Since we're bypassing signal.connect and signal.disconnect, + # we have to keep messing with django's internals. + signal.sender_receivers_cache.clear() self.paused = {} def copy(self): diff --git a/tests/test_django.py b/tests/test_django.py index 245b3a47..5d6c54dc 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -16,15 +16,11 @@ if django is not None: os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') - if django.VERSION >= (1, 7, 0): - django.setup() + django.setup() from django import test as django_test from django.conf import settings from django.db import models as django_models - if django.VERSION <= (1, 8, 0): - from django.test.simple import DjangoTestSuiteRunner - else: - from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner + from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner from django.test import utils as django_test_utils from django.db.models import signals from .djapp import models From 849754ffcab59c7d90d4c2e81241de8423dbfe3e Mon Sep 17 00:00:00 2001 From: Zahim Anass Date: Fri, 14 Apr 2017 23:47:02 +0100 Subject: [PATCH 245/714] [Faker] random state should be set only when calling `factory.random.reseed_random` For each Faker instanciation, the random state was set from factory.fuzzy. Now the random state is set only when `factory.random.reseed_random` is called explicitly. That allows us to have consistancy between fuzzy and faker and also avoid Faker to generate the same set of "randoms" in each instanciation. --- factory/faker.py | 2 -- factory/random.py | 5 +++++ tests/test_using.py | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/factory/faker.py b/factory/faker.py index 064863da..1d444809 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -22,7 +22,6 @@ class Meta: import faker import faker.config -from .random import get_random_state from . import declarations @@ -77,7 +76,6 @@ def _get_faker(cls, locale=None): subfaker = faker.Faker(locale=locale) cls._FAKER_REGISTRY[locale] = subfaker - cls._FAKER_REGISTRY[locale].random.setstate(get_random_state()) return cls._FAKER_REGISTRY[locale] @classmethod diff --git a/factory/random.py b/factory/random.py index 88a5d9e8..7ca898d1 100644 --- a/factory/random.py +++ b/factory/random.py @@ -2,6 +2,8 @@ import random +from factory.faker import Faker + randgen = random.Random() randgen.state_set = False @@ -22,3 +24,6 @@ def reseed_random(seed): """Reseed factory.fuzzy's random generator.""" r = random.Random(seed) set_random_state(r.getstate()) + + for locale in Faker._FAKER_REGISTRY: + Faker._FAKER_REGISTRY[locale].random.setstate(r.getstate()) diff --git a/tests/test_using.py b/tests/test_using.py index 38f31576..42196dc0 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2211,6 +2211,7 @@ class StudentFactory(factory.Factory): one = factory.fuzzy.FuzzyDate(datetime.date(1950, 1, 1), ) two = factory.Faker('name') three = factory.Faker('name', locale='it') + four = factory.Faker('name') class Meta: model = TestObject @@ -2225,6 +2226,7 @@ class Meta: self.assertEqual(students_1[0].one, students_2[0].one) self.assertEqual(students_1[0].two, students_2[0].two) self.assertEqual(students_1[0].three, students_2[0].three) + self.assertEqual(students_1[0].four, students_2[0].four) class SelfReferentialTests(unittest.TestCase): From 9be88c62f305d6f02897be096da0f63a1ecac79b Mon Sep 17 00:00:00 2001 From: Zahim Anass Date: Sat, 15 Apr 2017 16:47:06 +0100 Subject: [PATCH 246/714] [Random] Reuse random.getstate() value --- factory/random.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/factory/random.py b/factory/random.py index 7ca898d1..75732fdd 100644 --- a/factory/random.py +++ b/factory/random.py @@ -23,7 +23,8 @@ def set_random_state(state): def reseed_random(seed): """Reseed factory.fuzzy's random generator.""" r = random.Random(seed) - set_random_state(r.getstate()) + random_internal_state = r.getstate() + set_random_state(random_internal_state) for locale in Faker._FAKER_REGISTRY: - Faker._FAKER_REGISTRY[locale].random.setstate(r.getstate()) + Faker._FAKER_REGISTRY[locale].random.setstate(random_internal_state) From c99deeb4203e90e515c9aee0c440b3b663dc4b69 Mon Sep 17 00:00:00 2001 From: Jules Robichaud-Gagnon Date: Sun, 9 Apr 2017 21:53:27 -0400 Subject: [PATCH 247/714] tests: adds django tests exposing three regressions --- tests/djapp/models.py | 4 ++++ tests/test_django.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 26674096..92634e5f 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -67,6 +67,10 @@ class PointingModel(models.Model): ) +class WithDefaultValue(models.Model): + foo = models.CharField(max_length=20, default='') + + 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 5d6c54dc..ba2b3252 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -524,6 +524,61 @@ def test_no_file(self): self.assertFalse(o.afile) +@unittest.skipIf(django is None, "Django not installed.") +class DjangoParamsTestCase(unittest.TestCase): + + def test_undeclared_fields(self): + class WithDefaultValueFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithDefaultValue + + class Params: + with_bar = factory.Trait( + foo='bar' + ) + + o = WithDefaultValueFactory() + self.assertEqual('', o.foo) + + def test_pointing_with_traits_using_same_name(self): + class PointedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointedModel + + class Params: + with_bar = factory.Trait( + foo='bar' + ) + + class PointerFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointingModel + pointed = factory.SubFactory(PointedFactory) + + class Params: + with_bar = factory.Trait( + foo='bar', + pointed__with_bar=True + ) + + o = PointerFactory(with_bar=True) + self.assertEqual('bar', o.foo) + self.assertEqual('bar', o.pointed.foo) + + +@unittest.skipIf(django is None, "Django not installed.") +class DjangoFakerTestCase(unittest.TestCase): + def test_random(self): + class StandardModelFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + foo = factory.Faker('pystr') + + o1 = StandardModelFactory() + o2 = StandardModelFactory() + self.assertNotEqual(o1.foo, o2.foo) + + @unittest.skipIf(django is None, "Django not installed.") @unittest.skipIf(Image is None, "PIL not installed.") class DjangoImageFieldTestCase(unittest.TestCase): From 52be0aec4a78e09e720bf79d279213ffd4c7ac45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 11 Apr 2017 22:24:25 +0200 Subject: [PATCH 248/714] Move SPLITTER to factory.enums Use a single declaration of the 'splitter' field. --- factory/builder.py | 8 ++++---- factory/enums.py | 5 +++++ factory/utils.py | 11 ++++------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index b181cd92..a74fafbf 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -3,6 +3,7 @@ import collections from . import declarations +from . import enums from . import errors @@ -20,7 +21,6 @@ class DeclarationSet(object): """A set of declarations, including the recursive parameters.""" - SPLITTER = '__' def __init__(self, initial=None): self.declarations = {} @@ -29,14 +29,14 @@ def __init__(self, initial=None): @classmethod def split(cls, entry): - if cls.SPLITTER in entry: - return entry.split(cls.SPLITTER, 1) + if enums.SPLITTER in entry: + return entry.split(enums.SPLITTER, 1) else: return (entry, None) @classmethod def join(cls, root, subkey): - return cls.SPLITTER.join((root, subkey)) + return enums.SPLITTER.join((root, subkey)) def copy(self): other = self.__class__() diff --git a/factory/enums.py b/factory/enums.py index 387828e4..8d5cab31 100644 --- a/factory/enums.py +++ b/factory/enums.py @@ -5,3 +5,8 @@ BUILD_STRATEGY = 'build' CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' + + +#: String for splitting an attribute name into a +#: (subfactory_name, subfactory_field) tuple. +SPLITTER = '__' diff --git a/factory/utils.py b/factory/utils.py index e38c8622..cb91688f 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -6,10 +6,7 @@ import collections from . import compat - -#: String for splitting an attribute name into a -#: (subfactory_name, subfactory_field) tuple. -ATTR_SPLITTER = '__' +from . import enums def extract_dict(prefix, kwargs, pop=True, exclude=()): @@ -25,10 +22,10 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): Returns: A new dict, containing values from kwargs and beginning with - prefix + ATTR_SPLITTER. That full prefix is removed from the keys + prefix + enums.SPLITTER. That full prefix is removed from the keys of the returned dict. """ - prefix = prefix + ATTR_SPLITTER + prefix = prefix + enums.SPLITTER extracted = {} for key in list(kwargs): @@ -66,7 +63,7 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude) results[prefix] = extracted exclude.extend( - ['%s%s%s' % (prefix, ATTR_SPLITTER, key) for key in extracted]) + ['%s%s%s' % (prefix, enums.SPLITTER, key) for key in extracted]) return results From c8831e4564dffd309fa965d16f2020469b4cbd70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 11 Apr 2017 22:32:24 +0200 Subject: [PATCH 249/714] Fix syntax of PR #373 - DB-interacting Django tests should inherit from django_test.TestCase for proper cleanup - In a multi-line parameter listing, *all* lines should end with a comma. --- tests/test_django.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index ba2b3252..3840d301 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -525,7 +525,7 @@ def test_no_file(self): @unittest.skipIf(django is None, "Django not installed.") -class DjangoParamsTestCase(unittest.TestCase): +class DjangoParamsTestCase(django_test.TestCase): def test_undeclared_fields(self): class WithDefaultValueFactory(factory.django.DjangoModelFactory): @@ -534,7 +534,7 @@ class Meta: class Params: with_bar = factory.Trait( - foo='bar' + foo='bar', ) o = WithDefaultValueFactory() @@ -547,7 +547,7 @@ class Meta: class Params: with_bar = factory.Trait( - foo='bar' + foo='bar', ) class PointerFactory(factory.django.DjangoModelFactory): @@ -558,7 +558,7 @@ class Meta: class Params: with_bar = factory.Trait( foo='bar', - pointed__with_bar=True + pointed__with_bar=True, ) o = PointerFactory(with_bar=True) From 0bd4b61985e60fd53d89c3aac5c896f6606fe653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 11 Apr 2017 22:25:07 +0200 Subject: [PATCH 250/714] Traits: only override when triggered. Introduce a ``factory.declarations.SKIP`` object, that is never passed to an object's constructor - otherwise, an un-activated Trait would try to pass 'None' as a value to the model constructor, overriding default values for keyword arguments. --- factory/base.py | 2 +- factory/declarations.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/factory/base.py b/factory/base.py index 7d7a3d7b..daa8546a 100644 --- a/factory/base.py +++ b/factory/base.py @@ -289,7 +289,7 @@ def prepare_arguments(self, attributes): # 2. Remove hidden objects kwargs = { k: v for k, v in kwargs.items() - if k not in self.exclude and k not in self.parameters + if k not in self.exclude and k not in self.parameters and v is not declarations.SKIP } # 3. Rename fields diff --git a/factory/declarations.py b/factory/declarations.py index 3a98320a..163baea5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -425,12 +425,19 @@ def __init__(self, params, list_factory='factory.ListFactory'): # ========== -class UNDEFINED(object): - pass +class Skip(object): + def __bool__(self): + return False + + # Py2 compatibility + __nonzero__ = __bool__ + + +SKIP = Skip() class Maybe(BaseDeclaration): - def __init__(self, decider, yes_declaration, no_declaration=None): + def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): self.decider = decider self.yes = yes_declaration self.no = no_declaration @@ -505,7 +512,7 @@ def as_declarations(self, field_name, declarations): overrides[maybe_field] = Maybe( decider=field_name, yes_declaration=new_value, - no_declaration=declarations.get(maybe_field, None), + no_declaration=declarations.get(maybe_field, SKIP), ) return overrides From 7b1ee6991b07338349014c90e1221b33d20be3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 11 Apr 2017 22:30:24 +0200 Subject: [PATCH 251/714] Fix traits setting sub-object values. The conversion from Trait to Maybe didn't consider cases where the updated field belonged to a SubFactory-based object. With this change, the decider for a Maybe may also be any declaration; however, without more extended testing, this will be kept as a non-public feature. --- factory/declarations.py | 15 ++++++++++++--- tests/test_using.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 163baea5..6fcfd2f4 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -443,8 +443,11 @@ def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): self.no = no_declaration def evaluate(self, instance, step, extra): - decider = getattr(instance, self.decider, None) - target = self.yes if decider else self.no + if isinstance(self.decider, BaseDeclaration): + choice = self.decider.evaluate(instance=instance, step=step, extra={}) + else: + choice = getattr(instance, self.decider, None) + target = self.yes if choice else self.no if isinstance(target, BaseDeclaration): return target.evaluate( @@ -510,7 +513,13 @@ def as_declarations(self, field_name, declarations): overrides = {} for maybe_field, new_value in self.overrides.items(): overrides[maybe_field] = Maybe( - decider=field_name, + decider=SelfAttribute( + '%s.%s' % ( + '.' * maybe_field.count(enums.SPLITTER), + field_name, + ), + default=False, + ), yes_declaration=new_value, no_declaration=declarations.get(maybe_field, SKIP), ) diff --git a/tests/test_using.py b/tests/test_using.py index 42196dc0..71fb2a31 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1211,6 +1211,27 @@ class Params: a = factory.Trait(b=True, one=True) b = factory.Trait(a=True, two=True) + def test_deep_traits(self): + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + class WrapperFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + deep_one = factory.Trait( + one=1, + two__one=2, + ) + + two = factory.SubFactory(TestObjectFactory) + + wrapper = WrapperFactory(deep_one=True) + self.assertEqual(1, wrapper.one) + self.assertEqual(2, wrapper.two.one) + class SubFactoryTestCase(unittest.TestCase): def test_sub_factory(self): From d1ba867f9af3f76b18840dc0f2d66135d3c40b6c Mon Sep 17 00:00:00 2001 From: Ritesh Kadmawala Date: Tue, 30 May 2017 23:13:07 +0530 Subject: [PATCH 252/714] Updated the traits test case to also test for overriding values while chaining traits --- tests/test_using.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 71fb2a31..b4c73bb2 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1181,6 +1181,7 @@ class Params: even = factory.Trait(two=True, four=True) odd = factory.Trait(one=True, three=True, five=True) full = factory.Trait(even=True, odd=True) + override = factory.Trait(even=True, two=False) # Setting "full" should enable all fields. obj = TestObjectFactory(full=True) @@ -1200,6 +1201,11 @@ class Params: self.assertEqual(obj3.as_dict(), dict(one=True, two=None, three=True, four=None, five=True)) + # Setting override should override two and set it to False + obj = TestObjectFactory(override=True) + self.assertEqual(obj.as_dict(), + dict(one=None, two=False, three=None, four=True, five=None)) + def test_prevent_cyclic_traits(self): with self.assertRaises(errors.CyclicDefinitionError): From b169eaad0ceeb0764c344dff2c1d4ef5bb4971c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 10:46:10 +0200 Subject: [PATCH 253/714] Refactor the Makefile. - Add documentation to all targets - Add a help: target - Rename 'install-deps' to 'update' (clearer name) --- Makefile | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 011fee54..9f181e28 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,18 @@ EXAMPLES_DIR=examples COVERAGE = python $(shell which coverage) FLAKE8 = flake8 + all: default default: +# Package management +# ================== + + +# DOC: Remove temporary or compiled files clean: find . -type f -name '*.pyc' -delete find . -type f -path '*/__pycache__/*' -delete @@ -20,27 +26,40 @@ clean: @rm -rf tmp_test/ -install-deps: +# DOC: Install and/or upgrade dependencies +update: pip install --upgrade pip setuptools pip install --upgrade -r requirements_dev.txt pip freeze + +.PHONY: clean update + + +# Tests and quality +# ================= + + +# DOC: Run tests for all supported versions (creates a set of virtualenvs) testall: tox +# DOC: Run tests for the currently installed version test: python -Wdefault -m unittest $(TESTS_DIR) +# DOC: Test the examples example-test: $(MAKE) -C $(EXAMPLES_DIR) test # Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude +# DOC: Perform code quality tasks lint: - check-manifest $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py + check-manifest coverage: $(COVERAGE) erase @@ -48,8 +67,42 @@ coverage: $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" $(COVERAGE) html "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" + +.PHONY: test testall example-test lint coverage + + +# Documentation +# ============= + + +# DOC: Compile the documentation doc: $(MAKE) -C $(DOC_DIR) html -.PHONY: all default clean coverage doc install-deps lint test +# DOC: Show this help message +help: + @grep -A1 '^# DOC:' Makefile \ + | awk ' \ + BEGIN { FS="\n"; RS="--\n"; opt_len=0; } \ + { \ + doc=$$1; name=$$2; \ + sub("# DOC: ", "", doc); \ + sub(":", "", name); \ + if (length(name) > opt_len) { \ + opt_len = length(name) \ + } \ + opts[NR] = name; \ + docs[name] = doc; \ + } \ + END { \ + pat="%-" (opt_len + 4) "s %s\n"; \ + asort(opts); \ + for (i in opts) { \ + opt=opts[i]; \ + printf pat, opt, docs[opt] \ + } \ + }' + + +.PHONY: doc help From e9d83ca09d163e81ada97fe4183ea9aa0f0b07f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 12:34:45 +0200 Subject: [PATCH 254/714] Fix trait overriding. Traits will always be evaluated in their declaration order. Also, unify all "ordered declaration" code. --- factory/base.py | 5 +++-- factory/builder.py | 5 +++-- factory/declarations.py | 29 +++++++++++------------------ factory/utils.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/factory/base.py b/factory/base.py index daa8546a..2c7f9f94 100644 --- a/factory/base.py +++ b/factory/base.py @@ -11,6 +11,7 @@ from . import declarations from . import enums from . import errors +from . import utils logger = logging.getLogger('factory.generate') @@ -155,7 +156,7 @@ def __init__(self): @property def declarations(self): base_declarations = dict(self.base_declarations) - for name, param in self.parameters.items(): + for name, param in utils.sort_ordered_objects(self.parameters.items(), getter=lambda item: item[1]): base_declarations.update(param.as_declarations(name, base_declarations)) return base_declarations @@ -223,7 +224,7 @@ def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=N self.base_declarations[k] = v if params is not None: - for k, v in vars(params).items(): + for k, v in utils.sort_ordered_objects(vars(params).items(), getter=lambda item: item[1]): if not k.startswith('_'): self.parameters[k] = declarations.SimpleParameter.wrap(v) diff --git a/factory/builder.py b/factory/builder.py index a74fafbf..51ebfa9f 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -5,6 +5,7 @@ from . import declarations from . import enums from . import errors +from . import utils DeclarationWithContext = collections.namedtuple( @@ -72,9 +73,9 @@ def filter(self, entries): ] def sorted(self): - return sorted( + return utils.sort_ordered_objects( self.declarations, - key=lambda entry: self.declarations[entry].creation_counter, + getter=lambda entry: self.declarations[entry], ) def __contains__(self, key): diff --git a/factory/declarations.py b/factory/declarations.py index 6fcfd2f4..a0113685 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -15,21 +15,14 @@ logger = logging.getLogger('factory.generate') -class BaseDeclaration(object): +class BaseDeclaration(utils.OrderedBase): """A factory declaration. - Ordered declarations mark an attribute as needing lazy evaluation. + Declarations mark an attribute as needing lazy evaluation. This allows them to refer to attributes defined by other BaseDeclarations in the same factory. """ - creation_counter = 0 - - def __init__(self, **kwargs): - super(BaseDeclaration, self).__init__(**kwargs) - self.creation_counter = BaseDeclaration.creation_counter - BaseDeclaration.creation_counter += 1 - def evaluate(self, instance, step, extra): """Evaluate this declaration. @@ -463,7 +456,7 @@ def __repr__(self): return 'Maybe(%r, yes=%r, no=%r)' % (self.decider, self.yes, self.no) -class Parameter(object): +class Parameter(utils.OrderedBase): """A complex parameter, to be used in a Factory.Params section. Must implement: @@ -507,6 +500,7 @@ def wrap(cls, value): class Trait(Parameter): """The simplest complex parameter, it enables a bunch of new declarations based on a boolean flag.""" def __init__(self, **overrides): + super(Trait, self).__init__() self.overrides = overrides def as_declarations(self, field_name, declarations): @@ -529,21 +523,20 @@ def get_revdeps(self, parameters): """This might alter fields it's injecting.""" return [param for param in parameters if param in self.overrides] + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, + ', '.join('%s=%r' % t for t in self.overrides.items()) + ) + # Post-generation # =============== -class PostGenerationDeclaration(object): +class PostGenerationDeclaration(utils.OrderedBase): """Declarations to be called once the model object has been generated.""" - creation_counter = 0 - """Global creation counter of the declaration.""" - - def __init__(self, *args, **kwargs): - self.creation_counter = PostGenerationDeclaration.creation_counter - PostGenerationDeclaration.creation_counter += 1 - def call(self, instance, step, context): # pragma: no cover """Call this hook; no return value is expected. diff --git a/factory/utils.py b/factory/utils.py index cb91688f..75f352ba 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -140,3 +140,37 @@ def __iter__(self): def reset(self): self.next_elements.clear() self.next_elements.extend(self.past_elements) + + +class OrderedBase(object): + """Marks a class as being ordered. + + Each instance (even from subclasses) will share a global creation counter. + """ + + CREATION_COUNTER_FIELD = '_creation_counter' + + def __init__(self, **kwargs): + super(OrderedBase, self).__init__(**kwargs) + if type(self) is not OrderedBase: + bases = type(self).__mro__ + root = bases[bases.index(OrderedBase) - 1] + if not hasattr(root, self.CREATION_COUNTER_FIELD): + setattr(root, self.CREATION_COUNTER_FIELD, 0) + next_counter = getattr(self, self.CREATION_COUNTER_FIELD) + setattr(self, self.CREATION_COUNTER_FIELD, next_counter) + setattr(root, self.CREATION_COUNTER_FIELD, next_counter + 1) + + +def sort_ordered_objects(items, getter=lambda x: x): + """Sort an iterable of OrderedBase instances. + + Args: + items (iterable): the objects to sort + getter (callable or None): a function to extract the OrderedBase instance from an object. + + Examples: + >>> sort_ordered_objects([x, y, z]) + >>> sort_ordered_objects(v.items(), getter=lambda e: e[1]) + """ + return sorted(items, key=lambda x: getattr(getter(x), OrderedBase.CREATION_COUNTER_FIELD, -1)) From d4de769f3f6768bbfacd337836643a72c80c73aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 12:36:19 +0200 Subject: [PATCH 255/714] Improve docstrings on the 'builder' module. --- factory/builder.py | 48 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index 51ebfa9f..b25c14c0 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -21,7 +21,16 @@ class DeclarationSet(object): - """A set of declarations, including the recursive parameters.""" + """A set of declarations, including the recursive parameters. + + Attributes: + declarations (dict(name => declaration)): the top-level declarations + contexts (dict(name => dict(subfield => value))): the nested parameters related + to a given top-level declaration + + This object behaves similarly to a dict mapping a top-level declaration name to a + DeclarationWithContext, containing field name, declaration object and extra context. + """ def __init__(self, initial=None): self.declarations = {} @@ -30,6 +39,16 @@ def __init__(self, initial=None): @classmethod def split(cls, entry): + """Split a declaration name into a (declaration, subpath) tuple. + + Examples: + >>> DeclarationSet.split('foo__bar') + ('foo', 'bar') + >>> DeclarationSet.split('foo') + ('foo', None) + >>> DeclarationSet.split('foo__bar__baz') + ('foo', 'bar__baz') + """ if enums.SPLITTER in entry: return entry.split(enums.SPLITTER, 1) else: @@ -37,6 +56,12 @@ def split(cls, entry): @classmethod def join(cls, root, subkey): + """Rebuild a full declaration name from its components. + + for every string x, we have `join(split(x)) == x`. + """ + if subkey is None: + return root return enums.SPLITTER.join((root, subkey)) def copy(self): @@ -46,6 +71,11 @@ def copy(self): return other def update(self, values): + """Add new declarations to this set/ + + Args: + values (dict(name, declaration)): the declarations to ingest. + """ for k, v in values.items(): root, sub = self.split(k) if sub is None: @@ -67,6 +97,12 @@ def update(self, values): ) def filter(self, entries): + """Filter a set of declarations: keep only those related to this object. + + This will keep: + - Declarations that 'override' the current ones + - Declarations that are parameters to current ones + """ return [ entry for entry in entries if self.split(entry)[0] in self.declarations @@ -91,6 +127,11 @@ def __getitem__(self, key): def __iter__(self): return iter(self.declarations) + def values(self): + """Retrieve the list of declarations, with their context.""" + for name in self: + yield self[name] + def _items(self): """Extract a list of (key, value) pairs, suitable for our __init__.""" for name in self.declarations: @@ -98,11 +139,8 @@ def _items(self): for subkey, value in self.contexts[name].items(): yield self.join(name, subkey), value - def values(self): - for name in self: - yield self[name] - def as_dict(self): + """Return a dict() suitable for our __init__.""" return dict(self._items()) def __repr__(self): From 86cba2d814a28b4d9c8128f9ddba51a1032d1b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 23:32:08 +0200 Subject: [PATCH 256/714] [packaging] Improve tool configuration. - Ignore vim-related files for check-manifest - Fix warnings found by pyroma --- MANIFEST.in | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 8156ca2f..60209534 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,6 +7,6 @@ prune docs prune examples prune tests -global-exclude *.py[cod] __pycache__ +global-exclude *.py[cod] __pycache__ .*.sw[po] exclude Makefile tox.ini .flake8 diff --git a/setup.py b/setup.py index f1476276..37cd3d6c 100755 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ def get_version(package_name): url='https://github.com/FactoryBoy/factory_boy', keywords=['factory_boy', 'factory', 'fixtures'], packages=['factory'], + zip_safe=False, license='MIT', install_requires=[ 'Faker>=0.7.0', From ccb9be372a5a03ae361bc0aa522d98e66e8e3103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 23:32:52 +0200 Subject: [PATCH 257/714] [packaging] Introduce zest.releaser. This should simplify the release process, increasing the likelihood of performing frequent releases. --- Makefile | 6 +++++- docs/changelog.rst | 4 ++-- requirements_dev.txt | 1 + setup.cfg | 13 +++++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9f181e28..678bc14e 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,11 @@ update: pip freeze -.PHONY: clean update +release: + fullrelease + + +.PHONY: clean update release-patch release-minor release-major # Tests and quality diff --git a/docs/changelog.rst b/docs/changelog.rst index 1f965a65..22cf850a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,8 @@ ChangeLog ========= -master ------- +2.9.0 (unreleased) +------------------ This version brings massive changes to the core engine, thus reducing the number of corner cases and weird behaviourrs. diff --git a/requirements_dev.txt b/requirements_dev.txt index a7706c65..c6cccc56 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -9,6 +9,7 @@ SQLAlchemy mongoengine wheel tox +zest.releaser[recommended] Sphinx sphinx_rtd_theme diff --git a/setup.cfg b/setup.cfg index 2a9acf13..87ab85bf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,15 @@ [bdist_wheel] universal = 1 + +[zest.releaser] +; semver-style versions +version-levels = 3 + +; tags: vX.Y.Z; the double-percent is a guard against ConfigParser. +tag-format = v%%(version)s + +; Version flag location (we use __version__) +python-file-with-version = factory/__init__.py + +[distutils] +index-servers = pypi From 5022bf67da7c2583b6817c51b98cd8b19912486c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 23:38:37 +0200 Subject: [PATCH 258/714] Preparing release 2.9.0 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 22cf850a..0f620428 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.9.0 (unreleased) +2.9.0 (2017-07-30) ------------------ This version brings massive changes to the core engine, thus reducing the number of diff --git a/factory/__init__.py b/factory/__init__.py index f4ee32a4..d0a456ad 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.8.1' +__version__ = '2.9.0' __author__ = 'Raphaël Barrois ' From 040cb896bfc4f8369b2ad1c3a59dac805830a8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 Jul 2017 23:39:08 +0200 Subject: [PATCH 259/714] Back to development: 2.9.1 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f620428..62f1b06c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.9.1 (unreleased) +------------------ + +- Nothing changed yet. + + 2.9.0 (2017-07-30) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index d0a456ad..e0163b2f 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.9.0' +__version__ = '2.9.1.dev0' __author__ = 'Raphaël Barrois ' From bb1de00b767f461a456efd2e07d34650c2d0080c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 31 Jul 2017 00:06:10 +0200 Subject: [PATCH 260/714] Fix setup.cfg for Python2. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 87ab85bf..a90e807f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,8 @@ universal = 1 ; semver-style versions version-levels = 3 -; tags: vX.Y.Z; the double-percent is a guard against ConfigParser. -tag-format = v%%(version)s +; tags: vX.Y.Z +tag-format = v%(version)s ; Version flag location (we use __version__) python-file-with-version = factory/__init__.py From 86b0781411bf2730b6ee03877b6eed686ff1ae82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 31 Jul 2017 09:45:30 +0200 Subject: [PATCH 261/714] Revert "Fix setup.cfg for Python2." This reverts commit bb1de00b767f461a456efd2e07d34650c2d0080c. See https://github.com/zestsoftware/zest.releaser/issues/212 for details: the setup.cfg file is interpolated by distutils, and this fails. --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index a90e807f..ae937115 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,8 +5,8 @@ universal = 1 ; semver-style versions version-levels = 3 -; tags: vX.Y.Z -tag-format = v%(version)s +; tags: vX.Y.Z; the double-percent is a guard against ConfigParser. +#tag-format = v%%(version)s ; Version flag location (we use __version__) python-file-with-version = factory/__init__.py From 01b1c3462768844ea6012fc84cb24c60f30ff25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 31 Jul 2017 13:12:29 +0200 Subject: [PATCH 262/714] Advertise `self._meta.get_model_class()`. This is a safer entrypoint than `self._meta.model`, which might be the path to a class, or a framework-specific description (e.g with `DjangoModelFactory`). --- docs/reference.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 1996a993..d63ec688 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -37,6 +37,11 @@ Meta options .. versionadded:: 2.4.0 + .. method:: get_model_class() + + Returns the actual model class (:attr:`FactoryOptions.model` might be the + path to the class; this function will always return a proper class). + .. attribute:: abstract This attribute indicates that the :class:`Factory` subclass should not From edbf2728b851fcd4e504459ebdd3a0d873a65f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 31 Jul 2017 13:14:13 +0200 Subject: [PATCH 263/714] Fix syntax in docs/recipes.rst. --- docs/recipes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index d7817223..7a92ca4b 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -491,7 +491,7 @@ In order to get a dict, we'll just have to swap the model; the easiest way is to {'first_name': "Agent 001", 'username': 'john_doe'} Django models with `GenericForeignKeys` --------------------------------------- +--------------------------------------- For model which uses `GenericForeignKey `_ From d6aa0942f283dac13a9cb5cff5019f9a5f8d6a9d Mon Sep 17 00:00:00 2001 From: Austin Riba Date: Mon, 31 Jul 2017 11:22:30 -0700 Subject: [PATCH 264/714] Convert PostGenerationContext named tuple to dictionary for pretty printing --- factory/declarations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/declarations.py b/factory/declarations.py index a0113685..5e70247d 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -562,7 +562,7 @@ def call(self, instance, step, context): self.function.__name__, utils.log_pprint( (instance, step), - context, + context._asdict(), ), ) create = step.builder.strategy == enums.CREATE_STRATEGY From 01794bbb2b754faf7445e09e4e8af14e90f5c5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 1 Aug 2017 02:08:36 +0200 Subject: [PATCH 265/714] Improve warnings in FuzzyDate/FuzzyDateTime. Use the `stacklevel` hint to point at the proper line. --- factory/fuzzy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 70742a5d..77f15613 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -191,7 +191,7 @@ def __init__(self, start_date, end_date=None, **kwargs): if end_date is None: if random.randgen.state_set: cls_name = self.__class__.__name__ - warnings.warn(random_seed_warning.format(cls_name)) + warnings.warn(random_seed_warning.format(cls_name), stacklevel=2) end_date = datetime.date.today() if start_date > end_date: @@ -230,7 +230,7 @@ def __init__(self, start_dt, end_dt=None, if end_dt is None: if random.randgen.state_set: cls_name = self.__class__.__name__ - warnings.warn(random_seed_warning.format(cls_name)) + warnings.warn(random_seed_warning.format(cls_name), stacklevel=2) end_dt = self._now() self._check_bounds(start_dt, end_dt) From c7a61d83f9eee2e26fb6fb6d25427f6f65d510c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 2 Aug 2017 13:26:42 +0200 Subject: [PATCH 266/714] Update changelog. --- docs/changelog.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 62f1b06c..01596c5c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,10 @@ ChangeLog 2.9.1 (unreleased) ------------------ -- Nothing changed yet. +*Bugfix:* + + - Fix packaging issues (see https://github.com/zestsoftware/zest.releaser/issues/212) + - Don't crash when debugging PostGenerationDeclaration 2.9.0 (2017-07-30) From d05707393abea12b168d0e80ff07117d859be7ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 2 Aug 2017 13:27:07 +0200 Subject: [PATCH 267/714] Preparing release 2.9.1 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 01596c5c..058d4996 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.9.1 (unreleased) +2.9.1 (2017-08-02) ------------------ *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index e0163b2f..900628c5 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.9.1.dev0' +__version__ = '2.9.1' __author__ = 'Raphaël Barrois ' From 33c0fe9a4b6d994826abbfd664f5bde8e0152a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 2 Aug 2017 13:27:28 +0200 Subject: [PATCH 268/714] Back to development: 2.9.2 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 058d4996..527395a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.9.2 (unreleased) +------------------ + +- Nothing changed yet. + + 2.9.1 (2017-08-02) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index 900628c5..c4a950df 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.9.1' +__version__ = '2.9.2.dev0' __author__ = 'Raphaël Barrois ' From 14361e3fb02c993d69a38fe6c1430ffec95079a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 3 Aug 2017 10:53:20 +0200 Subject: [PATCH 269/714] Add some tests around post_generation overrides. --- tests/test_using.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index b4c73bb2..30a584f9 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1986,6 +1986,38 @@ class Meta: obj = TestObjectFactory.build(bar=42, bar__foo=13) + def test_post_generation_override_with_extra(self): + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = 1 + + @factory.post_generation + def incr_one(self, _create, override, **extra): + multiplier = extra.get('multiplier', 1) + if override is None: + override = 1 + self.one += override * multiplier + + obj = TestObjectFactory.build() + self.assertEqual(1 + 1 * 1, obj.one) + obj = TestObjectFactory.build(incr_one=2) + self.assertEqual(1 + 2 * 1, obj.one) + obj = TestObjectFactory.build(incr_one__multiplier=4) + self.assertEqual(1 + 1 * 4, obj.one) + obj = TestObjectFactory.build(incr_one=2, incr_one__multiplier=5) + self.assertEqual(1 + 2 * 5, obj.one) + + # Passing extras through inherited params + class OtherTestObjectFactory(TestObjectFactory): + class Params: + incr_one__multiplier = 4 + + obj = OtherTestObjectFactory.build() + self.assertEqual(1 + 1 * 4, obj.one) + + def test_post_generation_method_call(self): calls = [] From 5076d619720e2161b7fcf7a5d64ca02e06077a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 3 Aug 2017 23:00:49 +0200 Subject: [PATCH 270/714] Fix nasty declaration corruption bug. The issue arose with code similar to this: class SomeFactory(factory.Factory): foo = factory.SubFactory(FooFactory) class OtherFactory(factory.Factory): some = factory.SubFactory(SomeFactory) some__foo__bar = 1 When calling: >>> OtherFactory(some__foo=x) The `foo=x` was merged into OtherFactory's declarations, as if it had been defined directly. This lead to the following nasty bug: >>> OtherFactory(some__foo=x) >>> OtherFactory().some.foo is x True --- factory/builder.py | 5 +---- tests/test_using.py | 48 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index b25c14c0..041c7990 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -65,10 +65,7 @@ def join(cls, root, subkey): return enums.SPLITTER.join((root, subkey)) def copy(self): - other = self.__class__() - other.declarations = self.declarations.copy() - other.contexts = self.contexts.copy() - return other + return self.__class__(self.as_dict()) def update(self, values): """Add new declarations to this set/ diff --git a/tests/test_using.py b/tests/test_using.py index 30a584f9..b2ef5030 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1439,6 +1439,54 @@ class Meta: self.assertEqual(obj, outer.wrapped) self.assertEqual('four', outer.wrapped.two) + def test_deep_nested_subfactory(self): + counter = iter(range(100)) + + class Node(object): + def __init__(self, label, child=None): + self.id = next(counter) + self.label = label + self.child = child + + class LeafFactory(factory.Factory): + class Meta: + model = Node + label = 'leaf' + + class BranchFactory(factory.Factory): + class Meta: + model = Node + label = 'branch' + child = factory.SubFactory(LeafFactory) + + class TreeFactory(factory.Factory): + class Meta: + model = Node + label = 'tree' + child = factory.SubFactory(BranchFactory) + child__child__label = 'magic-leaf' + + leaf = LeafFactory() + # Magic corruption did happen here once: + # forcing child__child=X while another part already set another value + # on child__child__label meant that the value passed for child__child + # was merged into the factory's inner declaration dict. + mtree_1 = TreeFactory(child__child=leaf) + mtree_2 = TreeFactory() + + self.assertEqual(0, mtree_1.child.child.id) + self.assertEqual('leaf', mtree_1.child.child.label) + self.assertEqual(1, mtree_1.child.id) + self.assertEqual('branch', mtree_1.child.label) + self.assertEqual(2, mtree_1.id) + self.assertEqual('tree', mtree_1.label) + self.assertEqual(3, mtree_2.child.child.id) + self.assertEqual('magic-leaf', mtree_2.child.child.label) + self.assertEqual(4, mtree_2.child.id) + self.assertEqual('branch', mtree_2.child.label) + self.assertEqual(5, mtree_2.id) + self.assertEqual('tree', mtree_2.label) + def test_sub_factory_and_inheritance(self): """Test inheriting from a factory with subfactories, overriding.""" class TestObject(object): From ea4f43cb246c23e0203e397c23149ede80583d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 3 Aug 2017 23:06:45 +0200 Subject: [PATCH 271/714] Update changelog. --- docs/changelog.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 527395a5..5351082e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,11 @@ ChangeLog 2.9.2 (unreleased) ------------------ -- Nothing changed yet. +*Bugfix:* + + - Fix declaration corruption bug when a factory defined `foo__bar__baz=1` and a caller + provided a `foo__bar=x` parameter at call time: this got merged into the factory's base + declarations. 2.9.1 (2017-08-02) From e11497ab7be2a7388ab5d0d55af676b9bf48f606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 3 Aug 2017 23:07:12 +0200 Subject: [PATCH 272/714] Preparing release 2.9.2 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5351082e..91a5ae8a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.9.2 (unreleased) +2.9.2 (2017-08-03) ------------------ *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index c4a950df..1f93d9c2 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.9.2.dev0' +__version__ = '2.9.2' __author__ = 'Raphaël Barrois ' From db19030c26fd0a67cd1f32fac0b684fd6e5962c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 3 Aug 2017 23:07:44 +0200 Subject: [PATCH 273/714] Back to development: 2.9.3 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 91a5ae8a..6ccfa05e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.9.3 (unreleased) +------------------ + +- Nothing changed yet. + + 2.9.2 (2017-08-03) ------------------ diff --git a/factory/__init__.py b/factory/__init__.py index 1f93d9c2..feab6733 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.9.2' +__version__ = '2.9.3.dev0' __author__ = 'Raphaël Barrois ' From 6fce625ecc9f1045f781ccc52d760b1820303ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 3 Aug 2017 23:15:42 +0200 Subject: [PATCH 274/714] Changelog: Add version references. --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6ccfa05e..72203cb4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ ChangeLog - Nothing changed yet. +.. _v2.9.2: 2.9.2 (2017-08-03) ------------------ @@ -16,6 +17,7 @@ ChangeLog provided a `foo__bar=x` parameter at call time: this got merged into the factory's base declarations. +.. _v2.9.1: 2.9.1 (2017-08-02) ------------------ @@ -25,6 +27,7 @@ ChangeLog - Fix packaging issues (see https://github.com/zestsoftware/zest.releaser/issues/212) - Don't crash when debugging PostGenerationDeclaration +.. _v2.9.0: 2.9.0 (2017-07-30) ------------------ From 9a9a05c420113446de4229cdc35ac98b620e6dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 5 Aug 2017 01:00:41 +0200 Subject: [PATCH 275/714] tests: Remove conditional test running. Our test setup requires Django, SQLAlchemy and MongoEngine to be installed. However, a user may still disable MongoDB-related tests by setting the SKIP_MONGOENGINE=1 environment variable. --- tests/test_alchemy.py | 26 +------ tests/test_django.py | 149 ++++++++++++++++---------------------- tests/test_mongoengine.py | 42 +++++------ 3 files changed, 82 insertions(+), 135 deletions(-) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 1598e313..be1a222a 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -8,26 +8,8 @@ from .compat import mock import warnings - -try: - import sqlalchemy -except ImportError: - sqlalchemy = None - -if sqlalchemy: - from factory.alchemy import SQLAlchemyModelFactory - from .alchemyapp import models -else: - - class Fake(object): - class Meta: - sqlalchemy_session = None - - models = Fake() - models.StandardModel = Fake() - models.NonIntegerPk = Fake() - models.session = Fake() - SQLAlchemyModelFactory = Fake +from factory.alchemy import SQLAlchemyModelFactory +from .alchemyapp import models class StandardFactory(SQLAlchemyModelFactory): @@ -55,7 +37,6 @@ class Meta: id = factory.Sequence(lambda n: n) -@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyPkSequenceTestCase(unittest.TestCase): def setUp(self): @@ -94,7 +75,6 @@ def test_pk_force_value(self): self.assertEqual(0, std2.id) -@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemySessionPersistenceTestCase(unittest.TestCase): def setUp(self): super(SQLAlchemySessionPersistenceTestCase, self).setUp() @@ -172,7 +152,6 @@ class Meta: self.mock_session.flush.assert_called_once_with() -@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): super(SQLAlchemyNonIntegerPkTestCase, self).setUp() @@ -207,7 +186,6 @@ def test_force_pk(self): self.assertEqual('foo0', nonint2.id) -@unittest.skipIf(sqlalchemy is None, "SQLalchemy not installed.") class SQLAlchemyNoSessionTestCase(unittest.TestCase): def test_create_raises_exception_when_no_session_was_set(self): diff --git a/tests/test_django.py b/tests/test_django.py index 3840d301..17a79a7b 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -7,27 +7,19 @@ from .compat import is_python2, unittest, mock -try: - import django -except ImportError: # pragma: no cover - django = None +import django # Setup Django as soon as possible -if django is not None: - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') - - django.setup() - from django import test as django_test - from django.conf import settings - from django.db import models as django_models - from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner - from django.test import utils as django_test_utils - from django.db.models import signals - from .djapp import models - -else: - django_test = unittest +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') +django.setup() +from django import test as django_test +from django.conf import settings +from django.db import models as django_models +from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner +from django.test import utils as django_test_utils +from django.db.models import signals +from .djapp import models try: @@ -52,8 +44,6 @@ def setUpModule(): - if django is None: # pragma: no cover - raise unittest.SkipTest("Django not installed") django_test_utils.setup_test_environment() runner = DjangoTestSuiteRunner() runner_state = runner.setup_databases() @@ -64,99 +54,95 @@ def setUpModule(): def tearDownModule(): - if django is None: # pragma: no cover - return runner = test_state['runner'] runner_state = test_state['runner_state'] runner.teardown_databases(runner_state) django_test_utils.teardown_test_environment() -if django is not None: - class StandardFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.StandardModel +class StandardFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) - class StandardFactoryWithPKField(factory.django.DjangoModelFactory): - class Meta: - model = models.StandardModel - django_get_or_create = ('pk',) +class StandardFactoryWithPKField(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + django_get_or_create = ('pk',) - foo = factory.Sequence(lambda n: "foo%d" % n) - pk = None + foo = factory.Sequence(lambda n: "foo%d" % n) + pk = None - class NonIntegerPkFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.NonIntegerPk +class NonIntegerPkFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.NonIntegerPk - foo = factory.Sequence(lambda n: "foo%d" % n) - bar = '' + foo = factory.Sequence(lambda n: "foo%d" % n) + bar = '' - class MultifieldModelFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.MultifieldModel - django_get_or_create = ['slug'] +class MultifieldModelFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.MultifieldModel + django_get_or_create = ['slug'] - text = factory.Faker('text') + text = factory.Faker('text') - class AbstractBaseFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.AbstractBase - abstract = True +class AbstractBaseFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.AbstractBase + abstract = True - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) - class ConcreteSonFactory(AbstractBaseFactory): - class Meta: - model = models.ConcreteSon +class ConcreteSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteSon - class AbstractSonFactory(AbstractBaseFactory): - class Meta: - model = models.AbstractSon +class AbstractSonFactory(AbstractBaseFactory): + class Meta: + model = models.AbstractSon - class ConcreteGrandSonFactory(AbstractBaseFactory): - class Meta: - model = models.ConcreteGrandSon +class ConcreteGrandSonFactory(AbstractBaseFactory): + class Meta: + model = models.ConcreteGrandSon - class WithFileFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithFile +class WithFileFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithFile - if django is not None: - afile = factory.django.FileField() + if django is not None: + afile = factory.django.FileField() - class WithImageFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithImage +class WithImageFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithImage - if django is not None: - animage = factory.django.ImageField() + if django is not None: + animage = factory.django.ImageField() - class WithSignalsFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithSignals +class WithSignalsFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithSignals - class WithCustomManagerFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.WithCustomManager +class WithCustomManagerFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithCustomManager - foo = factory.Sequence(lambda n: "foo%d" % n) + foo = factory.Sequence(lambda n: "foo%d" % n) -@unittest.skipIf(django is None, "Django not installed.") class ModelTests(django_test.TestCase): def test_unset_model(self): class UnsetModelFactory(factory.django.DjangoModelFactory): @@ -175,7 +161,6 @@ class Meta: self.assertEqual(obj, models.StandardModel.objects.using('replica').get()) -@unittest.skipIf(django is None, "Django not installed.") class DjangoPkSequenceTestCase(django_test.TestCase): def setUp(self): super(DjangoPkSequenceTestCase, self).setUp() @@ -212,7 +197,6 @@ def test_pk_force_value(self): self.assertEqual(11, std2.pk) -@unittest.skipIf(django is None, "Django not installed.") class DjangoGetOrCreateTests(django_test.TestCase): def test_simple_call(self): obj1 = MultifieldModelFactory(slug='slug1') @@ -235,7 +219,6 @@ def test_multicall(self): self.assertEqual(2, models.MultifieldModel.objects.count()) -@unittest.skipIf(django is None, "Django not installed.") class DjangoPkForceTestCase(django_test.TestCase): def setUp(self): super(DjangoPkForceTestCase, self).setUp() @@ -260,7 +243,6 @@ def test_reuse_pk(self): self.assertEqual('bar', std2.foo) -@unittest.skipIf(django is None, "Django not installed.") class DjangoModelLoadingTestCase(django_test.TestCase): """Tests class Meta: model = 'app.Model' pattern.""" @@ -321,7 +303,6 @@ class Meta: self.assertEqual(2, e3.foo) -@unittest.skipIf(django is None, "Django not installed.") class DjangoNonIntegerPkTestCase(django_test.TestCase): def setUp(self): super(DjangoNonIntegerPkTestCase, self).setUp() @@ -358,7 +339,6 @@ def test_force_pk(self): self.assertEqual('foo0', nonint2.pk) -@unittest.skipIf(django is None, "Django not installed.") class DjangoAbstractBaseSequenceTestCase(django_test.TestCase): def test_auto_sequence(self): """The sequence of the concrete son of an abstract model should be autonomous.""" @@ -387,7 +367,6 @@ class Meta: self.assertEqual("foo0", obj.foo) -@unittest.skipIf(django is None, "Django not installed.") class DjangoRelatedFieldTestCase(django_test.TestCase): @classmethod @@ -413,7 +392,6 @@ def test_direct_related_create(self): self.assertEqual(ptr.pointed, models.PointedModel.objects.get()) -@unittest.skipIf(django is None, "Django not installed.") class DjangoFileFieldTestCase(unittest.TestCase): def tearDown(self): @@ -524,7 +502,6 @@ def test_no_file(self): self.assertFalse(o.afile) -@unittest.skipIf(django is None, "Django not installed.") class DjangoParamsTestCase(django_test.TestCase): def test_undeclared_fields(self): @@ -566,7 +543,6 @@ class Params: self.assertEqual('bar', o.pointed.foo) -@unittest.skipIf(django is None, "Django not installed.") class DjangoFakerTestCase(unittest.TestCase): def test_random(self): class StandardModelFactory(factory.django.DjangoModelFactory): @@ -579,7 +555,6 @@ class Meta: self.assertNotEqual(o1.foo, o2.foo) -@unittest.skipIf(django is None, "Django not installed.") @unittest.skipIf(Image is None, "PIL not installed.") class DjangoImageFieldTestCase(unittest.TestCase): @@ -742,7 +717,6 @@ def test_with_func(self): self.assertEqual(32, i.height) -@unittest.skipIf(django is None, "Django not installed.") class PreventSignalsTestCase(unittest.TestCase): def setUp(self): self.handlers = mock.MagicMock() @@ -863,7 +837,6 @@ def generate(cls): self.assertSignalsReactivated() -@unittest.skipIf(django is None, "Django not installed.") class DjangoCustomManagerTestCase(unittest.TestCase): def test_extra_args(self): diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 56cf2481..b1db6cbe 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -8,39 +8,35 @@ from .compat import unittest -try: - import mongoengine -except ImportError: - mongoengine = None +import mongoengine -if os.environ.get('SKIP_MONGOENGINE') == '1': - mongoengine = None +from factory.mongoengine import MongoEngineFactory -if mongoengine: - from factory.mongoengine import MongoEngineFactory +class Address(mongoengine.EmbeddedDocument): + street = mongoengine.StringField() - class Address(mongoengine.EmbeddedDocument): - street = mongoengine.StringField() +class Person(mongoengine.Document): + name = mongoengine.StringField() + address = mongoengine.EmbeddedDocumentField(Address) - class Person(mongoengine.Document): - name = mongoengine.StringField() - address = mongoengine.EmbeddedDocumentField(Address) +class AddressFactory(MongoEngineFactory): + class Meta: + model = Address - class AddressFactory(MongoEngineFactory): - class Meta: - model = Address + street = factory.Sequence(lambda n: 'street%d' % n) - street = factory.Sequence(lambda n: 'street%d' % n) +class PersonFactory(MongoEngineFactory): + class Meta: + model = Person - class PersonFactory(MongoEngineFactory): - class Meta: - model = Person + name = factory.Sequence(lambda n: 'name%d' % n) + address = factory.SubFactory(AddressFactory) - name = factory.Sequence(lambda n: 'name%d' % n) - address = factory.SubFactory(AddressFactory) +SKIP_MONGODB = bool(os.environ.get('SKIP_MONGOENGINE') == '1') -@unittest.skipIf(mongoengine is None, "mongoengine not installed.") + +@unittest.skipIf(SKIP_MONGODB, "mongodb tests disabled.") class MongoEngineTestCase(unittest.TestCase): db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') From 602a5513bb11ce72242216418e13d1d05f82939e Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 30 Aug 2017 10:37:44 +0100 Subject: [PATCH 276/714] correct spelling mistakes --- docs/changelog.rst | 2 +- docs/reference.rst | 2 +- factory/base.py | 2 +- factory/declarations.py | 2 +- factory/django.py | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 72203cb4..c496c71b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -199,7 +199,7 @@ A simple shell command to upgrade the code would be: # grep -l: only file names, not matching lines sed -i 's/FACTORY_FOR =/class Meta:\n model =/' $(grep -l FACTORY_FOR $(find . -name '*.py')) -This takes care of all ``FACTORY_FOR`` occurences; the files containing other attributes to rename can be found with ``grep -R FACTORY .`` +This takes care of all ``FACTORY_FOR`` occurrences; the files containing other attributes to rename can be found with ``grep -R FACTORY .`` .. _v2.4.1: diff --git a/docs/reference.rst b/docs/reference.rst index d63ec688..0ba47a8a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1296,7 +1296,7 @@ Iterator .. class:: Iterator(iterable, cycle=True, getter=None) - The :class:`Iterator` declaration takes succesive values from the given + The :class:`Iterator` declaration takes successive values from the given iterable. When it is exhausted, it starts again from zero (unless ``cycle=False``). .. attribute:: cycle diff --git a/factory/base.py b/factory/base.py index 2c7f9f94..6f8d1d67 100644 --- a/factory/base.py +++ b/factory/base.py @@ -274,7 +274,7 @@ def reset_sequence(self, value=None, force=False): if self.counter_reference is not self and not force: raise ValueError( - "Can't reset a sequence on decendant factory %r; reset sequence on %r or use `force=True`." + "Can't reset a sequence on descendant factory %r; reset sequence on %r or use `force=True`." % (self.factory, self.counter_reference.factory)) if value is None: diff --git a/factory/declarations.py b/factory/declarations.py index 5e70247d..2ed143e9 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -261,7 +261,7 @@ class ParameteredAttribute(BaseDeclaration): """Base class for attributes expecting parameters. Attributes: - defaults (dict): Default values for the paramters. + defaults (dict): Default values for the parameters. May be overridden by call-time parameters. Class attributes: diff --git a/factory/django.py b/factory/django.py index 89ccc054..09f59206 100644 --- a/factory/django.py +++ b/factory/django.py @@ -310,7 +310,7 @@ def __call__(self, callable_obj): @classmethod @functools.wraps(generate_method) def wrapped_generate(*args, **kwargs): - # A mute_signals() object is not reentrant; use a copy everytime. + # A mute_signals() object is not reentrant; use a copy every time. with self.copy(): return generate_method(*args, **kwargs) @@ -320,7 +320,7 @@ def wrapped_generate(*args, **kwargs): else: @functools.wraps(callable_obj) def wrapper(*args, **kwargs): - # A mute_signals() object is not reentrant; use a copy everytime. + # A mute_signals() object is not reentrant; use a copy every time. with self.copy(): return callable_obj(*args, **kwargs) return wrapper From 1c92125981276956cb2950ca2d29478ad93c5d17 Mon Sep 17 00:00:00 2001 From: Ber Clausen Date: Thu, 24 Aug 2017 22:51:19 -0700 Subject: [PATCH 277/714] Test creating django objects with `OneToOneField` relationships --- tests/djapp/models.py | 9 +++-- tests/test_django.py | 76 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 92634e5f..20cdb8c0 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -17,6 +17,7 @@ from django.conf import settings from django.db import models + class StandardModel(models.Model): foo = models.CharField(max_length=20) @@ -59,10 +60,12 @@ class PointedModel(models.Model): foo = models.CharField(max_length=20) -class PointingModel(models.Model): - foo = models.CharField(max_length=20) +class PointerModel(models.Model): + bar = models.CharField(max_length=20) pointed = models.OneToOneField( - PointedModel, related_name='pointer', null=True, + PointedModel, + related_name='pointer', + null=True, on_delete=models.CASCADE ) diff --git a/tests/test_django.py b/tests/test_django.py index 17a79a7b..e72a178e 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -375,21 +375,73 @@ def setUpClass(cls): class PointedFactory(factory.django.DjangoModelFactory): class Meta: model = models.PointedModel - foo = 'ahah' + foo = 'foo' class PointerFactory(factory.django.DjangoModelFactory): class Meta: - model = models.PointingModel - pointed = factory.SubFactory(PointedFactory, foo='hihi') - foo = 'bar' + model = models.PointerModel + bar = 'bar' + pointed = factory.SubFactory(PointedFactory, foo='new_foo') + + class PointedRelatedFactory(PointedFactory): + pointer = factory.RelatedFactory(PointerFactory, 'pointed') + + class PointerExtraFactory(PointerFactory): + pointed__foo = 'extra_new_foo' + + class PointedRelatedExtraFactory(PointedRelatedFactory): + pointer__bar = 'extra_new_bar' cls.PointedFactory = PointedFactory cls.PointerFactory = PointerFactory - - def test_direct_related_create(self): - ptr = self.PointerFactory() - self.assertEqual('hihi', ptr.pointed.foo) - self.assertEqual(ptr.pointed, models.PointedModel.objects.get()) + cls.PointedRelatedFactory = PointedRelatedFactory + cls.PointerExtraFactory = PointerExtraFactory + cls.PointedRelatedExtraFactory = PointedRelatedExtraFactory + + def test_create_pointed(self): + pointed = self.PointedFactory() + self.assertEqual(pointed, models.PointedModel.objects.get()) + self.assertEqual(pointed.foo, 'foo') + + def test_create_pointer(self): + pointer = self.PointerFactory() + self.assertEqual(pointer.pointed, models.PointedModel.objects.get()) + self.assertEqual(pointer.pointed.foo, 'new_foo') + + def test_create_pointer_with_deep_context(self): + pointer = self.PointerFactory(pointed__foo='new_new_foo') + self.assertEqual(pointer, models.PointerModel.objects.get()) + self.assertEqual(pointer.bar, 'bar') + self.assertEqual(pointer.pointed, models.PointedModel.objects.get()) + self.assertEqual(pointer.pointed.foo, 'new_new_foo') + + def test_create_pointed_related(self): + pointed = self.PointedRelatedFactory() + self.assertEqual(pointed, models.PointedModel.objects.get()) + self.assertEqual(pointed.foo, 'foo') + self.assertEqual(pointed.pointer, models.PointerModel.objects.get()) + self.assertEqual(pointed.pointer.bar, 'bar') + + def test_create_pointed_related_with_deep_context(self): + pointed = self.PointedRelatedFactory(pointer__bar='new_new_bar') + self.assertEqual(pointed, models.PointedModel.objects.get()) + self.assertEqual(pointed.foo, 'foo') + self.assertEqual(pointed.pointer, models.PointerModel.objects.get()) + self.assertEqual(pointed.pointer.bar, 'new_new_bar') + + def test_create_pointer_extra(self): + pointer = self.PointerExtraFactory() + self.assertEqual(pointer, models.PointerModel.objects.get()) + self.assertEqual(pointer.bar, 'bar') + self.assertEqual(pointer.pointed, models.PointedModel.objects.get()) + self.assertEqual(pointer.pointed.foo, 'extra_new_foo') + + def test_create_pointed_related_extra(self): + pointed = self.PointedRelatedExtraFactory() + self.assertEqual(pointed, models.PointedModel.objects.get()) + self.assertEqual(pointed.foo, 'foo') + self.assertEqual(pointed.pointer, models.PointerModel.objects.get()) + self.assertEqual(pointed.pointer.bar, 'extra_new_bar') class DjangoFileFieldTestCase(unittest.TestCase): @@ -529,17 +581,17 @@ class Params: class PointerFactory(factory.django.DjangoModelFactory): class Meta: - model = models.PointingModel + model = models.PointerModel pointed = factory.SubFactory(PointedFactory) class Params: with_bar = factory.Trait( - foo='bar', + bar='bar', pointed__with_bar=True, ) o = PointerFactory(with_bar=True) - self.assertEqual('bar', o.foo) + self.assertEqual('bar', o.bar) self.assertEqual('bar', o.pointed.foo) From e55493d7aa7aebead8fd5f689503477cb0c00e0e Mon Sep 17 00:00:00 2001 From: "Petros G. Sideris" Date: Thu, 17 Aug 2017 23:27:16 +0100 Subject: [PATCH 278/714] Update travis, tox and setup for Pypy3. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running `tox` (or `make testall`) will run the tests against both CPython and pypy versions. Signed-off-by: Raphaël Barrois --- .travis.yml | 4 +++- README.rst | 2 +- setup.py | 3 ++- tox.ini | 4 +++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 58f13fab..8570f161 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,9 @@ matrix: # Pypy - python: "pypy" - env: TOXENV=py27-django111-alchemy10-mongoengine010 + env: TOXENV=pypy27-django111-alchemy10-mongoengine010 + - python: "pypy3.5-5.8.0" + env: TOXENV=pypy3-django19-alchemy10-mongoengine010 # Linting - python: "3.6" diff --git a/README.rst b/README.rst index 7d429ae6..a4c5c3a8 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy; it requires only the standard Python library. +factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy 2.7 and 5.8; it requires only the standard Python library. Download diff --git a/setup.py b/setup.py index 37cd3d6c..ed04864e 100755 --- a/setup.py +++ b/setup.py @@ -68,7 +68,8 @@ def get_version(package_name): "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: Implementation :: PyPy :: 2.7", + "Programming Language :: Python :: Implementation :: PyPy :: 3.5", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", ], diff --git a/tox.ini b/tox.ini index 607999d1..5d33a3da 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = - py{27,34,35,36}-django{18,19,110,111}-alchemy10-mongoengine010 + py{27,34,35,36}-django{18,19,110,111}-alchemy10-mongoengine010, + pypy27-django{18,19,110,111}-alchemy10-mongoengine010, + pypy3-django{18,19}-alchemy10-mongoengine010, examples lint From 24b835048f8b82fe8a0b011d49f504c64aa84711 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 27 Oct 2017 21:38:25 -0700 Subject: [PATCH 279/714] Enable pip cache in Travis CI Can speed up builds and reduce load on PyPI servers. For more information, see: https://docs.travis-ci.com/user/caching/#pip-cache --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8570f161..0818559f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ sudo: false language: python +cache: pip script: - tox From 2a265ebd4a15643285f45b51400f3d8abc31aa9d Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 27 Oct 2017 21:34:17 -0700 Subject: [PATCH 280/714] Include license file in the generated wheel package The wheel package format supports including the license file. This is done using the [metadata] section in the setup.cfg file. For additional information on this feature, see: https://wheel.readthedocs.io/en/stable/index.html#including-the-license-in-the-generated-wheel-file --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index ae937115..6954392d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,9 @@ [bdist_wheel] universal = 1 +[metadata] +license_file = LICENSE + [zest.releaser] ; semver-style versions version-levels = 3 From f003d847d70547aa4a806356c5a77a7c8f2ecce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simen=20Heggest=C3=B8yl?= Date: Wed, 25 Oct 2017 17:01:26 +0200 Subject: [PATCH 281/714] Update sequence usage example in the introduction Since commit 13d310fa14f4e4b9a559f8b7887f2a2492357013, sequences have started on 0 by default instead of 1. Update the usage example in the introduction text to reflect that. --- docs/introduction.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index 5b535c94..58f9dfb3 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -100,9 +100,9 @@ This is achieved with the :class:`~factory.Sequence` declaration: .. code-block:: pycon >>> UserFactory() - + >>> UserFactory() - + .. note:: For more complex situations, you may also use the :meth:`~factory.@sequence` decorator (note that ``self`` is not added as first parameter): From 22f22e54b0420245549d671fdee6a46f8a37846e Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Fri, 10 Nov 2017 01:11:25 +0300 Subject: [PATCH 282/714] Updates README.rst with svg badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a4c5c3a8..9cc65e56 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ factory_boy =========== -.. image:: https://secure.travis-ci.org/FactoryBoy/factory_boy.png?branch=master +.. image:: https://secure.travis-ci.org/FactoryBoy/factory_boy.svg?branch=master :target: http://travis-ci.org/FactoryBoy/factory_boy/ .. image:: https://img.shields.io/pypi/v/factory_boy.svg From 2ce595f89416894ba69318f22ccf0c7f232b1333 Mon Sep 17 00:00:00 2001 From: Josh Crompton Date: Thu, 20 Apr 2017 09:55:49 +1000 Subject: [PATCH 283/714] Update changelog with breaking change in v2.7.0 --- docs/changelog.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c496c71b..6bf99d58 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -89,7 +89,12 @@ corner cases and weird behaviourrs. *Removed:* - - :issue:`278`: Formally drop support for Python2.6 + - :issue:`278`: Formally drop support for Python2.6 + +.. warning:: Version 2.7.0 moves all error classes to + `factory.errors`. This breaks existing import statements + for any error classes except those importing + `FactoryError` directly from the `factory` module. .. _v2.6.1: From 8ff315bc7efaa4c1b4bee268d5e3d2e16a430dac Mon Sep 17 00:00:00 2001 From: Susan Dreher Date: Mon, 31 Jul 2017 13:37:29 -0400 Subject: [PATCH 284/714] Update recipes.rst Update recipes.rst to reflect changes to underlying base factory. The `_generate` method can no longer be overridden. Overriding at the create level seems to make the most sense. --- docs/recipes.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 7a92ca4b..a1289bac 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -119,12 +119,12 @@ factory_boy allows to define attributes of such profiles dynamically when creati profile = factory.RelatedFactory(ProfileFactory, 'user') @classmethod - def _generate(cls, create, attrs): - """Override the default _generate() to disable the post-save signal.""" + def _create(cls, model_class, *args, **kwargs): + """Override the default _create() 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)._generate(create, attrs) + user = super(UserFactory, cls)._create(model_class, *args, **kwargs) post_save.connect(handler_create_user_profile, auth_models.User) return user From 6de59225c92f2d6c588315fc956be5c82a2d8a60 Mon Sep 17 00:00:00 2001 From: Alejandro De Jesus Date: Sun, 15 Oct 2017 12:33:45 -0400 Subject: [PATCH 285/714] refs #342 . Add LazyFunction mutable collection copy example --- docs/reference.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 0ba47a8a..a2f4d111 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -770,6 +770,20 @@ and return a value. >>> LogFactory(timestamp=now - timedelta(days=1)) +:class:`LazyFunction` is also useful for assigning copies of mutable objects +(like lists) to an objects' property. Example: + +.. code-block:: python + + DEFAULT_TEAM = ['Player1', 'Player2'] + + class TeamFactory(factory.Factory): + class Meta: + model = models.Team + + teammates = factory.LazyFunction(lambda: list(DEFAULT_TEAM)) + + Decorator ~~~~~~~~~ From 4486dd28f7ef907cfaa65e9e1f75a38ffd01ec68 Mon Sep 17 00:00:00 2001 From: Alejandro De Jesus Date: Thu, 19 Oct 2017 21:58:48 -0400 Subject: [PATCH 286/714] Fix typo in LazyFunction example --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index a2f4d111..b7c61082 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -771,7 +771,7 @@ and return a value. :class:`LazyFunction` is also useful for assigning copies of mutable objects -(like lists) to an objects' property. Example: +(like lists) to an object's property. Example: .. code-block:: python From d1e5210a4d9989f2ed4541b764f9354f36939242 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 10 Jan 2018 20:52:08 -0800 Subject: [PATCH 287/714] Pass python_requires argument to setuptools Helps pip decide what version of the library to install. https://packaging.python.org/tutorials/distributing-packages/#python-requires > If your project only runs on certain Python versions, setting the > python_requires argument to the appropriate PEP 440 version specifier > string will prevent pip from installing the project on other Python > versions. https://setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords > python_requires > > A string corresponding to a version specifier (as defined in PEP 440) > for the Python version, used to specify the Requires-Python defined in > PEP 345. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ed04864e..fabf1811 100755 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ def get_version(package_name): packages=['factory'], zip_safe=False, license='MIT', + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", install_requires=[ 'Faker>=0.7.0', ], From 21fa6bf2cee518d6f81aed69ad93510249805984 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 24 Jan 2018 13:40:14 -0500 Subject: [PATCH 288/714] factory_girl --> factory_bot --- README.rst | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9cc65e56..9251e085 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ factory_boy :target: https://pypi.python.org/pypi/factory_boy/ :alt: License -factory_boy is a fixtures replacement based on thoughtbot's `factory_girl `_. +factory_boy is a fixtures replacement based on thoughtbot's `factory_bot `_. As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures with easy-to-use factories for complex object. diff --git a/setup.py b/setup.py index fabf1811..1e33b8d6 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def get_version(package_name): setup( name='factory_boy', version=get_version(PACKAGE), - description="A versatile test fixtures replacement based on thoughtbot's factory_girl for Ruby.", + description="A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby.", long_description=codecs.open(os.path.join(root_dir, 'README.rst'), 'r', 'utf-8').read(), author='Mark Sandstrom', author_email='mark@deliciouslynerdy.com', From 61a9ecd01892ff756d46d70add69b1b301f0e81e Mon Sep 17 00:00:00 2001 From: Rich Rauenzahn Date: Thu, 25 Jan 2018 12:05:25 -0800 Subject: [PATCH 289/714] Add ability to reset() a fresh Iterator. --- factory/declarations.py | 3 ++- tests/test_declarations.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/factory/declarations.py b/factory/declarations.py index 2ed143e9..182c830c 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -183,7 +183,8 @@ def evaluate(self, instance, step, extra): def reset(self): """Reset the internal iterator.""" - self.iterator.reset() + if self.iterator is not None: + self.iterator.reset() class Sequence(BaseDeclaration): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 870df49d..003d836e 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -89,6 +89,10 @@ def test_no_cycling(self): self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) + def test_initial_reset(self): + it = declarations.Iterator([1, 2]) + it.reset() + def test_reset_cycle(self): it = declarations.Iterator([1, 2]) self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) From 6acf56b3dcc4d999ddecd1c92917b69af29f0d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 1 Jan 2018 20:00:14 +0100 Subject: [PATCH 290/714] [core] Reduce reliance on isinstance(). Instead of forcing declarations (both pre- and post-generation variants) to inherit from two specific classes, use a custom attribute, ``FACTORY_BUILDER_PHASE``. This brings a couple of advantages: - Provide for future expansion on the number of phases in the build process; - Allow declarations to adjust dynamically their phase; - Avoid relying on isinstance() - use duck-typing instead. --- factory/base.py | 11 +++-------- factory/builder.py | 4 ++-- factory/declarations.py | 4 ++++ factory/enums.py | 13 +++++++++++++ 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/factory/base.py b/factory/base.py index 6f8d1d67..ab89db38 100644 --- a/factory/base.py +++ b/factory/base.py @@ -220,7 +220,7 @@ def contribute_to_class(self, factory, meta=None, base_meta=None, base_factory=N self.parameters.update(parent._meta.parameters) for k, v in vars(self.factory).items(): - if self._is_declaration(k, v) or self._is_postgen_declaration(k, v): + if self._is_declaration(k, v): self.base_declarations[k] = v if params is not None: @@ -333,16 +333,11 @@ def _is_declaration(self, name, value): """ if isinstance(value, (classmethod, staticmethod)): return False - elif isinstance(value, declarations.BaseDeclaration): + elif enums.get_builder_phase(value): + # All objects with a defined 'builder phase' are declarations. return True - elif isinstance(value, declarations.PostGenerationDeclaration): - return False return not name.startswith("_") - def _is_postgen_declaration(self, name, value): - """Captures instances of PostGenerationDeclaration.""" - return isinstance(value, declarations.PostGenerationDeclaration) - def _check_parameter_dependencies(self, parameters): """Find out in what order parameters should be called.""" # Warning: parameters only provide reverse dependencies; we reverse them into standard dependencies. diff --git a/factory/builder.py b/factory/builder.py index 041c7990..e64ed5dd 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -164,7 +164,7 @@ def parse_declarations(decls, base_pre=None, base_post=None): extra_post = {} extra_maybenonpost = {} for k, v in decls.items(): - if isinstance(v, declarations.PostGenerationDeclaration): + if enums.get_builder_phase(v) == enums.BuilderPhase.POST_INSTANTIATION: if k in pre_declarations: # Conflict: PostGenerationDeclaration with the same # name as a BaseDeclaration @@ -354,7 +354,7 @@ def __getattr__(self, name): elif name in self.__declarations: declaration = self.__declarations[name] value = declaration.declaration - if isinstance(value, declarations.BaseDeclaration): + if enums.get_builder_phase(value) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: self.__pending.append(name) try: value = value.evaluate( diff --git a/factory/declarations.py b/factory/declarations.py index 182c830c..60745ad6 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -23,6 +23,8 @@ class BaseDeclaration(utils.OrderedBase): in the same factory. """ + FACTORY_BUILDER_PHASE = enums.BuilderPhase.ATTRIBUTE_RESOLUTION + def evaluate(self, instance, step, extra): """Evaluate this declaration. @@ -538,6 +540,8 @@ def __repr__(self): class PostGenerationDeclaration(utils.OrderedBase): """Declarations to be called once the model object has been generated.""" + FACTORY_BUILDER_PHASE = enums.BuilderPhase.POST_INSTANTIATION + def call(self, instance, step, context): # pragma: no cover """Call this hook; no return value is expected. diff --git a/factory/enums.py b/factory/enums.py index 8d5cab31..116e3152 100644 --- a/factory/enums.py +++ b/factory/enums.py @@ -10,3 +10,16 @@ #: String for splitting an attribute name into a #: (subfactory_name, subfactory_field) tuple. SPLITTER = '__' + + +# Target build phase, for declarations +class BuilderPhase: + #: During attribute resolution/computation + ATTRIBUTE_RESOLUTION = 'attributes' + + #: Once the target object has been built + POST_INSTANTIATION = 'post_instance' + + +def get_builder_phase(obj): + return getattr(obj, 'FACTORY_BUILDER_PHASE', None) From 45ff906cc3c41992fbe59e09341e00cba1ceecee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 22:38:13 +0100 Subject: [PATCH 291/714] declarations: Fix missing calls to super.__init__. --- factory/declarations.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/factory/declarations.py b/factory/declarations.py index 60745ad6..5309e7ff 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -434,6 +434,7 @@ def __bool__(self): class Maybe(BaseDeclaration): def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): + super(Maybe, self).__init__() self.decider = decider self.yes = yes_declaration self.no = no_declaration @@ -486,6 +487,7 @@ def get_revdeps(self, parameters): class SimpleParameter(Parameter): def __init__(self, value): + super(SimpleParameter, self).__init__() self.value = value def as_declarations(self, field_name, declarations): From 518403bf6310b741e6483dccd08bf3527540999a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 22:39:53 +0100 Subject: [PATCH 292/714] Properly test factory.Maybe. Note: this commit also introduces a simple dummy object to `test_using`, which should greatly improve readability of future tests. --- tests/test_using.py | 59 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index b2ef5030..190e8e6e 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -35,6 +35,23 @@ def as_dict(self): ) +class Dummy(object): + def __init__(self, **kwargs): + self._fields = set(kwargs) + for k, v in kwargs.items(): + setattr(self, k, v) + + @property + def as_dict(self): + return {field: getattr(self, field) for field in self._fields} + + def __repr__(self): + return '%s(%s)' % ( + self.__class__.__name__, + ', '.join('%s=%r' % pair for pair in sorted(self.as_dict.items())) + ) + + class FakeModel(object): @classmethod def create(cls, **kwargs): @@ -1099,6 +1116,48 @@ class Meta: self.assertEqual(42, obj.attributes) +class MaybeTestCase(unittest.TestCase): + def test_simple_maybe(self): + class DummyFactory(factory.Factory): + class Meta: + model = Dummy + + # Undeclared: key = None + both = factory.Maybe('key', 1, 2) + yes = factory.Maybe('key', 1) + no = factory.Maybe('key', no_declaration=2) + none = factory.Maybe('key') + + obj_default = DummyFactory.build() + obj_true = DummyFactory.build(key=True) + obj_false = DummyFactory.build(key=False) + + self.assertEqual(dict(both=2, no=2), obj_default.as_dict) + self.assertEqual(dict(key=True, both=1, yes=1), obj_true.as_dict) + self.assertEqual(dict(key=False, both=2, no=2), obj_false.as_dict) + + def test_declarations(self): + class DummyFactory(factory.Factory): + class Meta: + model = Dummy + + a = 0 + b = 1 + + # biggest = 'b' if .b > .a else 'a' + biggest = factory.Maybe(factory.LazyAttribute(lambda o: o.a < o.b), 'b', 'a') + # max_value = .b if .b > .a else .a = max(.a, .b) + max_value = factory.Maybe(factory.LazyAttribute(lambda o: o.a < o.b), factory.SelfAttribute('b'), factory.SelfAttribute('a')) + + obj_ordered = DummyFactory.build(a=1, b=2) + obj_equal = DummyFactory.build(a=3, b=3) + obj_reverse = DummyFactory.build(a=5, b=4) + + self.assertEqual(dict(a=1, b=2, biggest='b', max_value=2, ), obj_ordered.as_dict) + self.assertEqual(dict(a=3, b=3, biggest='a', max_value=3, ), obj_equal.as_dict) + self.assertEqual(dict(a=5, b=4, biggest='a', max_value=5, ), obj_reverse.as_dict) + + class TraitTestCase(unittest.TestCase): def test_traits(self): class TestObjectFactory(factory.Factory): From 33d27c219044d27658e3667e59e23ce033d9a653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 22:44:24 +0100 Subject: [PATCH 293/714] Support PostGeneration in Maybe() and Trait(). A Maybe or a Trait may now resolve to a PostGenerationDeclaration. However, for a Maybe, both options (yes/no) *must* be of the same kind (pre- or post- declaration). Closes #397. --- docs/changelog.rst | 9 ++++-- factory/declarations.py | 43 ++++++++++++++++++++++++---- tests/test_using.py | 63 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 8 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6bf99d58..b5d07789 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,13 @@ ChangeLog ========= -2.9.3 (unreleased) ------------------- +2.10.0 (unreleased) +------------------- + +*New:* -- Nothing changed yet. + - :issue:`397`: Allow a :class:`factory.Maybe` to contain a :class:`~factory.PostGenerationDeclaration`. + This also applies to :class:`factory.Trait`, since they use a :class:`factory.Maybe` declaration internally. .. _v2.9.2: diff --git a/factory/declarations.py b/factory/declarations.py index 5309e7ff..ed4cb191 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -435,15 +435,48 @@ def __bool__(self): class Maybe(BaseDeclaration): def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): super(Maybe, self).__init__() + phases = { + 'yes_declaration': enums.get_builder_phase(yes_declaration), + 'no_declaration': enums.get_builder_phase(no_declaration), + } + used_phases = set(phase for phase in phases.values() if phase is not None) + + if len(used_phases) > 1: + raise TypeError("Inconsistent phases for %r: %r" % (self, phases)) + + self.FACTORY_BUILDER_PHASE = used_phases.pop() if used_phases else enums.BuilderPhase.ATTRIBUTE_RESOLUTION + + if enums.get_builder_phase(decider) is None: + # No builder phase => flat value + decider = SelfAttribute(decider, default=None) + self.decider = decider self.yes = yes_declaration self.no = no_declaration - def evaluate(self, instance, step, extra): - if isinstance(self.decider, BaseDeclaration): - choice = self.decider.evaluate(instance=instance, step=step, extra={}) + def call(self, instance, step, context): + decider_phase = enums.get_builder_phase(self.decider) + if decider_phase == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: + # Note: we work on the *builder stub*, not on the actual instance. + # This gives us access to all Params-level definitions. + choice = self.decider.evaluate(instance=step.stub, step=step, extra=context.extra) + else: + assert decider_phase == enums.BuilderPhase.POST_INSTANTIATION + choice = self.decider.call(instance, step, context) + + target = self.yes if choice else self.no + if enums.get_builder_phase(target) == enums.BuilderPhase.POST_INSTANTIATION: + return target.call( + instance=instance, + step=step, + context=context, + ) else: - choice = getattr(instance, self.decider, None) + # Flat value (can't be ATTRIBUTE_RESOLUTION, checked in __init__) + return target + + def evaluate(self, instance, step, extra): + choice = self.decider.evaluate(instance=instance, step=step, extra={}) target = self.yes if choice else self.no if isinstance(target, BaseDeclaration): @@ -453,7 +486,7 @@ def evaluate(self, instance, step, extra): extra=extra, ) else: - # Flat value + # Flat value (can't be POST_INSTANTIATION, checked in __init__) return target def __repr__(self): diff --git a/tests/test_using.py b/tests/test_using.py index 190e8e6e..64b9acc6 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1157,6 +1157,47 @@ class Meta: self.assertEqual(dict(a=3, b=3, biggest='a', max_value=3, ), obj_equal.as_dict) self.assertEqual(dict(a=5, b=4, biggest='a', max_value=5, ), obj_reverse.as_dict) + def test_post_generation(self): + + # Helpers + @factory.post_generation + def square(obj, *args, **kwargs): + obj.value *= obj.value + + @factory.post_generation + def quintuple(obj, *args, **kwargs): + obj.value *= 5 + + @factory.post_generation + def double(obj, *args, **kwargs): + obj.value *= 2 + + @factory.post_generation + def decrement(obj, *args, **kwargs): + obj.value -= 1 + + + class DummyFactory(factory.Factory): + class Meta: + model = Dummy + + value = 0 + square_it = factory.Maybe('square', square) + quintuple_it = factory.Maybe('quintuple', quintuple) + adjust_nums = factory.Maybe( + factory.LazyAttribute(lambda o: o.value % 2 == 0), + double, + decrement, + ) + + obj_untouched = DummyFactory.build(value=4) + obj_squared = DummyFactory.build(value=5, square=True) + obj_combined = DummyFactory.build(value=6, square=True, quintuple=True) + + self.assertEqual(4 * 2, obj_untouched.value) + self.assertEqual(5 ** 2 - 1, obj_squared.value) + self.assertEqual(6 ** 2 * 5 * 2, obj_combined.value) + class TraitTestCase(unittest.TestCase): def test_traits(self): @@ -1188,6 +1229,28 @@ class Params: self.assertEqual(obj5.as_dict(), dict(one=True, two=True, three=True, four=None, five=True)) + def test_post_generation_traits(self): + @factory.post_generation + def compute(obj, _create, _value, power=2, **kwargs): + obj.value = obj.value ** power + + class DummyFactory(factory.Factory): + class Meta: + model = Dummy + + value = 3 + class Params: + exponentiate = factory.Trait(apply_exponent=compute) + + base = DummyFactory.build() + self.assertEqual(dict(value=3), base.as_dict) + + exp = DummyFactory.build(exponentiate=True) + self.assertEqual(dict(value=9), exp.as_dict) + + higher = DummyFactory.build(exponentiate=True, apply_exponent__power=4) + self.assertEqual(dict(value=81), higher.as_dict) + def test_traits_inheritance(self): """A trait can be set in an inherited class.""" class TestObjectFactory(factory.Factory): From 153d0872cdb5b5304b498b9bc3c2b88b4e2ec4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 22:55:48 +0100 Subject: [PATCH 294/714] Update ChangeLog for commit 61a9ecd; closes #443. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b5d07789..627c7b39 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ ChangeLog 2.10.0 (unreleased) ------------------- +*Bugfix:* + + - :issue:`443`: Don't crash when calling :meth:`factory.Iterator.reset()` on a brand new iterator. + *New:* - :issue:`397`: Allow a :class:`factory.Maybe` to contain a :class:`~factory.PostGenerationDeclaration`. From c5500656333036ed68d38434fca7cedd46bafba0 Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Fri, 1 Dec 2017 10:01:03 +0000 Subject: [PATCH 295/714] Improve mute_signals decorator documentation. Closes #436. --- docs/recipes.rst | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index a1289bac..2ffe39cb 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -95,10 +95,14 @@ using a :class:`~django.db.models.OneToOneField` from the ``Profile`` to the ``U 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``: +Prior to version 2.9, the solution to this was to override the :meth:`~factory.Factory._generate` method on the factory. + +Since version 2.9, the :meth:`~factory.django.mute_signals` decorator should be used: + .. code-block:: python + @factory.django.mute_signals(post_save) class ProfileFactory(factory.django.DjangoModelFactory): class Meta: model = my_models.Profile @@ -108,6 +112,7 @@ factory_boy allows to define attributes of such profiles dynamically when creati # (this disables the RelatedFactory) user = factory.SubFactory('app.factories.UserFactory', profile=None) + @factory.django.mute_signals(post_save) class UserFactory(factory.django.DjangoModelFactory): class Meta: model = auth_models.User @@ -118,16 +123,6 @@ factory_boy allows to define attributes of such profiles dynamically when creati # This will call ProfileFactory(user=our_new_user), thus skipping the SubFactory. profile = factory.RelatedFactory(ProfileFactory, 'user') - @classmethod - def _create(cls, model_class, *args, **kwargs): - """Override the default _create() 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(model_class, *args, **kwargs) - post_save.connect(handler_create_user_profile, auth_models.User) - return user - .. OHAI_VIM:* @@ -140,12 +135,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. +Any factories that call these classes with :class:`~factory.SubFactory` will also need to be decorated in the same manner. + .. 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. + Code working with signals should thus use the :meth:`~factory.django.mute_signals` decorator Simple Many-to-many relationship From be99c972ae2dc16bef9a304cf20d3804570dc34c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 23:17:41 +0100 Subject: [PATCH 296/714] testing: Update test matrix. - Test for latest Django, SQLAlchemy, MongoEngine - Test against latest pypy3, CPython nightly - Remove obsolete Django versions. --- .travis.yml | 20 +++++++++++++------- README.rst | 12 ++++++++++-- tox.ini | 18 ++++++++---------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0818559f..43308957 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,19 +11,25 @@ install: matrix: include: - python: "2.7" - env: TOXENV=py27-django111-alchemy10-mongoengine010 + env: TOXENV=py27-django111-alchemy12-mongoengine015 - python: "3.4" - env: TOXENV=py34-django111-alchemy10-mongoengine010 + env: TOXENV=py34-django20-alchemy12-mongoengine015 - python: "3.5" - env: TOXENV=py35-django111-alchemy10-mongoengine010 + env: TOXENV=py35-django20-alchemy12-mongoengine015 - python: "3.6" - env: TOXENV=py36-django111-alchemy10-mongoengine010 + env: TOXENV=py36-django20-alchemy12-mongoengine015 + - python: "nightly" + env: TOXENV=py37-django20-alchemy12-mongoengine015 + + # Django LTS + - python: "3.6" + env: TOXENV=py36-django111-alchemy12-mongoengine015 # Pypy - python: "pypy" - env: TOXENV=pypy27-django111-alchemy10-mongoengine010 - - python: "pypy3.5-5.8.0" - env: TOXENV=pypy3-django19-alchemy10-mongoengine010 + env: TOXENV=pypy27-django111-alchemy12-mongoengine015 + - python: "pypy3" + env: TOXENV=pypy3-django20-alchemy12-mongoengine015 # Linting - python: "3.6" diff --git a/README.rst b/README.rst index 9251e085..f5807f26 100644 --- a/README.rst +++ b/README.rst @@ -360,11 +360,19 @@ In order to test coverage, please use: $ make coverage -To test with a specific framework version, you may use: +To test with a specific framework version, you may use a ``tox`` target: .. code-block:: sh - $ make DJANGO=1.9 test + $ tox --listenvs + py27-django111-alchemy12-mongoengine015 + py27-django20-alchemy12-mongoengine015 + # ... + pypy3-django20-alchemy12-mongoengine015 + examples + lint + + $ tox -e py36-django20-alchemy12-mongoengine015 Valid options are: diff --git a/tox.ini b/tox.ini index 5d33a3da..09d8ece5 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = - py{27,34,35,36}-django{18,19,110,111}-alchemy10-mongoengine010, - pypy27-django{18,19,110,111}-alchemy10-mongoengine010, - pypy3-django{18,19}-alchemy10-mongoengine010, + py{27,34,35,36,37}-django{111,20}-alchemy12-mongoengine015, + pypy27-django{111}-alchemy12-mongoengine015, + pypy3-django{111,20}-alchemy12-mongoengine015, examples lint @@ -11,19 +11,17 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = -rrequirements_test.txt - django18: Django>=1.8,<1.9 - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 - django{18,19,110,111}: Pillow - alchemy10: SQLAlchemy>=1.0,<1.1 - mongoengine010: mongoengine>=0.10,<0.11 + django20: Django>=2.0,<2.1 + django{111,20}: Pillow + alchemy12: SQLAlchemy>=1.2,<1.3 + mongoengine015: mongoengine>=0.15,<0.16 whitelist_externals = make commands = make test [testenv:examples] -basepython = python3.4 +basepython = python3.6 deps = -rrequirements_test.txt -rexamples/requirements.txt From 6941ccc949bf8981cb41196b3ce582441a111306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 23:54:46 +0100 Subject: [PATCH 297/714] packaging: Fix trove classifier. There are no "Implementation :: Pypy :: X" classifiers (or they have been removed). --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 1e33b8d6..b007b681 100755 --- a/setup.py +++ b/setup.py @@ -69,8 +69,7 @@ def get_version(package_name): "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: Implementation :: PyPy :: 2.7", - "Programming Language :: Python :: Implementation :: PyPy :: 3.5", + "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", ], From f7377c0a9bcc6c81a1e7e3ba456b617a77b58a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 23:55:35 +0100 Subject: [PATCH 298/714] Preparing release 2.10.0 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 627c7b39..213e62ff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.10.0 (unreleased) +2.10.0 (2018-01-28) ------------------- *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index feab6733..47ae2eb0 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.9.3.dev0' +__version__ = '2.10.0' __author__ = 'Raphaël Barrois ' From 15a83d1669bef3a42491ffe1a7c846809df2ce9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 28 Jan 2018 23:56:04 +0100 Subject: [PATCH 299/714] Back to development: 2.10.1 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 213e62ff..3c04e5f1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.10.1 (unreleased) +------------------- + +- Nothing changed yet. + + 2.10.0 (2018-01-28) ------------------- diff --git a/factory/__init__.py b/factory/__init__.py index 47ae2eb0..8e06119b 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.10.0' +__version__ = '2.10.1.dev0' __author__ = 'Raphaël Barrois ' From 2d735767b7f3e1f9adfc3f14c28eeef7acbf6e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 11 Feb 2018 17:28:32 +0100 Subject: [PATCH 300/714] django: Remove some magic in FileField. Instead of inheriting and diverting `factory.Dict`, explicitly parse the provided parameters through a `DictFactory` call. Closes #451. --- docs/changelog.rst | 5 ++++- factory/django.py | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3c04e5f1..f10d6c11 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,10 @@ ChangeLog 2.10.1 (unreleased) ------------------- -- Nothing changed yet. +*Bugfix:* + + - :issue:`451`: Restore :class:`~factory.django.FileField` to a + :class:`~factory.declarations.ParameteredAttribute`, relying on composition to parse the provided parameters. 2.10.0 (2018-01-28) diff --git a/factory/django.py b/factory/django.py index 09f59206..15665375 100644 --- a/factory/django.py +++ b/factory/django.py @@ -172,14 +172,14 @@ def _after_postgeneration(cls, instance, create, results=None): instance.save() -class FileField(declarations.Dict): +class FileField(declarations.ParameteredAttribute): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' def __init__(self, **defaults): require_django() - super(FileField, self).__init__(defaults) + super(FileField, self).__init__(**defaults) def _make_data(self, params): """Create data for the field.""" @@ -224,7 +224,9 @@ def _make_content(self, params): def generate(self, step, params): """Fill in the field.""" - params = super(FileField, self).generate(step, params) + # Recurse into a DictFactory: allows users to have some params depending + # on others. + params = step.recurse(base.DictFactory, params, force_sequence=step.sequence) filename, content = self._make_content(params) return django_files.File(content.file, filename) From 80a56dd6d8b2421c47558700ad56c35b2335f2fd Mon Sep 17 00:00:00 2001 From: Kristian Uzhca Date: Wed, 7 Feb 2018 15:46:35 -0500 Subject: [PATCH 301/714] fix typo in generic_foreignkey --- examples/django_demo/generic_foreignkey/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/django_demo/generic_foreignkey/models.py b/examples/django_demo/generic_foreignkey/models.py index 33a8d481..f4e1afa7 100644 --- a/examples/django_demo/generic_foreignkey/models.py +++ b/examples/django_demo/generic_foreignkey/models.py @@ -6,7 +6,7 @@ class TaggedItem(models.Model): - """Example GemericForeinKey model from django docs""" + """Example GenericForeinKey model from django docs""" tag = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() From 52d9826f5463fc0b354236ecdfc482963f45cb1d Mon Sep 17 00:00:00 2001 From: wim glenn Date: Wed, 7 Feb 2018 14:25:27 -0600 Subject: [PATCH 302/714] seems to be not true since Faker is a dep --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index f5807f26..7c02cc38 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.python.org/pypi/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy 2.7 and 5.8; it requires only the standard Python library. +factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy 2.7 and 5.8. Download From 988d5801620dde0fd0c9eb25d1597b4cfd9d1a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 11 Feb 2018 19:29:25 +0100 Subject: [PATCH 303/714] Fix seeding of faker's random generator. Adjust faker's core generator, instead of iterating through instantiated fakers: * All Faker instances share the same random generator; * The previous code had no effect if no Faker instance had been configured when first resetting the random state. Closes #438. Closes #389. --- docs/changelog.rst | 1 + factory/faker.py | 1 + factory/random.py | 10 ++++------ tests/test_using.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f10d6c11..62889981 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ ChangeLog - :issue:`451`: Restore :class:`~factory.django.FileField` to a :class:`~factory.declarations.ParameteredAttribute`, relying on composition to parse the provided parameters. + - :issue:`389`: Fix random state management with ``faker``. 2.10.0 (2018-01-28) diff --git a/factory/faker.py b/factory/faker.py index 1d444809..4ca3e301 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -40,6 +40,7 @@ class Faker(declarations.BaseDeclaration): >>> foo = factory.Faker('name') """ def __init__(self, provider, locale=None, **kwargs): + super(Faker, self).__init__() self.provider = provider self.provider_kwargs = kwargs self.locale = locale diff --git a/factory/random.py b/factory/random.py index 75732fdd..8a6b0e88 100644 --- a/factory/random.py +++ b/factory/random.py @@ -1,9 +1,8 @@ from __future__ import absolute_import +import faker.generator import random -from factory.faker import Faker - randgen = random.Random() randgen.state_set = False @@ -17,7 +16,9 @@ def get_random_state(): def set_random_state(state): """Force-set the state of factory.fuzzy's random generator.""" randgen.state_set = True - return randgen.setstate(state) + randgen.setstate(state) + + faker.generator.random.setstate(state) def reseed_random(seed): @@ -25,6 +26,3 @@ def reseed_random(seed): r = random.Random(seed) random_internal_state = r.getstate() set_random_state(random_internal_state) - - for locale in Faker._FAKER_REGISTRY: - Faker._FAKER_REGISTRY[locale].random.setstate(random_internal_state) diff --git a/tests/test_using.py b/tests/test_using.py index 64b9acc6..929e8343 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2445,7 +2445,7 @@ class StudentFactory(factory.Factory): class Meta: model = TestObject - seed = "seed1" + seed = 1000 factory.random.reseed_random(seed) students_1 = (StudentFactory(), StudentFactory()) From 3799493885901b93e8d1a71976fc7cc603013780 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Fri, 13 Apr 2018 17:29:24 -0700 Subject: [PATCH 304/714] Use https:// for URLs where available --- README.rst | 4 ++-- docs/conf.py | 10 +++++----- docs/orms.rst | 2 +- tests/alter_time.py | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 7c02cc38..1b49f81e 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ factory_boy =========== .. image:: https://secure.travis-ci.org/FactoryBoy/factory_boy.svg?branch=master - :target: http://travis-ci.org/FactoryBoy/factory_boy/ + :target: https://travis-ci.org/FactoryBoy/factory_boy/ .. image:: https://img.shields.io/pypi/v/factory_boy.svg :target: https://factoryboy.readthedocs.io/en/latest/changelog.html @@ -342,7 +342,7 @@ Contributing factory_boy is distributed under the MIT License. -Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. +Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. Questions and suggestions are welcome on the `mailing-list `_. All pull request should pass the test suite, which can be launched simply with: diff --git a/docs/conf.py b/docs/conf.py index d9b98d6c..3fb4b732 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -245,13 +245,13 @@ def get_version(*module_dir_components): # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'http://docs.python.org/': None, + 'https://docs.python.org/': None, 'django': ( - 'http://docs.djangoproject.com/en/dev/', - 'http://docs.djangoproject.com/en/dev/_objects/', + 'https://docs.djangoproject.com/en/dev/', + 'https://docs.djangoproject.com/en/dev/_objects/', ), 'sqlalchemy': ( - 'http://docs.sqlalchemy.org/en/latest/', - 'http://docs.sqlalchemy.org/en/latest/objects.inv', + 'https://docs.sqlalchemy.org/en/latest/', + 'https://docs.sqlalchemy.org/en/latest/objects.inv', ), } diff --git a/docs/orms.rst b/docs/orms.rst index 7a09348a..833e8b7d 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -298,7 +298,7 @@ Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyMod To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:`Meta.sqlalchemy_session ` attribute. -.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org/ .. class:: SQLAlchemyModelFactory(factory.Factory) diff --git a/tests/alter_time.py b/tests/alter_time.py index aa2db3b9..824e4878 100644 --- a/tests/alter_time.py +++ b/tests/alter_time.py @@ -22,8 +22,8 @@ def mock_datetime_now(target, datetime_module): A mock.patch context, can be used as a decorator or in a with. """ - # See http://bugs.python.org/msg68532 - # And http://docs.python.org/reference/datamodel.html#customizing-instance-and-subclass-checks + # See https://bugs.python.org/msg68532 + # And https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks class DatetimeSubclassMeta(type): """We need to customize the __instancecheck__ method for isinstance(). @@ -59,8 +59,8 @@ def mock_date_today(target, datetime_module): A mock.patch context, can be used as a decorator or in a with. """ - # See http://bugs.python.org/msg68532 - # And http://docs.python.org/reference/datamodel.html#customizing-instance-and-subclass-checks + # See https://bugs.python.org/msg68532 + # And https://docs.python.org/3/reference/datamodel.html#customizing-instance-and-subclass-checks class DateSubclassMeta(type): """We need to customize the __instancecheck__ method for isinstance(). From 2c21bb00953bdc2e958b2f859cca76c0fd9dc952 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Thu, 19 Apr 2018 18:07:34 +0200 Subject: [PATCH 305/714] Improve FuzzyFloat precision to use freely with postgres PostreSQL's default float precision is 15 digits, where python's random return a 16 digit precision float. This means that when using a FuzzyFloat field in a factory, the returned object when calling the factory will have one more digit than the saved float in the data base. --- docs/changelog.rst | 1 + factory/fuzzy.py | 6 +++-- tests/test_fuzzy.py | 60 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 62889981..31328f2e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ ChangeLog *Bugfix:* + - Fix :class:`~factory.fuzzy.FuzzyFloat` to return a 15 decimal digits precision float by default - :issue:`451`: Restore :class:`~factory.django.FileField` to a :class:`~factory.declarations.ParameteredAttribute`, relying on composition to parse the provided parameters. - :issue:`389`: Fix random state management with ``faker``. diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 77f15613..dfd962c0 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -169,18 +169,20 @@ def fuzz(self): class FuzzyFloat(BaseFuzzyAttribute): """Random float within a given range.""" - def __init__(self, low, high=None, **kwargs): + def __init__(self, low, high=None, precision=15, **kwargs): if high is None: high = low low = 0 self.low = low self.high = high + self.precision = precision super(FuzzyFloat, self).__init__(**kwargs) def fuzz(self): - return random.randgen.uniform(self.low, self.high) + base = random.randgen.uniform(self.low, self.high) + return float(format(base, '.%dg' % self.precision)) class FuzzyDate(BaseFuzzyAttribute): diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 133e4ddd..0e07a056 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -186,6 +186,66 @@ def test_no_approximation(self): decimal_context.traps[decimal.FloatOperation] = old_traps +class FuzzyFloatTestCase(unittest.TestCase): + def test_definition(self): + """Tests all ways of defining a FuzzyFloat.""" + fuzz = fuzzy.FuzzyFloat(2.0, 3.0) + for _i in range(20): + res = utils.evaluate_declaration(fuzz) + self.assertTrue(2.0 <= res <= 3.0, "value %d is not between 2.0 and 3.0" % res) + + fuzz = fuzzy.FuzzyFloat(4.0) + for _i in range(20): + res = utils.evaluate_declaration(fuzz) + self.assertTrue(0.0 <= res <= 4.0, "value %d is not between 0.0 and 4.0" % res) + + fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5) + for _i in range(20): + res = utils.evaluate_declaration(fuzz) + self.assertTrue(1.0 <= res <= 4.0, "value %d is not between 1.0 and 4.0" % res) + self.assertTrue(res.as_tuple().exponent, -5) + + def test_biased(self): + fake_uniform = lambda low, high: low + high + + fuzz = fuzzy.FuzzyFloat(2.0, 8.0) + + with mock.patch('factory.random.randgen.uniform', fake_uniform): + res = utils.evaluate_declaration(fuzz) + + self.assertEqual(10.0, res) + + def test_biased_high_only(self): + fake_uniform = lambda low, high: low + high + + fuzz = fuzzy.FuzzyFloat(8.0) + + with mock.patch('factory.random.randgen.uniform', fake_uniform): + res = utils.evaluate_declaration(fuzz) + + self.assertEqual(8.0, res) + + def test_default_precision(self): + fake_uniform = lambda low, high: low + high + 0.000000000000011 + + fuzz = fuzzy.FuzzyFloat(8.0) + + with mock.patch('factory.random.randgen.uniform', fake_uniform): + res = utils.evaluate_declaration(fuzz) + + self.assertEqual(8.00000000000001, res) + + def test_precision(self): + fake_uniform = lambda low, high: low + high + 0.001 + + fuzz = fuzzy.FuzzyFloat(8.0, precision=4) + + with mock.patch('factory.random.randgen.uniform', fake_uniform): + res = utils.evaluate_declaration(fuzz) + + self.assertEqual(8.001, res) + + class FuzzyDateTestCase(unittest.TestCase): @classmethod def setUpClass(cls): From 7fdd83339509b63cbd76fdc2b34a7386d72c324b Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Fri, 20 Apr 2018 09:44:05 +0200 Subject: [PATCH 306/714] Fix FuzzyDecimal test --- tests/test_fuzzy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 0e07a056..56f278d7 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -138,8 +138,8 @@ def test_definition(self): fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5) for _i in range(20): res = utils.evaluate_declaration(fuzz) - self.assertTrue(decimal.Decimal('0.54') <= res <= decimal.Decimal('4.0'), - "value %d is not between 0.54 and 4.0" % res) + self.assertTrue(decimal.Decimal('1.0') <= res <= decimal.Decimal('4.0'), + "value %d is not between 1.0 and 4.0" % res) self.assertTrue(res.as_tuple().exponent, -5) def test_biased(self): From 12c0892a23579f286823b9daa64e4dc4cb0fd60d Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 18 Apr 2018 19:29:38 -0700 Subject: [PATCH 307/714] Update all pypi.python.org URLs to pypi.org For details on the new PyPI, see the blog post: https://pythoninsider.blogspot.ca/2018/04/new-pypi-launched-legacy-pypi-shutting.html --- README.rst | 12 ++++++------ docs/changelog.rst | 2 +- docs/reference.rst | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 1b49f81e..1e8c327c 100644 --- a/README.rst +++ b/README.rst @@ -9,15 +9,15 @@ factory_boy :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg - :target: https://pypi.python.org/pypi/factory_boy/ + :target: https://pypi.org/project/factory_boy/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/wheel/factory_boy.svg - :target: https://pypi.python.org/pypi/factory_boy/ + :target: https://pypi.org/project/factory_boy/ :alt: Wheel status .. image:: https://img.shields.io/pypi/l/factory_boy.svg - :target: https://pypi.python.org/pypi/factory_boy/ + :target: https://pypi.org/project/factory_boy/ :alt: License factory_boy is a fixtures replacement based on thoughtbot's `factory_bot `_. @@ -77,7 +77,7 @@ Links * Documentation: https://factoryboy.readthedocs.io/ * Repository: https://github.com/FactoryBoy/factory_boy -* Package: https://pypi.python.org/pypi/factory_boy/ +* Package: https://pypi.org/project/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy 2.7 and 5.8. @@ -86,7 +86,7 @@ factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy 2.7 and 5.8. Download -------- -PyPI: https://pypi.python.org/pypi/factory_boy/ +PyPI: https://pypi.org/project/factory_boy/ .. code-block:: sh @@ -187,7 +187,7 @@ Realistic, random values """""""""""""""""""""""" Demos look better with random yet realistic values; and those realistic values can also help discover bugs. -For this, factory_boy relies on the excellent `faker `_ library: +For this, factory_boy relies on the excellent `faker `_ library: .. code-block:: python diff --git a/docs/changelog.rst b/docs/changelog.rst index 31328f2e..58264e97 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -131,7 +131,7 @@ corner cases and weird behaviourrs. *New:* - Add :attr:`factory.FactoryOptions.rename` to help handle conflicting names (:issue:`206`) - - Add support for random-yet-realistic values through `fake-factory `_, + - Add support for random-yet-realistic values through `fake-factory `_, through the :class:`factory.Faker` class. - :class:`factory.Iterator` no longer begins iteration of its argument at import time, thus allowing to pass in a lazy iterator such as a Django queryset diff --git a/docs/reference.rst b/docs/reference.rst index b7c61082..2db1680e 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -675,7 +675,7 @@ Faker In order to easily define realistic-looking factories, use the :class:`Faker` attribute declaration. - This is a wrapper around `faker `_; + This is a wrapper around `faker `_; its argument is the name of a ``faker`` provider: .. code-block:: python From b40bec85439f8b45f338b848b118ee11aec52377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 4 May 2018 23:07:45 +0200 Subject: [PATCH 308/714] examples: Adjust to Flask>=1.0 extension nanespace Previous versions of Flask installed extensions under `flask.ext`; that is no longer the case. --- examples/flask_alchemy/demoapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py index 83b2e8e6..b49bfe84 100644 --- a/examples/flask_alchemy/demoapp.py +++ b/examples/flask_alchemy/demoapp.py @@ -2,7 +2,7 @@ # Copyright: See the LICENSE file. from flask import Flask -from flask.ext.sqlalchemy import SQLAlchemy +from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' From e06e16c6fb997ffeb9d3ff5bd47db86c5bf6a09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 4 May 2018 09:51:07 +0200 Subject: [PATCH 309/714] Add reproduction for issue #466. When combining a Trait and a post_generation declaration, the Trait's Maybe is not resolved before calling the post_generation declaration. --- tests/test_using.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 929e8343..96da0584 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -4,6 +4,7 @@ """Tests using factory.""" +import collections import datetime import functools import os @@ -1360,6 +1361,36 @@ class Params: self.assertEqual(1, wrapper.one) self.assertEqual(2, wrapper.two.one) + def test_traits_and_postgeneration(self): + """A trait parameter should be resolved before post_generation hooks. + + See https://github.com/FactoryBoy/factory_boy/issues/466. + """ + PRICES = {} + + Pizza = collections.namedtuple('Pizza', ['style', 'toppings']) + + class PizzaFactory(factory.Factory): + class Meta: + model = Pizza + + class Params: + fancy = factory.Trait( + toppings=['eggs', 'ham', 'extra_cheese'], + pricing__extra=10, + ) + + pricing__extra = 0 + toppings = ['tomato', 'cheese'] + style = 'margharita' + + @factory.post_generation + def pricing(self, create, extracted, base_price=5, extra=0, **kwargs): + PRICES[base_price + extra] = self + + p = PizzaFactory.build() + self.assertEqual({5: p}, PRICES) + class SubFactoryTestCase(unittest.TestCase): def test_sub_factory(self): From 8a960a4fa79b9155f80ca33fe4ac56981c9ee72a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 4 May 2018 15:28:57 +0200 Subject: [PATCH 310/714] Increase testing regarding issue #466. The problem is that declarations passed as kwargs to a post-generation hook aren't resolved, unless that declaration is a RelatedFactory. --- tests/test_using.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 96da0584..e650a936 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2248,6 +2248,25 @@ class Meta: self.assertEqual(2, obj.two) self.assertEqual(((), {'one': 2, 'two': 3}), obj.extra) + def test_post_generation_extraction_declaration(self): + LIBRARY = {} + + Book = collections.namedtuple('Book', ['author']) + + class BookFactory(factory.Factory): + class Meta: + model = Book + + author = factory.Faker('name') + register__reference = factory.Sequence(lambda n: n) + + @factory.post_generation + def register(self, create, extracted, reference=0, **kwargs): + LIBRARY[reference] = self + + book = BookFactory.build() + self.assertEqual({0: book}, LIBRARY) + def test_related_factory(self): class TestRelatedObject(object): def __init__(self, obj=None, one=None, two=None): From 3d502ff7d9503af6fad4d7ec252cde29052b4caf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 4 May 2018 22:43:47 +0200 Subject: [PATCH 311/714] Evaluate extra kwargs passed to declarations. Unroll a declaration's context before evaluating it (i.e evaluate all lazy declarations in that context before passing them as kwargs to the actual declaration's evaluate() / call() function). Note: this allows for the following code: class SomeFactory: value__low = factory.Sequence(lambda n: n) value = factory.fuzzy.FuzzyInteger(low=3, high=100) Whereas passing ``FuzzyInteger(low=factory.Sequence(lambda n: n))`` wouldn't work as expected. Whether that feature is supported or not has not been decided yet; it's considered to be an undefined behaviour and a side-effect of internal changes. Closes #466. --- docs/changelog.rst | 1 + factory/builder.py | 20 ++++++++++++++++---- factory/declarations.py | 13 ++++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 58264e97..95c00b92 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog - :issue:`451`: Restore :class:`~factory.django.FileField` to a :class:`~factory.declarations.ParameteredAttribute`, relying on composition to parse the provided parameters. - :issue:`389`: Fix random state management with ``faker``. + - :issue:`466`: Restore mixing :class:`~factory.Trait` and :meth:`~factory.post_generation`. 2.10.0 (2018-01-28) diff --git a/factory/builder.py b/factory/builder.py index e64ed5dd..189b7d63 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -282,10 +282,16 @@ def build(self, parent_step=None, force_sequence=None): postgen_results = {} for declaration_name in post.sorted(): declaration = post[declaration_name] + unrolled_context = declaration.declaration.unroll_context( + instance=instance, + step=step, + context=declaration.context, + ) + postgen_context = PostGenerationContext( - value_provided='' in declaration.context, - value=declaration.context.get(''), - extra={k: v for k, v in declaration.context.items() if k != ''}, + value_provided='' in unrolled_context, + value=unrolled_context.get(''), + extra={k: v for k, v in unrolled_context.items() if k != ''}, ) postgen_results[declaration_name] = declaration.declaration.call( instance=instance, @@ -357,10 +363,16 @@ def __getattr__(self, name): if enums.get_builder_phase(value) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: self.__pending.append(name) try: + context = value.unroll_context( + instance=self, + step=self.__step, + context=declaration.context, + ) + value = value.evaluate( instance=self, step=self.__step, - extra=declaration.context, + extra=context, ) finally: last = self.__pending.pop() diff --git a/factory/declarations.py b/factory/declarations.py index ed4cb191..e948ac57 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -25,6 +25,17 @@ class BaseDeclaration(utils.OrderedBase): FACTORY_BUILDER_PHASE = enums.BuilderPhase.ATTRIBUTE_RESOLUTION + def unroll_context(self, instance, step, context): + # XXX: This means that, for a SubFactory/RelatedFactory, we'll unroll + # its extra context *before* evaluating it. This might lead to some issues... + if not any(enums.get_builder_phase(v) for v in context.values()): + # Optimization for simple contexts - don't do anything. + return context + + import factory.base + subfactory = factory.base.DictFactory + return step.recurse(subfactory, context, force_sequence=step.sequence) + def evaluate(self, instance, step, extra): """Evaluate this declaration. @@ -572,7 +583,7 @@ def __repr__(self): # =============== -class PostGenerationDeclaration(utils.OrderedBase): +class PostGenerationDeclaration(BaseDeclaration): """Declarations to be called once the model object has been generated.""" FACTORY_BUILDER_PHASE = enums.BuilderPhase.POST_INSTANTIATION From 5b21e4984875eb9ba6fcf00ebe3dc331fb1e7a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 5 May 2018 00:12:32 +0200 Subject: [PATCH 312/714] Preparing release 2.11.0 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 95c00b92..7f19eba7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.10.1 (unreleased) +2.11.0 (2018-05-05) ------------------- *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index 8e06119b..cf509c57 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.10.1.dev0' +__version__ = '2.11.0' __author__ = 'Raphaël Barrois ' From 9d0eeb71e9f03ff1d7dc2e8fca49adecc273cf6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 5 May 2018 00:12:48 +0200 Subject: [PATCH 313/714] Back to development: 2.11.1 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7f19eba7..feea1efe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.11.1 (unreleased) +------------------- + +- Nothing changed yet. + + 2.11.0 (2018-05-05) ------------------- diff --git a/factory/__init__.py b/factory/__init__.py index cf509c57..bbd7b9d9 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.11.0' +__version__ = '2.11.1.dev0' __author__ = 'Raphaël Barrois ' From d6b65e9642aa045c9a1a57333c68d8864d0a236b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 5 May 2018 16:58:51 +0200 Subject: [PATCH 314/714] Restore overriding values deep in subfactories. A user should be able to call `SomeFactory(book__author__city=factory.Faker('city'))`, but that feature got broken in 2.11.0 - we'd try to unroll the values too early. --- docs/changelog.rst | 4 +++- factory/declarations.py | 11 +++++++++-- tests/test_using.py | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index feea1efe..f22b56ca 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,9 @@ ChangeLog 2.11.1 (unreleased) ------------------- -- Nothing changed yet. +*Bugfix:* + + - Fix passing deep context to a :class:`~factory.SubFactory` (``Foo(x__y__z=factory.Faker('name')``) 2.11.0 (2018-05-05) diff --git a/factory/declarations.py b/factory/declarations.py index e948ac57..fa18799b 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -25,9 +25,13 @@ class BaseDeclaration(utils.OrderedBase): FACTORY_BUILDER_PHASE = enums.BuilderPhase.ATTRIBUTE_RESOLUTION + #: Whether to unroll the context before evaluating the declaration. + #: Set to False on declarations that perform their own unrolling. + UNROLL_CONTEXT_BEFORE_EVALUATION = True + def unroll_context(self, instance, step, context): - # XXX: This means that, for a SubFactory/RelatedFactory, we'll unroll - # its extra context *before* evaluating it. This might lead to some issues... + if not self.UNROLL_CONTEXT_BEFORE_EVALUATION: + return context if not any(enums.get_builder_phase(v) for v in context.values()): # Optimization for simple contexts - don't do anything. return context @@ -381,6 +385,7 @@ class SubFactory(ParameteredAttribute): EXTEND_CONTAINERS = True FORCE_SEQUENCE = False + UNROLL_CONTEXT_BEFORE_EVALUATION = False def __init__(self, factory, **kwargs): super(SubFactory, self).__init__(**kwargs) @@ -631,6 +636,8 @@ class RelatedFactory(PostGenerationDeclaration): calling the related factory """ + UNROLL_CONTEXT_BEFORE_EVALUATION = False + def __init__(self, factory, factory_related_name='', **defaults): super(RelatedFactory, self).__init__() diff --git a/tests/test_using.py b/tests/test_using.py index e650a936..98acc52e 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1504,6 +1504,32 @@ class Meta: self.assertEqual(wrapping.wrapped.three, 3) self.assertEqual(wrapping.wrapped.four, 4) + def test_sub_factory_deep_overrides(self): + Author = collections.namedtuple('Author', ['name', 'country']) + Book = collections.namedtuple('Book', ['title', 'author']) + Chapter = collections.namedtuple('Chapter', ['book', 'number']) + + class AuthorFactory(factory.Factory): + class Meta: + model = Author + name = "John" + country = 'XX' + + class BookFactory(factory.Factory): + class Meta: + model = Book + title = "The mighty adventures of nobody." + author = factory.SubFactory(AuthorFactory) + + class ChapterFactory(factory.Factory): + class Meta: + model = Chapter + book = factory.SubFactory(BookFactory) + number = factory.Sequence(lambda n: n) + book__author__country = factory.LazyAttribute(lambda o: 'FR') + + c = ChapterFactory() + def test_nested_sub_factory(self): """Test nested sub-factories.""" From c31aa42002b2dab60c7a19af329f679aef00a55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 5 May 2018 17:05:30 +0200 Subject: [PATCH 315/714] Preparing release 2.11.1 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f22b56ca..afa5c861 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.11.1 (unreleased) +2.11.1 (2018-05-05) ------------------- *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index bbd7b9d9..10848265 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.11.1.dev0' +__version__ = '2.11.1' __author__ = 'Raphaël Barrois ' From 3c52fa206e9f91ac5d294b2c74fd80c22ae9c861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 5 May 2018 17:05:47 +0200 Subject: [PATCH 316/714] Back to development: 2.11.2 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index afa5c861..6e1c1d2f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.11.2 (unreleased) +------------------- + +- Nothing changed yet. + + 2.11.1 (2018-05-05) ------------------- diff --git a/factory/__init__.py b/factory/__init__.py index 10848265..801ab2b2 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -74,7 +74,7 @@ from . import mongoengine -__version__ = '2.11.1' +__version__ = '2.11.2.dev0' __author__ = 'Raphaël Barrois ' From 74f0e55ddeca9f3649c482289ef259619bbbb651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 29 Jul 2018 15:34:12 +0100 Subject: [PATCH 317/714] Add contribution guidelines. * Add a Code of Conduct, based on https://www.contributor-covenant.org/ * Add contributing instructions * Add Issue/Pull Request templates --- .github/ISSUE_TEMPLATE/bug_report.md | 26 +++++++ .../ISSUE_TEMPLATE/improvement-suggestion.md | 14 ++++ CODE_OF_CONDUCT.md | 46 +++++++++++ CONTRIBUTING.rst | 78 +++++++++++++++++++ MANIFEST.in | 3 +- 5 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/improvement-suggestion.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.rst diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..0d2fbb50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +#### Description +*A clear and concise description of what the bug is.* + +#### To Reproduce +*Share how the bug happened:* + +##### Model / Factory code +```python +# Include your factories and models here +``` + +##### The issue +*Add a short description along with your code* + +```python +# Include the code that provoked the bug, including as full a stack-trace as possible +``` + +#### Notes +*Add any notes you feel relevant here :)* diff --git a/.github/ISSUE_TEMPLATE/improvement-suggestion.md b/.github/ISSUE_TEMPLATE/improvement-suggestion.md new file mode 100644 index 00000000..cd109f64 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/improvement-suggestion.md @@ -0,0 +1,14 @@ +--- +name: Improvement suggestion +about: Suggest an idea for this project + +--- + +#### The problem +*Please describe the problem you're encountering (e.g "It's very complex to do [...]")* + +#### Proposed solution +*Please provide some wild idea you think could solve this issue. It's much easier to work from an existing suggestion :)* + +#### Extra notes +*Any notes you feel interesting to include: alternatives you've considered, reasons to include the change, anything!* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..d55199f3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at raphael DOT barrois AT xelmail DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 00000000..3f2b64d1 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,78 @@ +Contributing +============ + +Thanks for taking the time to contribute to factory_boy! + +Code of Conduct +--------------- + +This project and everyone participating in it is governed by the `Code of +Conduct`_. By participating, you are expected to uphold this code. Please +report inappropriate behavior to raphael DOT barrois AT xelmail DOT com. + +.. _Code of Conduct: https://github.com/FactoryBoy/factory_boy/blob/master/CODE_OF_CONDUCT.md + +*(If I'm the person with the inappropriate behavior, please accept my +apologies. I know I can mess up. I can't expect you to tell me, but if you +chose to do so, I'll do my best to handle criticism constructively. +-- Raphaël)* + +*(As the community around this project grows, we hope to have more core +developers available to handle that kind of issues)* + + +Contributions +------------- + +Bug reports, patches, documentation improvements and suggestions are welcome! + +Please open an issue_ or send a `pull request`_. + +Feedback about the documentation is especially valuable — the authors of +``factory_boy`` feel more confident about writing code than writing docs :-) + +.. _issue: https://github.com/FactoryBoy/factory_boy/issues/new +.. _pull request: https://github.com/FactoryBoy/factory_boy/compare/ + + +Where to start? +--------------- + +If you're new to the project and want to help, a great first step would be: + +* Fixing an issue in the docs (outdated setup instructions, missing information, + unclear feature, etc.); +* Working on an existing issue (some should be marked ``BeginnerFriendly``); +* Reviewing an existing pull request; +* Or any other way you'd like to help. + + +Code contributions +------------------ + +In order to merge some code, you'll need to open a `pull request`_. + +There are a few rules to keep in mind regarding pull requests: + +* A pull request should only solve a single issue / add a single feature; +* If the code change is significant, please also create an issue_ for easier discussion; +* We have automated testing; please make sure that the updated code passes automated checks; +* We're striving to improve the quality of the library, with higher test and docs coverage. + If you don't know how/where to add docs or tests, we'll be very happy to point you in the right + direction! + + +Questions +--------- + +GitHub issues aren't a good medium for handling questions. There are better +places to ask questions, for example Stack Overflow; please use the +``factory-boy`` tag to make those questions easy to find by the maintainers. + +If you want to ask a question anyway, please make sure that: + +- it's a question about ``factory_boy`` and not about ``Django`` or ``Faker``; +- it isn't answered by the documentation; +- it wasn't asked already. + +A good question can be written as a suggestion to improve the documentation. diff --git a/MANIFEST.in b/MANIFEST.in index 60209534..6ae50b7b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include ChangeLog CREDITS LICENSE README.rst +include ChangeLog CODE_OF_CONDUCT.md CONTRIBUTING.rst CREDITS LICENSE README.rst include requirements*.txt graft factory @@ -10,3 +10,4 @@ prune tests global-exclude *.py[cod] __pycache__ .*.sw[po] exclude Makefile tox.ini .flake8 +prune .github From d5db588d3b105f29cc0ee3529d9e63999e78a8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 29 Jul 2018 15:34:56 +0100 Subject: [PATCH 318/714] Adjust MANIFEST.in for downstream distribution. A downstream distribution will build from our ``sdist`` file; they'll need all test files for that. --- MANIFEST.in | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 6ae50b7b..d6b2c4f8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1,13 @@ include ChangeLog CODE_OF_CONDUCT.md CONTRIBUTING.rst CREDITS LICENSE README.rst include requirements*.txt +include Makefile tox.ini .flake8 graft factory -prune docs -prune examples -prune tests +graft docs +graft examples +graft tests global-exclude *.py[cod] __pycache__ .*.sw[po] - -exclude Makefile tox.ini .flake8 prune .github +prune docs/_build From c82b7778215d08d5cf7a1cb250cbf6a1e3c85fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 29 Jul 2018 16:07:27 +0100 Subject: [PATCH 319/714] Adjust the name used to select pypy2.7. It seems that a recent update to travis-ci has changed the name under which pypy2.7 is installed, resulting in a tox error. --- .travis.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 43308957..6c574b13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ matrix: # Pypy - python: "pypy" - env: TOXENV=pypy27-django111-alchemy12-mongoengine015 + env: TOXENV=pypy-django111-alchemy12-mongoengine015 - python: "pypy3" env: TOXENV=pypy3-django20-alchemy12-mongoengine015 diff --git a/tox.ini b/tox.ini index 09d8ece5..e3198056 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = py{27,34,35,36,37}-django{111,20}-alchemy12-mongoengine015, - pypy27-django{111}-alchemy12-mongoengine015, + pypy-django{111}-alchemy12-mongoengine015, pypy3-django{111,20}-alchemy12-mongoengine015, examples lint From e598a3713cffc9fa850c6044bf77fa8409a999e3 Mon Sep 17 00:00:00 2001 From: Emmanuelle Delescolle Date: Sun, 29 Jul 2018 16:59:06 +0200 Subject: [PATCH 320/714] Update fuzzy.rst --- docs/fuzzy.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index c6fa2808..bfc64ae7 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -75,6 +75,15 @@ FuzzyChoice This allows passing in, for instance, a Django queryset that will only hit the database during the database, not at import time. + + .. warning:: When using Python2 and list comprehension, use private variable + names as in: + + `[_x.name for _x in items]` + + instead of: + + `[x.name for x in items]` .. attribute:: choices From f8b74efcb20254fd403d516f07b509b4f58765c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 31 Jul 2018 16:12:10 +0200 Subject: [PATCH 321/714] Use subclasses of django TestCase for django tests Django's TestCase prevents tests from having side effects in the database. Without it, test suite execution may report failures depending on the tests ordering. More details available in the warning at the end of the writing tests section of Django's documentation: https://docs.djangoproject.com/en/2.0/topics/testing/overview/#writing-tests Related travis build: https://travis-ci.org/FactoryBoy/factory_boy/jobs/410329301 --- tests/test_django.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index e72a178e..f49354f6 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -444,7 +444,7 @@ def test_create_pointed_related_extra(self): self.assertEqual(pointed.pointer.bar, 'extra_new_bar') -class DjangoFileFieldTestCase(unittest.TestCase): +class DjangoFileFieldTestCase(django_test.TestCase): def tearDown(self): super(DjangoFileFieldTestCase, self).tearDown() @@ -595,7 +595,7 @@ class Params: self.assertEqual('bar', o.pointed.foo) -class DjangoFakerTestCase(unittest.TestCase): +class DjangoFakerTestCase(django_test.TestCase): def test_random(self): class StandardModelFactory(factory.django.DjangoModelFactory): class Meta: @@ -608,7 +608,7 @@ class Meta: @unittest.skipIf(Image is None, "PIL not installed.") -class DjangoImageFieldTestCase(unittest.TestCase): +class DjangoImageFieldTestCase(django_test.TestCase): def tearDown(self): super(DjangoImageFieldTestCase, self).tearDown() @@ -769,7 +769,7 @@ def test_with_func(self): self.assertEqual(32, i.height) -class PreventSignalsTestCase(unittest.TestCase): +class PreventSignalsTestCase(django_test.TestCase): def setUp(self): self.handlers = mock.MagicMock() @@ -889,7 +889,7 @@ def generate(cls): self.assertSignalsReactivated() -class DjangoCustomManagerTestCase(unittest.TestCase): +class DjangoCustomManagerTestCase(django_test.TestCase): def test_extra_args(self): # Our CustomManager will remove the 'arg=' argument. From 239bdeac12fba5b0abb3599be3165d91343923ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 31 Jul 2018 15:19:32 +0200 Subject: [PATCH 322/714] Simplify test discovery Use `unittest` test discovery feature instead of making star-imports in tests/__init__.py. It avoids explicitly listing all test cases and allows linting tools like flake8 to identify unknown names. More reasons not to use star imports can be found on this stack overflow answer https://stackoverflow.com/a/2386740. Unittest discover feature is described in https://docs.python.org/3.7/library/unittest.html#unittest-test-discovery. --- Makefile | 2 +- setup.py | 2 +- tests/__init__.py | 16 ---------------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 678bc14e..96a740ba 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ testall: # DOC: Run tests for the currently installed version test: - python -Wdefault -m unittest $(TESTS_DIR) + python -Wdefault -m unittest # DOC: Test the examples example-test: diff --git a/setup.py b/setup.py index b007b681..5847d06c 100755 --- a/setup.py +++ b/setup.py @@ -73,6 +73,6 @@ def get_version(package_name): "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", ], - test_suite='tests', + test_suite='', test_loader=test_loader, ) diff --git a/tests/__init__.py b/tests/__init__.py index f90d2d27..e69de29b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: See the LICENSE file. - -# factory.django needs a configured Django. -from .test_django import * - -from .test_base import * -from .test_declarations import * -from .test_docs_internals import * -from .test_faker import * -from .test_fuzzy import * -from .test_helpers import * -from .test_using import * -from .test_utils import * -from .test_alchemy import * -from .test_mongoengine import * From 2f687f617c5792a280d2f463106ee8a1d5673046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 31 Jul 2018 15:30:44 +0200 Subject: [PATCH 323/714] Lint setup.py `setup.py` is a Python script, linting it may reveal mistakes that could otherwise go unnoticed. --- Makefile | 5 +++-- setup.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 96a740ba..d39ddd3e 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ PACKAGE=factory TESTS_DIR=tests DOC_DIR=docs EXAMPLES_DIR=examples +SETUP_PY=setup.py # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) @@ -61,13 +62,13 @@ example-test: # Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude # DOC: Perform code quality tasks lint: - $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) + $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) $(SETUP_PY) $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py check-manifest coverage: $(COVERAGE) erase - $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch setup.py test + $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch $(SETUP_PY) test $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" $(COVERAGE) html "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" diff --git a/setup.py b/setup.py index 5847d06c..c89465a9 100755 --- a/setup.py +++ b/setup.py @@ -54,7 +54,7 @@ def get_version(package_name): 'setuptools>=0.8', ], tests_require=[ - #'mock', + # 'mock', ], classifiers=[ "Development Status :: 5 - Production/Stable", From 868313dad3d17a56eb7a0ba7782763ef56fd9176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 9 Aug 2018 21:52:02 +0200 Subject: [PATCH 324/714] Upgrade deprecated assertion assertRegexpMatches In Python 3.2, the assertRegexpMatches assertion has been renamed to assertRegex. assertRegexpMatches is deprecated. Follow upstream convention and use assertRegex. Example output: tests/test_django.py:749: DeprecationWarning: Please use assertRegex instead. --- tests/test_django.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index f49354f6..c0e1b07e 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -546,7 +546,7 @@ def test_existing_file(self): self.assertEqual(b'example_data\n', o2.afile.read()) self.assertNotEqual('django/example.data', o2.afile.name) - self.assertRegexpMatches(o2.afile.name, r'django/example_\w+.data') + self.assertRegex(o2.afile.name, r'django/example_\w+.data') def test_no_file(self): o = WithFileFactory.build(afile=None) @@ -746,7 +746,7 @@ def test_existing_file(self): # Image file for a 42x42 green jpeg: 301 bytes long. self.assertEqual(301, len(o2.animage.read())) self.assertNotEqual('django/example.jpeg', o2.animage.name) - self.assertRegexpMatches(o2.animage.name, r'django/example_\w+.jpeg') + self.assertRegex(o2.animage.name, r'django/example_\w+.jpeg') def test_no_file(self): o = WithImageFactory.build(animage=None) From 6e2390f496d50b7f5b2a983c54c880ae4fe8e4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 9 Aug 2018 21:40:05 +0200 Subject: [PATCH 325/714] Fix own UserWarning in test suite The following message was printed to stderr: tests/test_using.py:2516: UserWarning: Setting a specific random seed for FuzzyDate can still have varying results unless you also set a specific end date. For details and potential solutions see https://github.com/FactoryBoy/factory_boy/issues/331 --- tests/test_using.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_using.py b/tests/test_using.py index 98acc52e..416ba31b 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2513,7 +2513,7 @@ def test_example(self): class RepeatableRandomSeedFakerTests(unittest.TestCase): def test_same_seed_is_used_between_fuzzy_and_faker_generators(self): class StudentFactory(factory.Factory): - one = factory.fuzzy.FuzzyDate(datetime.date(1950, 1, 1), ) + one = factory.fuzzy.FuzzyDecimal(4.0) two = factory.Faker('name') three = factory.Faker('name', locale='it') four = factory.Faker('name') From 88c92462f9b88bdfe83ebd73e78624ea7424f55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 31 Jul 2018 14:47:02 +0200 Subject: [PATCH 326/714] Lint test files Following the same standard as the project and the broader Python community makes maintenance easier. Linting can also reveal real mistakes, such as `test_auto_sequence` being shadowed. --- .flake8 | 7 +- CREDITS | 1 + Makefile | 2 +- tests/alchemyapp/models.py | 1 + tests/alter_time.py | 4 +- tests/compat.py | 19 ++-- tests/cyclic/bar.py | 2 +- tests/cyclic/foo.py | 1 + tests/cyclic/self_ref.py | 1 + tests/djapp/models.py | 1 + tests/test_base.py | 7 +- tests/test_declarations.py | 7 +- tests/test_django.py | 49 +++++----- tests/test_docs_internals.py | 5 +- tests/test_fuzzy.py | 95 +++++++++++++----- tests/test_helpers.py | 9 +- tests/test_mongoengine.py | 4 + tests/test_using.py | 185 +++++++++++++++++++++++------------ tests/test_utils.py | 103 ++++++++++++------- tests/utils.py | 4 +- 20 files changed, 325 insertions(+), 182 deletions(-) diff --git a/.flake8 b/.flake8 index eceea153..d2583294 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,7 @@ [flake8] -# Ignore "and" at start of line. -ignore = W503 +ignore = + # Ignore "and" at start of line. + W503 + # Ignore "do not assign a lambda expression, use a def". + E731 max-line-length = 120 diff --git a/CREDITS b/CREDITS index 07be3f58..ef4011f0 100644 --- a/CREDITS +++ b/CREDITS @@ -36,6 +36,7 @@ The project has received contributions from (in alphabetical order): * Demur Nodia (https://github.com/demonno) * Eduard Iskandarov * Flavio Curella +* François Freitag * George Hickman * Hervé Cauwelier * Ilya Baryshev diff --git a/Makefile b/Makefile index d39ddd3e..457e9a2a 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ example-test: # Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude # DOC: Perform code quality tasks lint: - $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) $(SETUP_PY) + $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py check-manifest diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 88708fd5..c6e1838a 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -26,4 +26,5 @@ class NonIntegerPk(Base): id = Column(Unicode(20), primary_key=True) + Base.metadata.create_all(engine) diff --git a/tests/alter_time.py b/tests/alter_time.py index 824e4878..d08e79f6 100644 --- a/tests/alter_time.py +++ b/tests/alter_time.py @@ -12,6 +12,7 @@ real_datetime_class = datetime.datetime + def mock_datetime_now(target, datetime_module): """Override ``datetime.datetime.now()`` with a custom target value. @@ -48,8 +49,10 @@ def utcnow(cls): return mock.patch.object(datetime_module, 'datetime', MockedDatetime) + real_date_class = datetime.date + def mock_date_today(target, datetime_module): """Override ``datetime.date.today()`` with a custom target value. @@ -98,7 +101,6 @@ def main(): # pragma: no cover print("- isinstance(now, date) ->", isinstance(datetime.date.today(), datetime.date)) print("- isinstance(target, date) ->", isinstance(target_date, datetime.date)) - print("Outside mock") print("- now ->", datetime.datetime.now()) print("- isinstance(now, dt) ->", isinstance(datetime.datetime.now(), datetime.datetime)) diff --git a/tests/compat.py b/tests/compat.py index 364d0513..78098aff 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -7,18 +7,17 @@ is_python2 = (sys.version_info[0] == 2) -if sys.version_info[0:2] < (2, 7): # pragma: no cover +if sys.version_info[0:2] < (2, 7): import unittest2 as unittest -else: # pragma: no cover - import unittest +else: + import unittest # noqa: F401 -if sys.version_info[0] == 2: # pragma: no cover +if is_python2: import StringIO as io -else: # pragma: no cover - import io +else: + import io # noqa: F401 -if sys.version_info[0:2] < (3, 3): # pragma: no cover +if sys.version_info[0:2] < (3, 3): import mock -else: # pragma: no cover - from unittest import mock - +else: + from unittest import mock # noqa: F401 diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index 4cd7c79d..bbc420ed 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -5,6 +5,7 @@ import factory + class Bar(object): def __init__(self, foo, y): self.foo = foo @@ -17,4 +18,3 @@ class Meta: y = 13 foo = factory.SubFactory('cyclic.foo.FooFactory') - diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index a9e7c958..8c0a92a9 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -7,6 +7,7 @@ from . import bar as bar_mod + class Foo(object): def __init__(self, bar, x): self.bar = bar diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py index 0384bcc2..bf757eb2 100644 --- a/tests/cyclic/self_ref.py +++ b/tests/cyclic/self_ref.py @@ -5,6 +5,7 @@ import factory + class TreeElement(object): def __init__(self, name, parent): self.parent = parent diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 20cdb8c0..7691284b 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -77,6 +77,7 @@ class WithDefaultValue(models.Model): WITHFILE_UPLOAD_TO = 'django' WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO) + class WithFile(models.Model): afile = models.FileField(upload_to=WITHFILE_UPLOAD_TO) diff --git a/tests/test_base.py b/tests/test_base.py index 5e5c27b4..615ebe5b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -import warnings - from factory import base from factory import declarations from factory import enums @@ -10,6 +8,7 @@ from .compat import unittest + class TestObject(object): def __init__(self, one=None, two=None, three=None, four=None): self.one = one @@ -245,7 +244,7 @@ class TestObjectFactory(base.Factory): class Meta: model = TestObject - one = declarations.LazyAttribute(lambda a: a.does_not_exist ) + one = declarations.LazyAttribute(lambda a: a.does_not_exist) self.assertRaises(AttributeError, TestObjectFactory) @@ -349,7 +348,6 @@ class SubTestObjectFactory(self.TestObjectFactory): self.assertEqual(1, o4.one) - class FactoryDefaultStrategyTestCase(unittest.TestCase): def setUp(self): self.default_strategy = base.Factory._meta.strategy @@ -537,6 +535,5 @@ class Meta: self.assertIn('foo__bar', TestObjectFactory._meta.post_declarations.as_dict()) - if __name__ == '__main__': # pragma: no cover unittest.main() diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 003d836e..3a6561e7 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -2,7 +2,6 @@ # Copyright: See the LICENSE file. import datetime -import itertools from factory import base from factory import declarations @@ -125,6 +124,7 @@ def test_getter(self): class PostGenerationDeclarationTestCase(unittest.TestCase): def test_post_generation(self): call_params = [] + def foo(*args, **kwargs): call_params.append(args) call_params.append(kwargs) @@ -143,6 +143,7 @@ def foo(*args, **kwargs): def test_decorator_simple(self): call_params = [] + @helpers.post_generation def foo(*args, **kwargs): call_params.append(args) @@ -215,7 +216,7 @@ def test_simplest_setup_and_call(self): def test_call_with_method_args(self): obj = self.build( - declarations.PostGenerationMethodCall( 'method', 'data'), + declarations.PostGenerationMethodCall('method', 'data'), ) obj.method.assert_called_once_with('data') @@ -255,7 +256,7 @@ def test_call_with_passed_kwargs(self): def test_multi_call_with_multi_method_args(self): with self.assertRaises(errors.InvalidDeclarationError): - obj = self.build( + self.build( declarations.PostGenerationMethodCall('method', 'arg1', 'arg2'), ) diff --git a/tests/test_django.py b/tests/test_django.py index c0e1b07e..1182d961 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -4,24 +4,19 @@ """Tests for factory_boy/Django interactions.""" import os -from .compat import is_python2, unittest, mock - import django # Setup Django as soon as possible os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') - django.setup() -from django import test as django_test -from django.conf import settings -from django.db import models as django_models -from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner -from django.test import utils as django_test_utils -from django.db.models import signals -from .djapp import models - +from django import test as django_test # noqa: E402 +from django.conf import settings # noqa: E402 +from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner # noqa: E402 +from django.test import utils as django_test_utils # noqa: E402 +from django.db.models import signals # noqa: E402 +from .djapp import models # noqa: E402 try: from PIL import Image except ImportError: # pragma: no cover @@ -32,12 +27,12 @@ # OK, not installed Image = None +import factory # noqa: E402 +import factory.django # noqa: E402 +from factory.compat import BytesIO # noqa: E402 -import factory -import factory.django -from factory.compat import BytesIO - -from . import testdata +from . import testdata # noqa: E402 +from .compat import unittest, mock # noqa: E402 test_state = {} @@ -201,7 +196,7 @@ class DjangoGetOrCreateTests(django_test.TestCase): def test_simple_call(self): obj1 = MultifieldModelFactory(slug='slug1') obj2 = MultifieldModelFactory(slug='slug1') - obj3 = MultifieldModelFactory(slug='alt') + MultifieldModelFactory(slug='alt') self.assertEqual(obj1, obj2) self.assertEqual(2, models.MultifieldModel.objects.count()) @@ -211,7 +206,8 @@ def test_missing_arg(self): MultifieldModelFactory() def test_multicall(self): - objs = MultifieldModelFactory.create_batch(6, + objs = MultifieldModelFactory.create_batch( + 6, slug=factory.Iterator(['main', 'alt']), ) self.assertEqual(6, len(objs)) @@ -340,12 +336,12 @@ def test_force_pk(self): class DjangoAbstractBaseSequenceTestCase(django_test.TestCase): - def test_auto_sequence(self): + def test_auto_sequence_son(self): """The sequence of the concrete son of an abstract model should be autonomous.""" obj = ConcreteSonFactory() self.assertEqual(1, obj.pk) - def test_auto_sequence(self): + def test_auto_sequence_grandson(self): """The sequence of the concrete grandson of an abstract model should be autonomous.""" obj = ConcreteGrandSonFactory() self.assertEqual(1, obj.pk) @@ -372,6 +368,7 @@ class DjangoRelatedFieldTestCase(django_test.TestCase): @classmethod def setUpClass(cls): super(DjangoRelatedFieldTestCase, cls).setUpClass() + class PointedFactory(factory.django.DjangoModelFactory): class Meta: model = models.PointedModel @@ -518,7 +515,9 @@ def test_with_path_empty_file(self): self.assertEqual('django/example.data', o.afile.name) def test_error_both_file_and_path(self): - self.assertRaises(ValueError, WithFileFactory.build, + self.assertRaises( + ValueError, + WithFileFactory.build, afile__from_file='fakefile', afile__from_path=testdata.TESTFILE_PATH, ) @@ -718,7 +717,9 @@ def test_with_path_empty_file(self): self.assertEqual('django/example.jpeg', o.animage.name) def test_error_both_file_and_path(self): - self.assertRaises(ValueError, WithImageFactory.build, + self.assertRaises( + ValueError, + WithImageFactory.build, animage__from_file='fakefile', animage__from_path=testdata.TESTIMAGE_PATH, ) @@ -754,7 +755,7 @@ def test_no_file(self): self.assertFalse(o.animage) def _img_test_func(self): - img = Image.new('RGB', (32,32), 'blue') + img = Image.new('RGB', (32, 32), 'blue') img_io = BytesIO() img.save(img_io, format='JPEG') img_io.seek(0) @@ -893,7 +894,7 @@ class DjangoCustomManagerTestCase(django_test.TestCase): def test_extra_args(self): # Our CustomManager will remove the 'arg=' argument. - model = WithCustomManagerFactory(arg='foo') + WithCustomManagerFactory(arg='foo') def test_with_manager_on_abstract(self): class ObjFactory(factory.django.DjangoModelFactory): diff --git a/tests/test_docs_internals.py b/tests/test_docs_internals.py index b329cae1..ee88eda2 100644 --- a/tests/test_docs_internals.py +++ b/tests/test_docs_internals.py @@ -29,9 +29,9 @@ from .compat import unittest - class User(object): - def __init__(self, + def __init__( + self, username, full_name, is_active=True, @@ -103,7 +103,6 @@ class Params: 'enabled', None, factory.fuzzy.FuzzyDateTime( -# factory.SelfAttribute('creation_date'), datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=10), datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=1), ), diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 56f278d7..2aa39618 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -126,20 +126,26 @@ def test_definition(self): fuzz = fuzzy.FuzzyDecimal(2.0, 3.0) for _i in range(20): res = utils.evaluate_declaration(fuzz) - self.assertTrue(decimal.Decimal('2.0') <= res <= decimal.Decimal('3.0'), - "value %d is not between 2.0 and 3.0" % res) + self.assertTrue( + decimal.Decimal('2.0') <= res <= decimal.Decimal('3.0'), + "value %d is not between 2.0 and 3.0" % res, + ) fuzz = fuzzy.FuzzyDecimal(4.0) for _i in range(20): res = utils.evaluate_declaration(fuzz) - self.assertTrue(decimal.Decimal('0.0') <= res <= decimal.Decimal('4.0'), - "value %d is not between 0.0 and 4.0" % res) + self.assertTrue( + decimal.Decimal('0.0') <= res <= decimal.Decimal('4.0'), + "value %d is not between 0.0 and 4.0" % res, + ) fuzz = fuzzy.FuzzyDecimal(1.0, 4.0, precision=5) for _i in range(20): res = utils.evaluate_declaration(fuzz) - self.assertTrue(decimal.Decimal('1.0') <= res <= decimal.Decimal('4.0'), - "value %d is not between 1.0 and 4.0" % res) + self.assertTrue( + decimal.Decimal('1.0') <= res <= decimal.Decimal('4.0'), + "value %d is not between 1.0 and 4.0" % res, + ) self.assertTrue(res.as_tuple().exponent, -5) def test_biased(self): @@ -274,13 +280,20 @@ def test_partial_definition(self): self.assertLessEqual(res, self.jan3) def test_invalid_definition(self): - self.assertRaises(ValueError, fuzzy.FuzzyDate, - self.jan31, self.jan1) + self.assertRaises( + ValueError, + fuzzy.FuzzyDate, + 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.assertRaises( + ValueError, + fuzzy.FuzzyDate, + self.jan31, + ) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -334,13 +347,21 @@ def test_partial_definition(self): 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.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)) + 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) @@ -392,13 +413,20 @@ def test_force_microsecond(self): self.assertEqual(4, res.microsecond) def test_invalid_definition(self): - self.assertRaises(ValueError, fuzzy.FuzzyNaiveDateTime, - self.jan31, self.jan1) + 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) + self.assertRaises( + ValueError, + fuzzy.FuzzyNaiveDateTime, + self.jan31, + ) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -451,23 +479,38 @@ def test_partial_definition(self): self.assertLessEqual(res, self.jan3) def test_invalid_definition(self): - self.assertRaises(ValueError, fuzzy.FuzzyDateTime, - self.jan31, self.jan1) + 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) + 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) + 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)) + 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) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b81613d8..6d787ae7 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -15,18 +15,18 @@ def test_default_logger(self): stream1 = io.StringIO() stream2 = io.StringIO() - l = logging.getLogger('factory.test') + logger = logging.getLogger('factory.test') h = logging.StreamHandler(stream1) h.setLevel(logging.INFO) - l.addHandler(h) + logger.addHandler(h) # Non-debug: no text gets out - l.debug("Test") + logger.debug("Test") self.assertEqual('', stream1.getvalue()) with helpers.debug(stream=stream2): # Debug: text goes to new stream only - l.debug("Test2") + logger.debug("Test2") self.assertEqual('', stream1.getvalue()) self.assertEqual("Test2\n", stream2.getvalue()) @@ -54,4 +54,3 @@ def test_alternate_logger(self): self.assertEqual("", stream1.getvalue()) self.assertEqual("Test2\n", stream2.getvalue()) - diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index b1db6cbe..2a24c61e 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -12,19 +12,23 @@ from factory.mongoengine import MongoEngineFactory + class Address(mongoengine.EmbeddedDocument): street = mongoengine.StringField() + class Person(mongoengine.Document): name = mongoengine.StringField() address = mongoengine.EmbeddedDocumentField(Address) + class AddressFactory(MongoEngineFactory): class Meta: model = Address street = factory.Sequence(lambda n: 'street%d' % n) + class PersonFactory(MongoEngineFactory): class Meta: model = Person diff --git a/tests/test_using.py b/tests/test_using.py index 416ba31b..7b784b04 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -6,10 +6,8 @@ import collections import datetime -import functools import os import sys -import warnings import factory from factory import errors @@ -123,8 +121,12 @@ def test_complex(self): self.assertEqual(obj.four, None) def test_build_batch(self): - objs = factory.build_batch(TestObject, 4, two=2, - three=factory.LazyAttribute(lambda o: o.two + 1)) + objs = factory.build_batch( + TestObject, + 4, + two=2, + three=factory.LazyAttribute(lambda o: o.two + 1), + ) self.assertEqual(4, len(objs)) self.assertEqual(4, len(set(objs))) @@ -156,8 +158,12 @@ def test_create_batch(self): self.assertEqual(obj.foo, 'bar') def test_create_batch_custom_base(self): - objs = factory.create_batch(FakeModel, 4, foo='bar', - FACTORY_CLASS=factory.django.DjangoModelFactory) + objs = factory.create_batch( + FakeModel, + 4, + foo='bar', + FACTORY_CLASS=factory.django.DjangoModelFactory, + ) self.assertEqual(4, len(objs)) self.assertEqual(4, len(set(objs))) @@ -192,8 +198,12 @@ def test_generate_create(self): self.assertEqual(obj.foo, 'bar') def test_generate_create_custom_base(self): - obj = factory.generate(FakeModel, factory.CREATE_STRATEGY, foo='bar', - FACTORY_CLASS=factory.django.DjangoModelFactory) + obj = factory.generate( + FakeModel, + factory.CREATE_STRATEGY, + foo='bar', + FACTORY_CLASS=factory.django.DjangoModelFactory, + ) self.assertEqual(obj.id, 2) self.assertEqual(obj.foo, 'bar') @@ -223,8 +233,13 @@ def test_generate_batch_create(self): self.assertEqual(obj.foo, 'bar') def test_generate_batch_create_custom_base(self): - objs = factory.generate_batch(FakeModel, factory.CREATE_STRATEGY, 20, foo='bar', - FACTORY_CLASS=factory.django.DjangoModelFactory) + objs = factory.generate_batch( + FakeModel, + factory.CREATE_STRATEGY, + 20, + foo='bar', + FACTORY_CLASS=factory.django.DjangoModelFactory, + ) self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) @@ -279,8 +294,13 @@ def test_simple_generate_batch_create(self): self.assertEqual(obj.foo, 'bar') def test_simple_generate_batch_create_custom_base(self): - objs = factory.simple_generate_batch(FakeModel, True, 20, foo='bar', - FACTORY_CLASS=factory.django.DjangoModelFactory) + objs = factory.simple_generate_batch( + FakeModel, + True, + 20, + foo='bar', + FACTORY_CLASS=factory.django.DjangoModelFactory, + ) self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) @@ -463,7 +483,7 @@ class TestObjectFactory(factory.Factory): class Meta: model = TestObject - one = factory.LazyAttribute(lambda a: 'abc' ) + one = factory.LazyAttribute(lambda a: 'abc') two = factory.LazyAttribute(lambda a: a.one + ' xyz') test_object = TestObjectFactory.build() @@ -557,6 +577,7 @@ class Meta: @factory.lazy_attribute_sequence def one(a, n): return 'one%d' % n + @factory.lazy_attribute_sequence def two(a, n): return a.one + ' two%d' % n @@ -759,8 +780,10 @@ class Meta: two = factory.LazyAttribute(lambda a: a.one + ' two') three = factory.Sequence(lambda n: int(n)) - objs = TestObjectFactory.stub_batch(20, - one=factory.Sequence(lambda n: str(n))) + objs = TestObjectFactory.stub_batch( + 20, + one=factory.Sequence(lambda n: str(n)), + ) self.assertEqual(20, len(objs)) self.assertEqual(20, len(set(objs))) @@ -902,7 +925,6 @@ class Meta: to2b = TestObjectFactory2() self.assertEqual(1, to2b.one) - def test_inheritance_with_inherited_class(self): class TestObjectFactory(factory.Factory): class Meta: @@ -1027,7 +1049,6 @@ class Meta: self.assertEqual({'t': 4}, obj.kwargs) - class NonKwargParametersTestCase(unittest.TestCase): def test_build(self): class TestObject(object): @@ -1148,7 +1169,11 @@ class Meta: # biggest = 'b' if .b > .a else 'a' biggest = factory.Maybe(factory.LazyAttribute(lambda o: o.a < o.b), 'b', 'a') # max_value = .b if .b > .a else .a = max(.a, .b) - max_value = factory.Maybe(factory.LazyAttribute(lambda o: o.a < o.b), factory.SelfAttribute('b'), factory.SelfAttribute('a')) + max_value = factory.Maybe( + factory.LazyAttribute(lambda o: o.a < o.b), + factory.SelfAttribute('b'), + factory.SelfAttribute('a'), + ) obj_ordered = DummyFactory.build(a=1, b=2) obj_equal = DummyFactory.build(a=3, b=3) @@ -1177,7 +1202,6 @@ def double(obj, *args, **kwargs): def decrement(obj, *args, **kwargs): obj.value -= 1 - class DummyFactory(factory.Factory): class Meta: model = Dummy @@ -1211,24 +1235,34 @@ class Params: odd = factory.Trait(one=True, three=True, five=True) obj1 = TestObjectFactory() - self.assertEqual(obj1.as_dict(), - dict(one=None, two=None, three=None, four=None, five=None)) + self.assertEqual( + obj1.as_dict(), + dict(one=None, two=None, three=None, four=None, five=None), + ) obj2 = TestObjectFactory(even=True) - self.assertEqual(obj2.as_dict(), - dict(one=None, two=True, three=None, four=True, five=None)) + self.assertEqual( + obj2.as_dict(), + dict(one=None, two=True, three=None, four=True, five=None), + ) obj3 = TestObjectFactory(odd=True) - self.assertEqual(obj3.as_dict(), - dict(one=True, two=None, three=True, four=None, five=True)) + self.assertEqual( + obj3.as_dict(), + dict(one=True, two=None, three=True, four=None, five=True), + ) obj4 = TestObjectFactory(even=True, odd=True) - self.assertEqual(obj4.as_dict(), - dict(one=True, two=True, three=True, four=True, five=True)) + self.assertEqual( + obj4.as_dict(), + dict(one=True, two=True, three=True, four=True, five=True), + ) obj5 = TestObjectFactory(odd=True, two=True) - self.assertEqual(obj5.as_dict(), - dict(one=True, two=True, three=True, four=None, five=True)) + self.assertEqual( + obj5.as_dict(), + dict(one=True, two=True, three=True, four=None, five=True), + ) def test_post_generation_traits(self): @factory.post_generation @@ -1240,6 +1274,7 @@ class Meta: model = Dummy value = 3 + class Params: exponentiate = factory.Trait(apply_exponent=compute) @@ -1267,13 +1302,17 @@ class EvenObjectFactory(TestObjectFactory): # Simple call obj1 = EvenObjectFactory() - self.assertEqual(obj1.as_dict(), - dict(one=None, two=True, three=None, four=True, five=None)) + self.assertEqual( + obj1.as_dict(), + dict(one=None, two=True, three=None, four=True, five=None), + ) # Force-disable it obj2 = EvenObjectFactory(even=False) - self.assertEqual(obj2.as_dict(), - dict(one=None, two=None, three=None, four=None, five=None)) + self.assertEqual( + obj2.as_dict(), + dict(one=None, two=None, three=None, four=None, five=None), + ) def test_traits_override(self): """Override a trait in a subclass.""" @@ -1291,8 +1330,10 @@ class Params: even = factory.Trait(two=True, four=True, one=True) obj = WeirdMathFactory(even=True) - self.assertEqual(obj.as_dict(), - dict(one=True, two=True, three=None, four=True, five=None)) + self.assertEqual( + obj.as_dict(), + dict(one=True, two=True, three=None, four=True, five=None), + ) def test_traits_chaining(self): """Use a trait to enable other traits.""" @@ -1308,26 +1349,36 @@ class Params: # Setting "full" should enable all fields. obj = TestObjectFactory(full=True) - self.assertEqual(obj.as_dict(), - dict(one=True, two=True, three=True, four=True, five=True)) + self.assertEqual( + obj.as_dict(), + dict(one=True, two=True, three=True, four=True, five=True), + ) # Does it break usual patterns? obj1 = TestObjectFactory() - self.assertEqual(obj1.as_dict(), - dict(one=None, two=None, three=None, four=None, five=None)) + self.assertEqual( + obj1.as_dict(), + dict(one=None, two=None, three=None, four=None, five=None), + ) obj2 = TestObjectFactory(even=True) - self.assertEqual(obj2.as_dict(), - dict(one=None, two=True, three=None, four=True, five=None)) + self.assertEqual( + obj2.as_dict(), + dict(one=None, two=True, three=None, four=True, five=None), + ) obj3 = TestObjectFactory(odd=True) - self.assertEqual(obj3.as_dict(), - dict(one=True, two=None, three=True, four=None, five=True)) + self.assertEqual( + obj3.as_dict(), + dict(one=True, two=None, three=True, four=None, five=True), + ) # Setting override should override two and set it to False obj = TestObjectFactory(override=True) - self.assertEqual(obj.as_dict(), - dict(one=None, two=False, three=None, four=True, five=None)) + self.assertEqual( + obj.as_dict(), + dict(one=None, two=False, three=None, four=True, five=None), + ) def test_prevent_cyclic_traits(self): @@ -1423,7 +1474,8 @@ class Meta: class TestModel2Factory(FakeModelFactory): class Meta: model = TestModel2 - two = factory.SubFactory(TestModelFactory, + two = factory.SubFactory( + TestModelFactory, one=factory.Sequence(lambda n: 'x%dx' % n), two=factory.LazyAttribute(lambda o: '%s%s' % (o.one, o.one)), ) @@ -1445,7 +1497,8 @@ class TestModel2Factory(FakeModelFactory): class Meta: model = TestModel2 one = 'parent' - child = factory.SubFactory(TestModelFactory, + child = factory.SubFactory( + TestModelFactory, one=factory.LazyAttribute(lambda o: '%s child' % o.factory_parent.one), ) @@ -1485,7 +1538,6 @@ class TestObjectFactory(factory.Factory): class Meta: model = TestObject - class OtherTestObject(object): def __init__(self, **kwargs): for k, v in kwargs.items(): @@ -1528,7 +1580,8 @@ class Meta: number = factory.Sequence(lambda n: n) book__author__country = factory.LazyAttribute(lambda o: 'FR') - c = ChapterFactory() + chapter = ChapterFactory() + self.assertEquals('FR', chapter.book.author.country) def test_nested_sub_factory(self): """Test nested sub-factories.""" @@ -1583,8 +1636,10 @@ class OuterWrappingTestObjectFactory(factory.Factory): class Meta: model = TestObject - wrap = factory.SubFactory(WrappingTestObjectFactory, - wrapped__two=factory.SubFactory(TestObjectFactory, four=4)) + wrap = factory.SubFactory( + WrappingTestObjectFactory, + wrapped__two=factory.SubFactory(TestObjectFactory, four=4), + ) outer = OuterWrappingTestObjectFactory.build() self.assertEqual(outer.wrap.wrapped.two.four, 4) @@ -1734,10 +1789,18 @@ class Meta: model = OuterMost foo = 30 - side_a = factory.SubFactory(SideAFactory, - inner_from_a__a=factory.ContainerAttribute(lambda obj, containers: containers[1].foo * 2)) - side_b = factory.SubFactory(SideBFactory, - inner_from_b=factory.ContainerAttribute(lambda obj, containers: containers[0].side_a.inner_from_a)) + side_a = factory.SubFactory( + SideAFactory, + inner_from_a__a=factory.ContainerAttribute( + lambda obj, containers: containers[1].foo * 2, + ) + ) + side_b = factory.SubFactory( + SideBFactory, + inner_from_b=factory.ContainerAttribute( + lambda obj, containers: containers[0].side_a.inner_from_a, + ) + ) outer = OuterMostFactory.build() self.assertEqual(outer.foo, 30) @@ -1745,7 +1808,7 @@ class Meta: self.assertEqual(outer.side_a.inner_from_a.a, outer.foo * 2) self.assertEqual(outer.side_a.inner_from_a.b, 20) - outer = OuterMostFactory.build(side_a__inner_from_a__b = 4) + outer = OuterMostFactory.build(side_a__inner_from_a__b=4) self.assertEqual(outer.foo, 30) self.assertEqual(outer.side_a.inner_from_a, outer.side_b.inner_from_b) self.assertEqual(outer.side_a.inner_from_a.a, outer.foo * 2) @@ -2211,7 +2274,7 @@ class Meta: bar = factory.PostGeneration(my_lambda) - obj = TestObjectFactory.build(bar=42, bar__foo=13) + TestObjectFactory.build(bar=42, bar__foo=13) def test_post_generation_override_with_extra(self): class TestObjectFactory(factory.Factory): @@ -2244,10 +2307,7 @@ class Params: obj = OtherTestObjectFactory.build() self.assertEqual(1 + 1 * 4, obj.one) - def test_post_generation_method_call(self): - calls = [] - class TestObject(object): def __init__(self, one=None, two=None): self.one = one @@ -2341,6 +2401,7 @@ class Meta: def test_related_factory_no_name(self): relateds = [] + class TestRelatedObject(object): def __init__(self, obj=None, one=None, two=None): relateds.append(self) @@ -2404,7 +2465,9 @@ class Meta: model = TestObject one = 3 two = 2 - three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj', + three = factory.RelatedFactory( + TestRelatedObjectFactory, + 'obj', two=factory.SelfAttribute('obj.two'), ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 05187fa1..a4045574 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -34,38 +34,50 @@ def test_one_key(self): def test_one_key_excluded(self): d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual({}, - utils.extract_dict('foo', d, pop=False, exclude=('foo__baz',))) + self.assertEqual( + {}, + utils.extract_dict('foo', d, pop=False, exclude=('foo__baz',)), + ) self.assertEqual(42, d['foo__baz']) - self.assertEqual({}, - utils.extract_dict('foo', d, pop=True, exclude=('foo__baz',))) + self.assertEqual( + {}, + utils.extract_dict('foo', d, pop=True, exclude=('foo__baz',)), + ) self.assertIn('foo__baz', d) def test_many_keys(self): d = {'foo': 13, 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, '__foo': 1} - self.assertEqual({'foo__bar': 2, 'bar': 3, 'baz': 42}, - utils.extract_dict('foo', d, pop=False)) + self.assertEqual( + {'foo__bar': 2, 'bar': 3, 'baz': 42}, + utils.extract_dict('foo', d, pop=False), + ) self.assertEqual(42, d['foo__baz']) self.assertEqual(3, d['foo__bar']) self.assertEqual(2, d['foo__foo__bar']) - self.assertEqual({'foo__bar': 2, 'bar': 3, 'baz': 42}, - utils.extract_dict('foo', d, pop=True)) + self.assertEqual( + {'foo__bar': 2, 'bar': 3, 'baz': 42}, + utils.extract_dict('foo', d, pop=True), + ) self.assertNotIn('foo__baz', d) self.assertNotIn('foo__bar', d) self.assertNotIn('foo__foo__bar', d) def test_many_keys_excluded(self): d = {'foo': 13, 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, '__foo': 1} - self.assertEqual({'foo__bar': 2, 'baz': 42}, - utils.extract_dict('foo', d, pop=False, exclude=('foo__bar', 'bar'))) + self.assertEqual( + {'foo__bar': 2, 'baz': 42}, + utils.extract_dict('foo', d, pop=False, exclude=('foo__bar', 'bar')), + ) self.assertEqual(42, d['foo__baz']) self.assertEqual(3, d['foo__bar']) self.assertEqual(2, d['foo__foo__bar']) - self.assertEqual({'foo__bar': 2, 'baz': 42}, - utils.extract_dict('foo', d, pop=True, exclude=('foo__bar', 'bar'))) + self.assertEqual( + {'foo__bar': 2, 'baz': 42}, + utils.extract_dict('foo', d, pop=True, exclude=('foo__bar', 'bar')), + ) self.assertNotIn('foo__baz', d) self.assertIn('foo__bar', d) self.assertNotIn('foo__foo__bar', d) @@ -76,55 +88,75 @@ def test_empty_dict(self): self.assertEqual({'foo': {}}, utils.multi_extract_dict(['foo'], {})) def test_unused_key(self): - self.assertEqual({'foo': {}}, - utils.multi_extract_dict(['foo'], {'bar__baz': 42})) - self.assertEqual({'foo': {}, 'baz': {}}, - utils.multi_extract_dict(['foo', 'baz'], {'bar__baz': 42})) + self.assertEqual( + {'foo': {}}, + utils.multi_extract_dict(['foo'], {'bar__baz': 42}), + ) + self.assertEqual( + {'foo': {}, 'baz': {}}, + utils.multi_extract_dict(['foo', 'baz'], {'bar__baz': 42}), + ) def test_no_key(self): self.assertEqual({}, utils.multi_extract_dict([], {'bar__baz': 42})) def test_empty_key(self): - self.assertEqual({'': {}}, - utils.multi_extract_dict([''], {'foo': 13, 'bar__baz': 42})) + self.assertEqual( + {'': {}}, + utils.multi_extract_dict([''], {'foo': 13, 'bar__baz': 42}), + ) d = {'foo': 13, 'bar__baz': 42, '__foo': 1} - self.assertEqual({'': {'foo': 1}}, - utils.multi_extract_dict([''], d)) + self.assertEqual( + {'': {'foo': 1}}, + utils.multi_extract_dict([''], d), + ) self.assertNotIn('__foo', d) def test_one_extracted(self): d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual({'foo': {'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=False)) + self.assertEqual( + {'foo': {'baz': 42}}, + utils.multi_extract_dict(['foo'], d, pop=False), + ) self.assertEqual(42, d['foo__baz']) - self.assertEqual({'foo': {'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=True)) + self.assertEqual( + {'foo': {'baz': 42}}, + utils.multi_extract_dict(['foo'], d, pop=True), + ) self.assertNotIn('foo__baz', d) def test_many_extracted(self): d = {'foo': 13, 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, '__foo': 1} - self.assertEqual({'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=False)) + self.assertEqual( + {'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}}, + utils.multi_extract_dict(['foo'], d, pop=False), + ) self.assertEqual(42, d['foo__baz']) self.assertEqual(3, d['foo__bar']) self.assertEqual(2, d['foo__foo__bar']) - self.assertEqual({'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=True)) + self.assertEqual( + {'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}}, + utils.multi_extract_dict(['foo'], d, pop=True), + ) self.assertNotIn('foo__baz', d) self.assertNotIn('foo__bar', d) self.assertNotIn('foo__foo__bar', d) def test_many_keys_one_extracted(self): d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual({'foo': {'baz': 42}, 'baz': {}}, - utils.multi_extract_dict(['foo', 'baz'], d, pop=False)) + self.assertEqual( + {'foo': {'baz': 42}, 'baz': {}}, + utils.multi_extract_dict(['foo', 'baz'], d, pop=False), + ) self.assertEqual(42, d['foo__baz']) - self.assertEqual({'foo': {'baz': 42}, 'baz': {}}, - utils.multi_extract_dict(['foo', 'baz'], d, pop=True)) + self.assertEqual( + {'foo': {'baz': 42}, 'baz': {}}, + utils.multi_extract_dict(['foo', 'baz'], d, pop=True), + ) self.assertNotIn('foo__baz', d) def test_many_keys_many_extracted(self): @@ -209,12 +241,10 @@ def test_datetime(self): self.assertEqual(d, imported) def test_unknown_attribute(self): - self.assertRaises(AttributeError, utils.import_object, - 'datetime', 'foo') + self.assertRaises(AttributeError, utils.import_object, 'datetime', 'foo') def test_invalid_module(self): - self.assertRaises(ImportError, utils.import_object, - 'this-is-an-invalid-module', '__name__') + self.assertRaises(ImportError, utils.import_object, 'this-is-an-invalid-module', '__name__') class LogPPrintTestCase(unittest.TestCase): @@ -365,4 +395,3 @@ def test_reset_shorter(self): self.assertEqual(2, next(iterator)) self.assertEqual(3, next(iterator)) self.assertEqual(4, next(iterator)) - diff --git a/tests/utils.py b/tests/utils.py index 18f35dc3..31ae519f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -import datetime import functools import warnings import factory -from .compat import mock from . import alter_time @@ -33,7 +31,7 @@ def _build_patcher(self, target_module): # pragma: no cover def __enter__(self): for patcher in self.patchers: - mocked_symbol = patcher.start() + patcher.start() def __exit__(self, exc_type=None, exc_val=None, exc_tb=None): for patcher in self.patchers: From 6336767b85cbac11c8e7cdb387697ab47eacbafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 9 Aug 2018 22:07:14 +0200 Subject: [PATCH 327/714] Don't test Django's presence in tests Django is a test requirement, there are import at the top of the test_django.py file, there's not need to test if django is defined in the factories. --- tests/test_django.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 1182d961..2bc9b03f 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -114,16 +114,14 @@ class WithFileFactory(factory.django.DjangoModelFactory): class Meta: model = models.WithFile - if django is not None: - afile = factory.django.FileField() + afile = factory.django.FileField() class WithImageFactory(factory.django.DjangoModelFactory): class Meta: model = models.WithImage - if django is not None: - animage = factory.django.ImageField() + animage = factory.django.ImageField() class WithSignalsFactory(factory.django.DjangoModelFactory): From 969a765ed5b3c50d00f451fb80e750eecc8f571e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Sep 2018 14:08:16 -0700 Subject: [PATCH 328/714] Remove unused workaround for Python 2.6's unittest2 Python 2.6 hasn't been supported since at least 5071655e34ba7d51e42454a0fa0463bc4d7c9e92. --- setup.py | 9 +-------- tests/compat.py | 5 ----- tests/test_alchemy.py | 2 +- tests/test_base.py | 4 ++-- tests/test_declarations.py | 3 ++- tests/test_django.py | 3 ++- tests/test_docs_internals.py | 3 +-- tests/test_fuzzy.py | 3 ++- tests/test_helpers.py | 3 ++- tests/test_mongoengine.py | 4 ++-- tests/test_using.py | 3 ++- tests/test_utils.py | 4 ++-- 12 files changed, 19 insertions(+), 27 deletions(-) diff --git a/setup.py b/setup.py index c89465a9..4291c806 100755 --- a/setup.py +++ b/setup.py @@ -4,7 +4,6 @@ import codecs import os import re -import sys from setuptools import setup @@ -23,12 +22,6 @@ def get_version(package_name): return '0.1.0' -if sys.version_info[0:2] < (2, 7): # pragma: no cover - test_loader = 'unittest2:TestLoader' -else: - test_loader = 'unittest:TestLoader' - - PACKAGE = 'factory' @@ -74,5 +67,5 @@ def get_version(package_name): "Topic :: Software Development :: Libraries :: Python Modules", ], test_suite='', - test_loader=test_loader, + test_loader='unittest:TestLoader', ) diff --git a/tests/compat.py b/tests/compat.py index 78098aff..87066eeb 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -7,11 +7,6 @@ is_python2 = (sys.version_info[0] == 2) -if sys.version_info[0:2] < (2, 7): - import unittest2 as unittest -else: - import unittest # noqa: F401 - if is_python2: import StringIO as io else: diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index be1a222a..edfa1104 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -4,9 +4,9 @@ """Tests for factory_boy/SQLAlchemy interactions.""" import factory -from .compat import unittest from .compat import mock import warnings +import unittest from factory.alchemy import SQLAlchemyModelFactory from .alchemyapp import models diff --git a/tests/test_base.py b/tests/test_base.py index 615ebe5b..963aaa37 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- # Copyright: See the LICENSE file. +import unittest + from factory import base from factory import declarations from factory import enums from factory import errors -from .compat import unittest - class TestObject(object): def __init__(self, one=None, two=None, three=None, four=None): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 3a6561e7..66c9955d 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -2,13 +2,14 @@ # Copyright: See the LICENSE file. import datetime +import unittest from factory import base from factory import declarations from factory import errors from factory import helpers -from .compat import mock, unittest +from .compat import mock from . import utils diff --git a/tests/test_django.py b/tests/test_django.py index 2bc9b03f..7c8828b2 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -4,6 +4,7 @@ """Tests for factory_boy/Django interactions.""" import os +import unittest import django @@ -32,7 +33,7 @@ from factory.compat import BytesIO # noqa: E402 from . import testdata # noqa: E402 -from .compat import unittest, mock # noqa: E402 +from .compat import mock # noqa: E402 test_state = {} diff --git a/tests/test_docs_internals.py b/tests/test_docs_internals.py index ee88eda2..82a3e4e6 100644 --- a/tests/test_docs_internals.py +++ b/tests/test_docs_internals.py @@ -21,13 +21,12 @@ """Tests for the docs/internals module.""" import datetime +import unittest import factory import factory.fuzzy from factory.compat import UTC -from .compat import unittest - class User(object): def __init__( diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 2aa39618..65426f20 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -4,13 +4,14 @@ import datetime import decimal +import unittest import warnings from factory import compat from factory import fuzzy from factory import random -from .compat import mock, unittest +from .compat import mock from . import utils diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 6d787ae7..c2a4f5f1 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -2,10 +2,11 @@ # Copyright: See the LICENSE file. import logging +import unittest from factory import helpers -from .compat import io, unittest +from .compat import io class DebugTest(unittest.TestCase): diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 2a24c61e..d4333838 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -3,10 +3,10 @@ """Tests for factory_boy/MongoEngine interactions.""" +import unittest + import factory import os -from .compat import unittest - import mongoengine diff --git a/tests/test_using.py b/tests/test_using.py index 7b784b04..08de02bc 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -8,11 +8,12 @@ import datetime import os import sys +import unittest import factory from factory import errors -from .compat import is_python2, unittest +from .compat import is_python2 from . import utils diff --git a/tests/test_utils.py b/tests/test_utils.py index a4045574..665fed78 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,12 +3,12 @@ from __future__ import unicode_literals - import itertools +import unittest from factory import utils -from .compat import is_python2, unittest +from .compat import is_python2 class ExtractDictTestCase(unittest.TestCase): From 34236d329b766623e657173898f749712c082da5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 13 Sep 2018 19:56:40 -0700 Subject: [PATCH 329/714] Clean up "ResourceWarning: unclosed file" warnings during tests When running tests with the command: python -Wa -m unittest Warnings are emitted of the form: ResourceWarning: unclosed file ... Explicitly close these files using a with statement. Refs #365 --- tests/test_django.py | 88 ++++++++++++++++++++++++++------------------ 1 file changed, 53 insertions(+), 35 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 7c8828b2..766a3616 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -460,7 +460,8 @@ def test_default_build(self): def test_default_create(self): o = WithFileFactory.create() self.assertIsNotNone(o.pk) - self.assertEqual(b'', o.afile.read()) + with o.afile as f: + self.assertEqual(b'', f.read()) self.assertEqual('django/example.dat', o.afile.name) def test_with_content(self): @@ -469,7 +470,8 @@ def test_with_content(self): # Django only allocates the full path on save() o.save() - self.assertEqual(b'foo', o.afile.read()) + with o.afile as f: + self.assertEqual(b'foo', f.read()) self.assertEqual('django/example.dat', o.afile.name) def test_with_file(self): @@ -477,16 +479,19 @@ def test_with_file(self): o = WithFileFactory.build(afile__from_file=f) o.save() - self.assertEqual(b'example_data\n', o.afile.read()) + with o.afile as f: + self.assertEqual(b'example_data\n', f.read()) self.assertEqual('django/example.data', o.afile.name) def test_with_path(self): o = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH) self.assertIsNone(o.pk) - # Django only allocates the full path on save() - o.save() - self.assertEqual(b'example_data\n', o.afile.read()) + with o.afile as f: + # Django only allocates the full path on save() + o.save() + f.seek(0) + self.assertEqual(b'example_data\n', f.read()) self.assertEqual('django/example.data', o.afile.name) def test_with_file_empty_path(self): @@ -498,7 +503,8 @@ def test_with_file_empty_path(self): # Django only allocates the full path on save() o.save() - self.assertEqual(b'example_data\n', o.afile.read()) + with o.afile as f: + self.assertEqual(b'example_data\n', f.read()) self.assertEqual('django/example.data', o.afile.name) def test_with_path_empty_file(self): @@ -508,9 +514,11 @@ def test_with_path_empty_file(self): ) self.assertIsNone(o.pk) - # Django only allocates the full path on save() - o.save() - self.assertEqual(b'example_data\n', o.afile.read()) + with o.afile as f: + # Django only allocates the full path on save() + o.save() + f.seek(0) + self.assertEqual(b'example_data\n', f.read()) self.assertEqual('django/example.data', o.afile.name) def test_error_both_file_and_path(self): @@ -528,21 +536,25 @@ def test_override_filename_with_path(self): ) self.assertIsNone(o.pk) - # Django only allocates the full path on save() - o.save() - self.assertEqual(b'example_data\n', o.afile.read()) + with o.afile as f: + # Django only allocates the full path on save() + o.save() + f.seek(0) + self.assertEqual(b'example_data\n', f.read()) self.assertEqual('django/example.foo', o.afile.name) def test_existing_file(self): o1 = WithFileFactory.build(afile__from_path=testdata.TESTFILE_PATH) - o1.save() + with o1.afile: + o1.save() self.assertEqual('django/example.data', o1.afile.name) o2 = WithFileFactory.build(afile__from_file=o1.afile) self.assertIsNone(o2.pk) - o2.save() - - self.assertEqual(b'example_data\n', o2.afile.read()) + with o2.afile as f: + o2.save() + f.seek(0) + self.assertEqual(b'example_data\n', f.read()) self.assertNotEqual('django/example.data', o2.afile.name) self.assertRegex(o2.afile.name, r'django/example_\w+.data') @@ -678,17 +690,19 @@ def test_with_file(self): o = WithImageFactory.build(animage__from_file=f) o.save() - # Image file for a 42x42 green jpeg: 301 bytes long. - self.assertEqual(301, len(o.animage.read())) + with o.animage as f: + # Image file for a 42x42 green jpeg: 301 bytes long. + self.assertEqual(301, len(f.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) - o.save() - - # Image file for a 42x42 green jpeg: 301 bytes long. - self.assertEqual(301, len(o.animage.read())) + with o.animage as f: + o.save() + f.seek(0) + # Image file for a 42x42 green jpeg: 301 bytes long. + self.assertEqual(301, len(f.read())) self.assertEqual('django/example.jpeg', o.animage.name) def test_with_file_empty_path(self): @@ -699,8 +713,9 @@ def test_with_file_empty_path(self): ) o.save() - # Image file for a 42x42 green jpeg: 301 bytes long. - self.assertEqual(301, len(o.animage.read())) + with o.animage as f: + # Image file for a 42x42 green jpeg: 301 bytes long. + self.assertEqual(301, len(f.read())) self.assertEqual('django/example.jpeg', o.animage.name) def test_with_path_empty_file(self): @@ -709,10 +724,11 @@ def test_with_path_empty_file(self): animage__from_file=None, ) self.assertIsNone(o.pk) - o.save() - - # Image file for a 42x42 green jpeg: 301 bytes long. - self.assertEqual(301, len(o.animage.read())) + with o.animage as f: + o.save() + f.seek(0) + # Image file for a 42x42 green jpeg: 301 bytes long. + self.assertEqual(301, len(f.read())) self.assertEqual('django/example.jpeg', o.animage.name) def test_error_both_file_and_path(self): @@ -729,10 +745,11 @@ def test_override_filename_with_path(self): animage__filename='example.foo', ) self.assertIsNone(o.pk) - o.save() - - # Image file for a 42x42 green jpeg: 301 bytes long. - self.assertEqual(301, len(o.animage.read())) + with o.animage as f: + o.save() + f.seek(0) + # Image file for a 42x42 green jpeg: 301 bytes long. + self.assertEqual(301, len(f.read())) self.assertEqual('django/example.foo', o.animage.name) def test_existing_file(self): @@ -743,8 +760,9 @@ def test_existing_file(self): self.assertIsNone(o2.pk) o2.save() - # Image file for a 42x42 green jpeg: 301 bytes long. - self.assertEqual(301, len(o2.animage.read())) + with o2.animage as f: + # Image file for a 42x42 green jpeg: 301 bytes long. + self.assertEqual(301, len(f.read())) self.assertNotEqual('django/example.jpeg', o2.animage.name) self.assertRegex(o2.animage.name, r'django/example_\w+.jpeg') From f32c1a374351a60f8d8fac46a87a228609a42084 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 4 Sep 2018 18:33:39 -0700 Subject: [PATCH 330/714] Remove unused factory.utils functions factory.utils.multi_extract_dict() has been unused since 6f202077a5c8156fe96f8a028f883c14962f5b95. factory.utils.extract_dict() has been unused since 8dadbe20e845ae7e311edf2cefc4ce9e24c25370. --- factory/utils.py | 60 ------------ tests/test_utils.py | 222 -------------------------------------------- 2 files changed, 282 deletions(-) diff --git a/factory/utils.py b/factory/utils.py index 75f352ba..7bf38dd6 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -6,66 +6,6 @@ import collections from . import compat -from . import enums - - -def extract_dict(prefix, kwargs, pop=True, exclude=()): - """Extracts all values beginning with a given prefix from a dict. - - Can either 'pop' or 'get' them; - - Args: - prefix (str): the prefix to use for lookups - kwargs (dict): the dict from which values should be extracted; WILL BE MODIFIED. - pop (bool): whether to use pop (True) or get (False) - exclude (iterable): list of prefixed keys that shouldn't be extracted - - Returns: - A new dict, containing values from kwargs and beginning with - prefix + enums.SPLITTER. That full prefix is removed from the keys - of the returned dict. - """ - prefix = prefix + enums.SPLITTER - extracted = {} - - for key in list(kwargs): - if key in exclude: - continue - - if key.startswith(prefix): - new_key = key[len(prefix):] - if pop: - value = kwargs.pop(key) - else: - value = kwargs[key] - extracted[new_key] = value - return extracted - - -def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): - """Extracts all values from a given list of prefixes. - - Extraction will start with longer prefixes. - - Args: - prefixes (str list): the prefixes to use for lookups - kwargs (dict): the dict from which values should be extracted; WILL BE MODIFIED. - pop (bool): whether to use pop (True) or get (False) - exclude (iterable): list of prefixed keys that shouldn't be extracted - - Returns: - dict(str => dict): a dict mapping each prefix to the dict of extracted - key/value. - """ - results = {} - exclude = list(exclude) - for prefix in sorted(prefixes, key=lambda x: -len(x)): - extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude) - results[prefix] = extracted - exclude.extend( - ['%s%s%s' % (prefix, enums.SPLITTER, key) for key in extracted]) - - return results def import_object(module_name, attribute_name): diff --git a/tests/test_utils.py b/tests/test_utils.py index 665fed78..43ba1222 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,228 +11,6 @@ from .compat import is_python2 -class ExtractDictTestCase(unittest.TestCase): - def test_empty_dict(self): - self.assertEqual({}, utils.extract_dict('foo', {})) - - def test_unused_key(self): - self.assertEqual({}, utils.extract_dict('foo', {'bar__baz': 42})) - - def test_empty_key(self): - self.assertEqual({}, utils.extract_dict('', {'foo': 13, 'bar__baz': 42})) - d = {'foo': 13, 'bar__baz': 42, '__foo': 1} - self.assertEqual({'foo': 1}, utils.extract_dict('', d)) - self.assertNotIn('__foo', d) - - def test_one_key(self): - d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual({'baz': 42}, utils.extract_dict('foo', d, pop=False)) - self.assertEqual(42, d['foo__baz']) - - self.assertEqual({'baz': 42}, utils.extract_dict('foo', d, pop=True)) - self.assertNotIn('foo__baz', d) - - def test_one_key_excluded(self): - d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual( - {}, - utils.extract_dict('foo', d, pop=False, exclude=('foo__baz',)), - ) - self.assertEqual(42, d['foo__baz']) - - self.assertEqual( - {}, - utils.extract_dict('foo', d, pop=True, exclude=('foo__baz',)), - ) - self.assertIn('foo__baz', d) - - def test_many_keys(self): - d = {'foo': 13, 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, '__foo': 1} - self.assertEqual( - {'foo__bar': 2, 'bar': 3, 'baz': 42}, - utils.extract_dict('foo', d, pop=False), - ) - self.assertEqual(42, d['foo__baz']) - self.assertEqual(3, d['foo__bar']) - self.assertEqual(2, d['foo__foo__bar']) - - self.assertEqual( - {'foo__bar': 2, 'bar': 3, 'baz': 42}, - utils.extract_dict('foo', d, pop=True), - ) - self.assertNotIn('foo__baz', d) - self.assertNotIn('foo__bar', d) - self.assertNotIn('foo__foo__bar', d) - - def test_many_keys_excluded(self): - d = {'foo': 13, 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, '__foo': 1} - self.assertEqual( - {'foo__bar': 2, 'baz': 42}, - utils.extract_dict('foo', d, pop=False, exclude=('foo__bar', 'bar')), - ) - self.assertEqual(42, d['foo__baz']) - self.assertEqual(3, d['foo__bar']) - self.assertEqual(2, d['foo__foo__bar']) - - self.assertEqual( - {'foo__bar': 2, 'baz': 42}, - utils.extract_dict('foo', d, pop=True, exclude=('foo__bar', 'bar')), - ) - self.assertNotIn('foo__baz', d) - self.assertIn('foo__bar', d) - self.assertNotIn('foo__foo__bar', d) - - -class MultiExtractDictTestCase(unittest.TestCase): - def test_empty_dict(self): - self.assertEqual({'foo': {}}, utils.multi_extract_dict(['foo'], {})) - - def test_unused_key(self): - self.assertEqual( - {'foo': {}}, - utils.multi_extract_dict(['foo'], {'bar__baz': 42}), - ) - self.assertEqual( - {'foo': {}, 'baz': {}}, - utils.multi_extract_dict(['foo', 'baz'], {'bar__baz': 42}), - ) - - def test_no_key(self): - self.assertEqual({}, utils.multi_extract_dict([], {'bar__baz': 42})) - - def test_empty_key(self): - self.assertEqual( - {'': {}}, - utils.multi_extract_dict([''], {'foo': 13, 'bar__baz': 42}), - ) - - d = {'foo': 13, 'bar__baz': 42, '__foo': 1} - self.assertEqual( - {'': {'foo': 1}}, - utils.multi_extract_dict([''], d), - ) - self.assertNotIn('__foo', d) - - def test_one_extracted(self): - d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual( - {'foo': {'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=False), - ) - self.assertEqual(42, d['foo__baz']) - - self.assertEqual( - {'foo': {'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=True), - ) - self.assertNotIn('foo__baz', d) - - def test_many_extracted(self): - d = {'foo': 13, 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, '__foo': 1} - self.assertEqual( - {'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=False), - ) - self.assertEqual(42, d['foo__baz']) - self.assertEqual(3, d['foo__bar']) - self.assertEqual(2, d['foo__foo__bar']) - - self.assertEqual( - {'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}}, - utils.multi_extract_dict(['foo'], d, pop=True), - ) - self.assertNotIn('foo__baz', d) - self.assertNotIn('foo__bar', d) - self.assertNotIn('foo__foo__bar', d) - - def test_many_keys_one_extracted(self): - d = {'foo': 13, 'foo__baz': 42, '__foo': 1} - self.assertEqual( - {'foo': {'baz': 42}, 'baz': {}}, - utils.multi_extract_dict(['foo', 'baz'], d, pop=False), - ) - self.assertEqual(42, d['foo__baz']) - - self.assertEqual( - {'foo': {'baz': 42}, 'baz': {}}, - utils.multi_extract_dict(['foo', 'baz'], d, pop=True), - ) - self.assertNotIn('foo__baz', d) - - def test_many_keys_many_extracted(self): - d = { - 'foo': 13, - 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, - 'bar__foo': 1, 'bar__bar__baz': 4, - } - - self.assertEqual( - { - 'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}, - 'bar': {'foo': 1, 'bar__baz': 4}, - 'baz': {} - }, - utils.multi_extract_dict(['foo', 'bar', 'baz'], d, pop=False)) - self.assertEqual(42, d['foo__baz']) - self.assertEqual(3, d['foo__bar']) - self.assertEqual(2, d['foo__foo__bar']) - self.assertEqual(1, d['bar__foo']) - self.assertEqual(4, d['bar__bar__baz']) - - self.assertEqual( - { - 'foo': {'foo__bar': 2, 'bar': 3, 'baz': 42}, - 'bar': {'foo': 1, 'bar__baz': 4}, - 'baz': {} - }, - utils.multi_extract_dict(['foo', 'bar', 'baz'], d, pop=True)) - self.assertNotIn('foo__baz', d) - self.assertNotIn('foo__bar', d) - self.assertNotIn('foo__foo__bar', d) - self.assertNotIn('bar__foo', d) - self.assertNotIn('bar__bar__baz', d) - - def test_son_in_list(self): - """Make sure that prefixes are used in decreasing match length order.""" - d = { - 'foo': 13, - 'foo__baz': 42, 'foo__foo__bar': 2, 'foo__bar': 3, - 'bar__foo': 1, 'bar__bar__baz': 4, - } - - self.assertEqual( - { - 'foo__foo': {'bar': 2}, - 'foo': {'bar': 3, 'baz': 42}, - 'bar__bar': {'baz': 4}, - 'bar': {'foo': 1}, - 'baz': {} - }, - utils.multi_extract_dict( - ['foo', 'bar', 'baz', 'foo__foo', 'bar__bar'], d, pop=False)) - self.assertEqual(42, d['foo__baz']) - self.assertEqual(3, d['foo__bar']) - self.assertEqual(2, d['foo__foo__bar']) - self.assertEqual(1, d['bar__foo']) - self.assertEqual(4, d['bar__bar__baz']) - - self.assertEqual( - { - 'foo__foo': {'bar': 2}, - 'foo': {'bar': 3, 'baz': 42}, - 'bar__bar': {'baz': 4}, - 'bar': {'foo': 1}, - 'baz': {} - }, - utils.multi_extract_dict( - ['foo', 'bar', 'baz', 'foo__foo', 'bar__bar'], d, pop=True)) - self.assertNotIn('foo__baz', d) - self.assertNotIn('foo__bar', d) - self.assertNotIn('foo__foo__bar', d) - self.assertNotIn('bar__foo', d) - self.assertNotIn('bar__bar__baz', d) - - class ImportObjectTestCase(unittest.TestCase): def test_datetime(self): imported = utils.import_object('datetime', 'date') From 3239a01dac52a517af1c1144dd7145ed3e096c97 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Thu, 13 Sep 2018 19:59:11 -0700 Subject: [PATCH 331/714] Replace deprecated assertEquals with assertEqual Fixes warning emitted during tests. For details, see: https://docs.python.org/3/library/unittest.html#deprecated-aliases Refs #365 --- tests/test_using.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_using.py b/tests/test_using.py index 08de02bc..f3d8f034 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1582,7 +1582,7 @@ class Meta: book__author__country = factory.LazyAttribute(lambda o: 'FR') chapter = ChapterFactory() - self.assertEquals('FR', chapter.book.author.country) + self.assertEqual('FR', chapter.book.author.country) def test_nested_sub_factory(self): """Test nested sub-factories.""" From 8a05af94f8c9331f448741efa8735d28e3c253ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 1 Sep 2018 20:29:27 +0200 Subject: [PATCH 332/714] Show FileField and ImageField's from_func argument in __init__ signature --- docs/orms.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 833e8b7d..11ffb94a 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -105,7 +105,7 @@ Extra fields Custom declarations for :class:`django.db.models.FileField` - .. method:: __init__(self, from_path='', from_file='', data=b'', filename='example.dat') + .. method:: __init__(self, from_path='', from_file='', from_func='', data=b'', filename='example.dat') :param str from_path: Use data from the file located at ``from_path``, and keep its filename @@ -138,7 +138,7 @@ Extra fields 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') + .. method:: __init__(self, from_path='', from_file='', from_func='', 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 From 97f48597d241aca598783f7bcaed34bf7b133343 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 28 Aug 2018 04:40:26 -0700 Subject: [PATCH 333/714] Add testing and support for Python 3.7 and Django 2.1 Python 3.7 was released on June 27, 2018. Django 2.1 was released on August 1, 2018. https://docs.python.org/3/whatsnew/3.7.html https://docs.djangoproject.com/en/2.1/releases/2.1/ Fixes #492 --- .travis.yml | 23 ++++++++++++++++++++--- README.rst | 2 +- docs/changelog.rst | 3 ++- factory/utils.py | 10 +++++++--- setup.py | 6 +++++- tox.ini | 7 +++++-- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c574b13..1773e905 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,20 +10,37 @@ install: matrix: include: + # Django 1.11 - python: "2.7" env: TOXENV=py27-django111-alchemy12-mongoengine015 + - python: "3.4" + env: TOXENV=py34-django111-alchemy12-mongoengine015 + - python: "3.5" + env: TOXENV=py35-django111-alchemy12-mongoengine015 + - python: "3.6" + env: TOXENV=py36-django111-alchemy12-mongoengine015 + + # Django 2.0 - python: "3.4" env: TOXENV=py34-django20-alchemy12-mongoengine015 - python: "3.5" env: TOXENV=py35-django20-alchemy12-mongoengine015 - python: "3.6" env: TOXENV=py36-django20-alchemy12-mongoengine015 - - python: "nightly" + - python: "3.7" env: TOXENV=py37-django20-alchemy12-mongoengine015 + dist: xenial + sudo: true - # Django LTS + # Django 2.1 + - python: "3.5" + env: TOXENV=py35-django21-alchemy12-mongoengine015 - python: "3.6" - env: TOXENV=py36-django111-alchemy12-mongoengine015 + env: TOXENV=py36-django21-alchemy12-mongoengine015 + - python: "3.7" + env: TOXENV=py37-django21-alchemy12-mongoengine015 + dist: xenial + sudo: true # Pypy - python: "pypy" diff --git a/README.rst b/README.rst index 1e8c327c..0bb58f52 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.org/project/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.4 to 3.6, as well as PyPy 2.7 and 5.8. +factory_boy supports Python 2.7, 3.4 to 3.7, as well as PyPy 2.7 and 5.8. Download diff --git a/docs/changelog.rst b/docs/changelog.rst index 6e1c1d2f..259acb4b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,8 @@ ChangeLog 2.11.2 (unreleased) ------------------- -- Nothing changed yet. +- Added support for Python 3.7 +- Added support for Django 2.1 2.11.1 (2018-05-05) diff --git a/factory/utils.py b/factory/utils.py index 7bf38dd6..15d87e4f 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -73,9 +73,13 @@ def __iter__(self): if self.next_elements: yield self.next_elements.popleft() else: - value = next(self.iterator) - self.past_elements.append(value) - yield value + try: + value = next(self.iterator) + except StopIteration: + break + else: + self.past_elements.append(value) + yield value def reset(self): self.next_elements.clear() diff --git a/setup.py b/setup.py index 4291c806..07fb60a4 100755 --- a/setup.py +++ b/setup.py @@ -51,8 +51,11 @@ def get_version(package_name): ], classifiers=[ "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", "Framework :: Django", + "Framework :: Django :: 1.11", + "Framework :: Django :: 2.0", + "Framework :: Django :: 2.1", + "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", @@ -62,6 +65,7 @@ def get_version(package_name): "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tox.ini b/tox.ini index e3198056..90d1a488 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = - py{27,34,35,36,37}-django{111,20}-alchemy12-mongoengine015, + py{27,34,35,36}-django111-alchemy12-mongoengine015, + py{34,35,36,37}-django20-alchemy12-mongoengine015, + py{35,36,37}-django21-alchemy12-mongoengine015, pypy-django{111}-alchemy12-mongoengine015, pypy3-django{111,20}-alchemy12-mongoengine015, examples @@ -13,7 +15,8 @@ deps = -rrequirements_test.txt django111: Django>=1.11,<1.12 django20: Django>=2.0,<2.1 - django{111,20}: Pillow + django21: Django>=2.1,<2.2 + django{111,20,21}: Pillow alchemy12: SQLAlchemy>=1.2,<1.3 mongoengine015: mongoengine>=0.15,<0.16 From ecde342771a29a804c939dcfc1e0b8bc07024228 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 28 Aug 2018 04:17:48 -0700 Subject: [PATCH 334/714] Prefer io.BytesIO; available on all supported Pythons Removes unnecessary compat.py workaround. --- factory/compat.py | 4 ---- factory/django.py | 5 +++-- tests/test_django.py | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/factory/compat.py b/factory/compat.py index 051c7dfe..bce21636 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -13,8 +13,6 @@ def is_string(obj): return isinstance(obj, (str, unicode)) # noqa - from StringIO import StringIO as BytesIO # noqa - def force_text(str_or_unicode): if isinstance(str_or_unicode, unicode): # noqa return str_or_unicode @@ -24,8 +22,6 @@ def force_text(str_or_unicode): def is_string(obj): return isinstance(obj, str) - from io import BytesIO # noqa - def force_text(text): return text diff --git a/factory/django.py b/factory/django.py index 15665375..d88db7af 100644 --- a/factory/django.py +++ b/factory/django.py @@ -7,6 +7,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import io import os import logging import functools @@ -23,7 +24,7 @@ from . import base from . import declarations from . import errors -from .compat import BytesIO, is_string +from .compat import is_string logger = logging.getLogger('factory.generate') @@ -248,7 +249,7 @@ def _make_data(self, params): image_format = params.get('format', 'JPEG') thumb = Image.new('RGB', (width, height), color) - thumb_io = BytesIO() + thumb_io = io.BytesIO() thumb.save(thumb_io, format=image_format) return thumb_io.getvalue() diff --git a/tests/test_django.py b/tests/test_django.py index 766a3616..1ee65437 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -3,6 +3,7 @@ """Tests for factory_boy/Django interactions.""" +import io import os import unittest @@ -30,7 +31,6 @@ import factory # noqa: E402 import factory.django # noqa: E402 -from factory.compat import BytesIO # noqa: E402 from . import testdata # noqa: E402 from .compat import mock # noqa: E402 @@ -773,7 +773,7 @@ def test_no_file(self): def _img_test_func(self): img = Image.new('RGB', (32, 32), 'blue') - img_io = BytesIO() + img_io = io.BytesIO() img.save(img_io, format='JPEG') img_io.seek(0) return img_io From f97f7d393b6cdf2aa86c9779fbcf59c6a060e5e5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 28 Aug 2018 04:22:38 -0700 Subject: [PATCH 335/714] Trim trailing white space throughout the project Many editors clean up trailing white space on save. By removing it all in one go, it helps keep future diffs cleaner by avoiding spurious white space changes on unrelated lines. --- docs/fuzzy.rst | 8 ++++---- docs/index.rst | 1 - docs/introduction.rst | 2 -- docs/orms.rst | 14 +++++++------- docs/reference.rst | 2 -- examples/django_demo/generic_foreignkey/models.py | 1 - 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index bfc64ae7..987d2b94 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -75,14 +75,14 @@ FuzzyChoice This allows passing in, for instance, a Django queryset that will only hit the database during the database, not at import time. - + .. warning:: When using Python2 and list comprehension, use private variable names as in: - + `[_x.name for _x in items]` - + instead of: - + `[x.name for x in items]` .. attribute:: choices diff --git a/docs/index.rst b/docs/index.rst index 7d9562a9..10997f1d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,4 +22,3 @@ Contents, indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/introduction.rst b/docs/introduction.rst index 58f9dfb3..e1b29625 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -361,5 +361,3 @@ Calling a :class:`~factory.Factory` subclass will provide an object through the The default strategy can be changed by setting the ``class Meta`` :attr:`~factory.FactoryOptions.strategy` attribute. - - diff --git a/docs/orms.rst b/docs/orms.rst index 11ffb94a..9b66b89e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -322,25 +322,25 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: .. attribute:: sqlalchemy_session_persistence Control the action taken by sqlalchemy session at the end of a create call. - + Valid values are: - + * ``None``: do nothing * ``'flush'``: perform a session :meth:`~sqlalchemy.orm.session.Session.flush` * ``'commit'``: perform a session :meth:`~sqlalchemy.orm.session.Session.commit` - + The default value is ``None``. - + If ``force_flush`` is set to ``True``, it overrides this option. .. attribute:: force_flush Force a session ``flush()`` at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. - + .. note:: - + This option is deprecated. Use ``sqlalchemy_session_persistence`` instead. - + A (very) simple example: .. code-block:: python diff --git a/docs/reference.rst b/docs/reference.rst index 2db1680e..ae041f01 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -2034,5 +2034,3 @@ extracting instances from them: :param int size: Number of instances to generate :param kwargs: Declarations to use for the generated factory :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`) - - diff --git a/examples/django_demo/generic_foreignkey/models.py b/examples/django_demo/generic_foreignkey/models.py index f4e1afa7..efaa23dc 100644 --- a/examples/django_demo/generic_foreignkey/models.py +++ b/examples/django_demo/generic_foreignkey/models.py @@ -14,4 +14,3 @@ class TaggedItem(models.Model): def __str__(self): # __unicode__ on Python 2 return self.tag - From d1a1aa1f8755a9b0f770e99c149878d3832c57be Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 28 Aug 2018 04:01:26 -0700 Subject: [PATCH 336/714] Consistently use full Django namespace in all examples and tests --- docs/recipes.rst | 2 +- examples/django_demo/generic_foreignkey/factories.py | 6 +++--- tests/test_django.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 2ffe39cb..86631580 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -346,7 +346,7 @@ default :meth:`Model.objects.create() ` .. code-block:: python - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): class Meta: model = UserenaSignup diff --git a/examples/django_demo/generic_foreignkey/factories.py b/examples/django_demo/generic_foreignkey/factories.py index d39da0b8..725bc0fc 100644 --- a/examples/django_demo/generic_foreignkey/factories.py +++ b/examples/django_demo/generic_foreignkey/factories.py @@ -5,21 +5,21 @@ from .models import TaggedItem -class UserFactory(factory.DjangoModelFactory): +class UserFactory(factory.django.DjangoModelFactory): first_name = 'Adam' class Meta: model = User -class GroupFactory(factory.DjangoModelFactory): +class GroupFactory(factory.django.DjangoModelFactory): name = 'group' class Meta: model = Group -class TaggedItemFactory(factory.DjangoModelFactory): +class TaggedItemFactory(factory.django.DjangoModelFactory): object_id = factory.SelfAttribute('content_object.id') content_type = factory.LazyAttribute( lambda o: ContentType.objects.get_for_model(o.content_object)) diff --git a/tests/test_django.py b/tests/test_django.py index 1ee65437..e573f1fb 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -243,14 +243,14 @@ class DjangoModelLoadingTestCase(django_test.TestCase): model = 'app.Model' pattern.""" def test_loading(self): - class ExampleFactory(factory.DjangoModelFactory): + class ExampleFactory(factory.django.DjangoModelFactory): class Meta: model = 'djapp.StandardModel' self.assertEqual(models.StandardModel, ExampleFactory._meta.get_model_class()) def test_building(self): - class ExampleFactory(factory.DjangoModelFactory): + class ExampleFactory(factory.django.DjangoModelFactory): class Meta: model = 'djapp.StandardModel' @@ -262,7 +262,7 @@ def test_inherited_loading(self): See https://github.com/FactoryBoy/factory_boy/issues/109. """ - class ExampleFactory(factory.DjangoModelFactory): + class ExampleFactory(factory.django.DjangoModelFactory): class Meta: model = 'djapp.StandardModel' @@ -277,7 +277,7 @@ def test_inherited_loading_and_sequence(self): See https://github.com/FactoryBoy/factory_boy/issues/109. """ - class ExampleFactory(factory.DjangoModelFactory): + class ExampleFactory(factory.django.DjangoModelFactory): class Meta: model = 'djapp.StandardModel' From 794c175e626d02d57e6987d5f08b2b7140bc27df Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 28 Aug 2018 04:42:03 -0700 Subject: [PATCH 337/714] Remove unnecessary imports of submodules The `django` and `alchemy` modules are imported into the `factory` namespace in `factory/__init__.py`. No need to import explicitly in other files. --- docs/orms.rst | 2 -- tests/test_django.py | 1 - 2 files changed, 3 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 9b66b89e..c600263e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -193,7 +193,6 @@ To work around this problem, use the :meth:`mute_signals()` decorator/context ma # foo/factories.py import factory - import factory.django from . import models from . import signals @@ -429,7 +428,6 @@ Here is an example layout: # myproject/factories.py import factory - import factory.alchemy from . import models from .test import common diff --git a/tests/test_django.py b/tests/test_django.py index e573f1fb..9a7827c3 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -30,7 +30,6 @@ Image = None import factory # noqa: E402 -import factory.django # noqa: E402 from . import testdata # noqa: E402 from .compat import mock # noqa: E402 From 4f2c2d036ef294c674cbbb27b10260837731c1a4 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Thu, 27 Sep 2018 18:25:10 -0400 Subject: [PATCH 338/714] Change Faker link from PyPI to Read The Docs. --- README.rst | 2 +- docs/reference.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0bb58f52..c48d6b96 100644 --- a/README.rst +++ b/README.rst @@ -187,7 +187,7 @@ Realistic, random values """""""""""""""""""""""" Demos look better with random yet realistic values; and those realistic values can also help discover bugs. -For this, factory_boy relies on the excellent `faker `_ library: +For this, factory_boy relies on the excellent `faker `_ library: .. code-block:: python diff --git a/docs/reference.rst b/docs/reference.rst index ae041f01..8617b66d 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -675,7 +675,7 @@ Faker In order to easily define realistic-looking factories, use the :class:`Faker` attribute declaration. - This is a wrapper around `faker `_; + This is a wrapper around `faker `_; its argument is the name of a ``faker`` provider: .. code-block:: python From 94359f400e6f88196d704d7f7778a2c0254bcbb5 Mon Sep 17 00:00:00 2001 From: Aleksey Nakoryakov Date: Sat, 13 Oct 2018 18:57:20 +0300 Subject: [PATCH 339/714] Remove private method call from documentation --- docs/reference.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 8617b66d..0b88db6f 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -345,8 +345,10 @@ Attributes and methods .. code-block:: pycon + >>> SomeFactory.build().sequenced_attribute + 0 >>> SomeFactory.reset_sequence(4) - >>> SomeFactory._next_sequence + >>> SomeFactory.build().sequenced_attribute 4 Since subclasses of a non-:attr:`abstract ` From 467a35c70b02eae3fa4bee1ece47c92e3aa6024e Mon Sep 17 00:00:00 2001 From: mstpierre Date: Thu, 11 Oct 2018 14:12:32 -0400 Subject: [PATCH 340/714] Add build for nightly python --- .travis.yml | 22 ++++++++++++++++++++++ tox.ini | 4 ++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1773e905..d1216b3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,11 +48,33 @@ matrix: - python: "pypy3" env: TOXENV=pypy3-django20-alchemy12-mongoengine015 + # Django 2.0 + - python: "nightly" + env: TOXENV=pynightly-django20-alchemy12-mongoengine015 + dist: xenial + sudo: true + # Django 2.1 + - python: "nightly" + env: TOXENV=pynightly-django21-alchemy12-mongoengine015 + dist: xenial + sudo: true + # Linting - python: "3.6" env: TOXENV=examples - python: "3.6" env: TOXENV=lint + allow_failures: + # Django 2.0 + - python: "nightly" + env: TOXENV=pynightly-django20-alchemy12-mongoengine015 + dist: xenial + sudo: true + # Django 2.1 + - python: "nightly" + env: TOXENV=pynightly-django21-alchemy12-mongoengine015 + dist: xenial + sudo: true services: - mongodb diff --git a/tox.ini b/tox.ini index 90d1a488..81168127 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,8 @@ [tox] envlist = py{27,34,35,36}-django111-alchemy12-mongoengine015, - py{34,35,36,37}-django20-alchemy12-mongoengine015, - py{35,36,37}-django21-alchemy12-mongoengine015, + py{34,35,36,37,nightly}-django20-alchemy12-mongoengine015, + py{35,36,37,nightly}-django21-alchemy12-mongoengine015, pypy-django{111}-alchemy12-mongoengine015, pypy3-django{111,20}-alchemy12-mongoengine015, examples From 1cc3fd149599e1845ef48b7c50f0dc9428f6f513 Mon Sep 17 00:00:00 2001 From: Ivan Pereira Date: Sat, 6 Oct 2018 11:58:43 +0100 Subject: [PATCH 341/714] fixes typo on simple parameters documentation --- docs/reference.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 0b88db6f..a628690b 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -415,9 +415,9 @@ Some factories only need little data: .. code-block:: pycon - >>> Conference(duration='short') + >>> ConferenceFactory(duration='short') - >>> Conference(duration='long') + >>> ConferenceFactory(duration='long') From 8cba0910d654683500829691c71e350aef944d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 21 Oct 2018 09:25:51 +0200 Subject: [PATCH 342/714] Directly link to pull requests in change log Avoids redirection from /issues/ to /pull/. For example: ``` $ curl -L https://github.com/FactoryBoy/factory_boy/issues/247 --include HTTP/1.1 302 Found Server: GitHub.com Content-Type: text/html; charset=utf-8 Status: 302 Found Location: https://github.com/FactoryBoy/factory_boy/pull/247 HTTP/1.1 200 OK Server: GitHub.com Content-Type: text/html; charset=utf-8 Status: 200 OK ``` --- docs/changelog.rst | 28 ++++++++++++++-------------- docs/conf.py | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 259acb4b..09f4f83b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -116,14 +116,14 @@ corner cases and weird behaviourrs. *New:* - - :issue:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambda parameters, + - :pr:`267`: Add :class:`factory.LazyFunction` to remove unneeded lambda parameters, thanks to `Hervé Cauwelier `_. - :issue:`251`: Add :ref:`parameterized factories ` and :class:`traits ` - - :issue:`256`, :issue:`292`: Improve error messages in corner cases + - :pr:`256`, :pr:`292`: Improve error messages in corner cases *Removed:* - - :issue:`278`: Formally drop support for Python2.6 + - :pr:`278`: Formally drop support for Python2.6 .. warning:: Version 2.7.0 moves all error classes to `factory.errors`. This breaks existing import statements @@ -137,7 +137,7 @@ corner cases and weird behaviourrs. *New:* - - :issue:`262`: Allow optional forced flush on SQLAlchemy, courtesy of `Minjung `_. + - :pr:`262`: Allow optional forced flush on SQLAlchemy, courtesy of `Minjung `_. .. _v2.6.0: @@ -160,7 +160,7 @@ corner cases and weird behaviourrs. - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required - - :issue:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago. + - :pr:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago. .. _v2.5.2: @@ -257,8 +257,8 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *New:* - - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:issue:`120`) - - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:issue:`122`) + - Add support for :attr:`factory.fuzzy.FuzzyInteger.step`, thanks to `ilya-pirogov `_ (:pr:`120`) + - Add :meth:`~factory.django.mute_signals` decorator to temporarily disable some signals, thanks to `ilya-pirogov `_ (:pr:`122`) - Add :class:`~factory.fuzzy.FuzzyFloat` (:issue:`124`) - Declare target model and other non-declaration fields in a ``class Meta`` section. @@ -290,9 +290,9 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *Bugfix:* - - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:issue:`126`) + - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:pr:`126`) - Don't crash when handling objects whose __repr__ is non-pure-ascii bytes on Py2, - discovered by `mbertheau `_ (:issue:`123`) and `strycore `_ (:issue:`127`) + discovered by `mbertheau `_ (:issue:`123`) and `strycore `_ (:pr:`127`) .. _v2.3.0: @@ -301,9 +301,9 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *New:* - - 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`) + - Add :class:`~factory.fuzzy.FuzzyText`, thanks to `jdufresne `_ (:pr:`97`) + - Add :class:`~factory.fuzzy.FuzzyDecimal`, thanks to `thedrow `_ (:pr:`94`) + - Add support for :class:`~mongoengine.EmbeddedDocument`, thanks to `imiric `_ (:pr:`100`) .. _v2.2.1: @@ -323,7 +323,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *Bugfix:* - Removed duplicated :class:`~factory.alchemy.SQLAlchemyModelFactory` lurking in :mod:`factory` - (:issue:`83`) + (:pr:`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`) @@ -376,7 +376,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a - 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é `_) + (:pr:`64`, thanks to `Romain Commandé `_) - Add :class:`factory.django.FileField` and :class:`factory.django.ImageField` hooks for related Django model fields (:issue:`52`) diff --git a/docs/conf.py b/docs/conf.py index 3fb4b732..64629229 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,6 +35,7 @@ extlinks = { 'issue': ('https://github.com/FactoryBoy/factory_boy/issues/%s', 'issue #'), + 'pr': ('https://github.com/FactoryBoy/factory_boy/pull/%s', 'pull request #'), } # Add any paths that contain templates here, relative to this directory. From dd2cbf3e56ed443b4ffe7f617f390b3d8c9e12cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 20 Oct 2018 23:39:35 +0200 Subject: [PATCH 343/714] Follow most recent version of Python Python 3.7 is now the latest stable release of Python, follow upstream to benefit from latest features and improvements. --- .travis.yml | 8 ++++++-- tox.ini | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index d1216b3d..a176618b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,10 +60,14 @@ matrix: sudo: true # Linting - - python: "3.6" + - python: "3.7" env: TOXENV=examples - - python: "3.6" + dist: xenial + sudo: true + - python: "3.7" env: TOXENV=lint + dist: xenial + sudo: true allow_failures: # Django 2.0 - python: "nightly" diff --git a/tox.ini b/tox.ini index 81168127..e56e647d 100644 --- a/tox.ini +++ b/tox.ini @@ -24,7 +24,7 @@ whitelist_externals = make commands = make test [testenv:examples] -basepython = python3.6 +basepython = python3.7 deps = -rrequirements_test.txt -rexamples/requirements.txt From bdbb32d0dde9560f867f8cdf53667c0eb64e7319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 21 Oct 2018 09:39:22 +0200 Subject: [PATCH 344/714] Verify external links in documentation --- .travis.yml | 4 ++++ Makefile | 4 +++- tox.ini | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a176618b..4cf123ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -59,6 +59,10 @@ matrix: dist: xenial sudo: true + # Documentation + - python: 3.6 + env: TOXENV=linkcheck + # Linting - python: "3.7" env: TOXENV=examples diff --git a/Makefile b/Makefile index 457e9a2a..2e172b4c 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,8 @@ coverage: doc: $(MAKE) -C $(DOC_DIR) html +linkcheck: + $(MAKE) -C $(DOC_DIR) linkcheck # DOC: Show this help message help: @@ -110,4 +112,4 @@ help: }' -.PHONY: doc help +.PHONY: doc linkcheck help diff --git a/tox.ini b/tox.ini index e56e647d..c6755078 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ envlist = pypy-django{111}-alchemy12-mongoengine015, pypy3-django{111,20}-alchemy12-mongoengine015, examples + linkcheck lint toxworkdir = {env:TOX_WORKDIR:.tox} @@ -32,6 +33,13 @@ deps = whitelist_externals = make commands = make example-test +[testenv:linkcheck] +deps = + -rrequirements_dev.txt + +whitelist_externals = make +commands = make linkcheck + [testenv:lint] deps = flake8 From fa08e36a67b95b6297db32f042e60406e9862d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 20 Oct 2018 23:19:35 +0200 Subject: [PATCH 345/714] Turn sphinx warnings into errors There are currently no warning when building the documentation. Turning warning to errors highlights issues and prevent them from getting into the code base. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2e172b4c..48bbaa80 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ coverage: # DOC: Compile the documentation doc: - $(MAKE) -C $(DOC_DIR) html + $(MAKE) -C $(DOC_DIR) SPHINXOPTS=-W html linkcheck: $(MAKE) -C $(DOC_DIR) linkcheck From 97b00d0a2055ebb7dc5ddb38057bc60b6c8a52f1 Mon Sep 17 00:00:00 2001 From: Charles-Axel Dein Date: Mon, 22 Oct 2018 09:45:37 +0200 Subject: [PATCH 346/714] Fix typo in reference.rst --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index a628690b..0ee461e3 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1734,7 +1734,7 @@ The :class:`PostGeneration` declaration performs actions once the model object has been generated. Its sole argument is a callable, that will be called once the base object has - been generated. +been generated. Once the base object has been generated, the provided callable will be called as ``callable(obj, create, extracted, **kwargs)``, where: From 92cc94d50ec892a0bada258be661dd544b45219d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 20 Oct 2018 23:13:14 +0200 Subject: [PATCH 347/714] Automatically build documentation on CI --- .travis.yml | 8 +++++++- tox.ini | 9 +++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4cf123ca..57e94dad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -60,8 +60,14 @@ matrix: sudo: true # Documentation - - python: 3.6 + - python: "3.7" + env: TOXENV=docs + dist: xenial + sudo: true + - python: "3.7" env: TOXENV=linkcheck + dist: xenial + sudo: true # Linting - python: "3.7" diff --git a/tox.ini b/tox.ini index c6755078..c8fddd5b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py{35,36,37,nightly}-django21-alchemy12-mongoengine015, pypy-django{111}-alchemy12-mongoengine015, pypy3-django{111,20}-alchemy12-mongoengine015, + docs examples linkcheck lint @@ -24,6 +25,14 @@ deps = whitelist_externals = make commands = make test +[testenv:docs] +basepython = python3.7 +deps = + -rrequirements_dev.txt + +whitelist_externals = make +commands = make doc + [testenv:examples] basepython = python3.7 deps = From 06385a81c8a6ea31142b2920d47bd28c250679f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Dec 2018 09:52:42 +0100 Subject: [PATCH 348/714] linting: Fix warning W504. As pointed by Matt Mahowald, new flake8 versions raise an additional warning. Closes #547. --- factory/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/factory/utils.py b/factory/utils.py index 15d87e4f..a3ad2a26 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -44,8 +44,9 @@ def __repr__(self): def __str__(self): return ', '.join( - [_safe_repr(arg) for arg in self.args] + [ + _safe_repr(arg) for arg in self.args + ] + [ '%s=%s' % (key, _safe_repr(value)) for key, value in self.kwargs.items() ] From bc9b5615558418012279aa5c526cbbf87f1fe5d7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 8 Dec 2018 05:09:56 -0800 Subject: [PATCH 349/714] Use 'dist: xenial' on Travis to simplify configuration Allows using Python version 3.7 without sudo declarations. Travis officially added support for Xenial on 2018-11-08. https://blog.travis-ci.com/2018-11-08-xenial-release Using PyPy on xenial requires an explicit version, see: https://travis-ci.community/t/pypy-2-7-on-xenial/889 --- .travis.yml | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/.travis.yml b/.travis.yml index 57e94dad..50d3b81d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial sudo: false language: python cache: pip @@ -29,8 +30,6 @@ matrix: env: TOXENV=py36-django20-alchemy12-mongoengine015 - python: "3.7" env: TOXENV=py37-django20-alchemy12-mongoengine015 - dist: xenial - sudo: true # Django 2.1 - python: "3.5" @@ -39,56 +38,38 @@ matrix: env: TOXENV=py36-django21-alchemy12-mongoengine015 - python: "3.7" env: TOXENV=py37-django21-alchemy12-mongoengine015 - dist: xenial - sudo: true # Pypy - - python: "pypy" + - python: "pypy2.7-6.0" env: TOXENV=pypy-django111-alchemy12-mongoengine015 - - python: "pypy3" + - python: "pypy3.5-6.0" env: TOXENV=pypy3-django20-alchemy12-mongoengine015 # Django 2.0 - python: "nightly" env: TOXENV=pynightly-django20-alchemy12-mongoengine015 - dist: xenial - sudo: true # Django 2.1 - python: "nightly" env: TOXENV=pynightly-django21-alchemy12-mongoengine015 - dist: xenial - sudo: true # Documentation - python: "3.7" env: TOXENV=docs - dist: xenial - sudo: true - python: "3.7" env: TOXENV=linkcheck - dist: xenial - sudo: true # Linting - python: "3.7" env: TOXENV=examples - dist: xenial - sudo: true - python: "3.7" env: TOXENV=lint - dist: xenial - sudo: true allow_failures: # Django 2.0 - python: "nightly" env: TOXENV=pynightly-django20-alchemy12-mongoengine015 - dist: xenial - sudo: true # Django 2.1 - python: "nightly" env: TOXENV=pynightly-django21-alchemy12-mongoengine015 - dist: xenial - sudo: true services: - mongodb From a5f0c32f0424ff4d20b7681a08763116b769e4fb Mon Sep 17 00:00:00 2001 From: Matt Mahowald <10021147+mattmahowald@users.noreply.github.com> Date: Wed, 5 Dec 2018 15:43:16 -0800 Subject: [PATCH 350/714] Fix typo --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index c600263e..261e2ef0 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -293,7 +293,7 @@ SQLAlchemy .. currentmodule:: factory.alchemy -Factoy_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class. +Factory_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class. To work, this class needs an `SQLAlchemy`_ session object affected to the :attr:`Meta.sqlalchemy_session ` attribute. From f2c39175783ffc7a9082b7127d6d6bdc9371dac5 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sat, 5 Jan 2019 17:58:51 -0500 Subject: [PATCH 351/714] Add untested PyPy3 + Django 1.11 combination --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 50d3b81d..cf10a6e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,6 +42,8 @@ matrix: # Pypy - python: "pypy2.7-6.0" env: TOXENV=pypy-django111-alchemy12-mongoengine015 + - python: "pypy3.5-6.0" + env: TOXENV=pypy3-django111-alchemy12-mongoengine015 - python: "pypy3.5-6.0" env: TOXENV=pypy3-django20-alchemy12-mongoengine015 From bf40d14a0e23d8432761b4846c4f0297ce9463e3 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sat, 5 Jan 2019 18:00:57 -0500 Subject: [PATCH 352/714] Test Python 3.7 + Django 1.11 This is an officially supported combination for Django and Python. --- .travis.yml | 2 ++ tox.ini | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cf10a6e0..d0df28c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,8 @@ matrix: env: TOXENV=py35-django111-alchemy12-mongoengine015 - python: "3.6" env: TOXENV=py36-django111-alchemy12-mongoengine015 + - python: "3.7" + env: TOXENV=py37-django111-alchemy12-mongoengine015 # Django 2.0 - python: "3.4" diff --git a/tox.ini b/tox.ini index c8fddd5b..ab025d07 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{27,34,35,36}-django111-alchemy12-mongoengine015, + py{27,34,35,36,37}-django111-alchemy12-mongoengine015, py{34,35,36,37,nightly}-django20-alchemy12-mongoengine015, py{35,36,37,nightly}-django21-alchemy12-mongoengine015, pypy-django{111}-alchemy12-mongoengine015, From 42c6eb639be9bb4b23cfea5bf25efadc187c8f09 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Sat, 5 Jan 2019 18:10:49 -0500 Subject: [PATCH 353/714] Simplify Travis CI configuration using tox-travis All of this was repeated in the Tox configuration. Let's see if this increases the build time too much for each Python version. --- .travis.yml | 41 +---------------------------------------- 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/.travis.yml b/.travis.yml index d0df28c0..a46bad8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,54 +7,20 @@ script: - tox install: - - pip install tox + - pip install tox tox-travis matrix: include: - # Django 1.11 - python: "2.7" - env: TOXENV=py27-django111-alchemy12-mongoengine015 - python: "3.4" - env: TOXENV=py34-django111-alchemy12-mongoengine015 - python: "3.5" - env: TOXENV=py35-django111-alchemy12-mongoengine015 - python: "3.6" - env: TOXENV=py36-django111-alchemy12-mongoengine015 - python: "3.7" - env: TOXENV=py37-django111-alchemy12-mongoengine015 - # Django 2.0 - - python: "3.4" - env: TOXENV=py34-django20-alchemy12-mongoengine015 - - python: "3.5" - env: TOXENV=py35-django20-alchemy12-mongoengine015 - - python: "3.6" - env: TOXENV=py36-django20-alchemy12-mongoengine015 - - python: "3.7" - env: TOXENV=py37-django20-alchemy12-mongoengine015 - - # Django 2.1 - - python: "3.5" - env: TOXENV=py35-django21-alchemy12-mongoengine015 - - python: "3.6" - env: TOXENV=py36-django21-alchemy12-mongoengine015 - - python: "3.7" - env: TOXENV=py37-django21-alchemy12-mongoengine015 - - # Pypy - python: "pypy2.7-6.0" - env: TOXENV=pypy-django111-alchemy12-mongoengine015 - python: "pypy3.5-6.0" - env: TOXENV=pypy3-django111-alchemy12-mongoengine015 - - python: "pypy3.5-6.0" - env: TOXENV=pypy3-django20-alchemy12-mongoengine015 - # Django 2.0 - - python: "nightly" - env: TOXENV=pynightly-django20-alchemy12-mongoengine015 - # Django 2.1 - python: "nightly" - env: TOXENV=pynightly-django21-alchemy12-mongoengine015 # Documentation - python: "3.7" @@ -68,12 +34,7 @@ matrix: - python: "3.7" env: TOXENV=lint allow_failures: - # Django 2.0 - - python: "nightly" - env: TOXENV=pynightly-django20-alchemy12-mongoengine015 - # Django 2.1 - python: "nightly" - env: TOXENV=pynightly-django21-alchemy12-mongoengine015 services: - mongodb From df3df08d49054d2b569be4a4bb5d4aa156badb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 23 Feb 2019 13:37:58 +0100 Subject: [PATCH 354/714] Improve documentation about factory.random. - Remove obsolete references to factory.fuzzy.random - Provide some sample recipes - Add a clearer entry in the project README. Closes #563. --- README.rst | 15 ++++++++++++-- docs/fuzzy.rst | 33 +++++------------------------- docs/recipes.rst | 50 ++++++++++++++++++++++++++++++++++++++++++++++ docs/reference.rst | 30 ++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 30 deletions(-) diff --git a/README.rst b/README.rst index c48d6b96..b8a385cf 100644 --- a/README.rst +++ b/README.rst @@ -204,8 +204,19 @@ For this, factory_boy relies on the excellent `faker -.. note:: Use of fully randomized data in tests is quickly a problem for reproducing broken builds. - To that purpose, factory_boy provides helpers to handle the random seeds it uses. +Reproducible random values +"""""""""""""""""""""""""" + +The use of fully randomized data in tests is quickly a problem for reproducing broken builds. +To that purpose, factory_boy provides helpers to handle the random seeds it uses, located in the ``factory.random`` module: + +.. code-block:: python + + import factory.random + + def setup_test_environment(): + factory.random.reseed_random('my_awesome_project') + # Other setup here Lazy Attributes diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 987d2b94..34a174ab 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -359,32 +359,9 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its The method responsible for generating random values. *Must* be overridden in subclasses. + .. warning:: -Managing randomness -------------------- - -Using :mod:`random` in factories allows to "fuzz" a program efficiently. -However, it's sometimes required to *reproduce* a failing test. - -:mod:`factory.fuzzy` uses a separate instance of :class:`random.Random`, -and provides a few helpers for this: - -.. method:: get_random_state() - - Call :meth:`get_random_state` to retrieve the random generator's current - state. - -.. method:: set_random_state(state) - - Use :meth:`set_random_state` to set a custom state into the random generator - (fetched from :meth:`get_random_state` in a previous run, for instance) - -.. method:: reseed_random(seed) - - The :meth:`reseed_random` function allows to load a chosen seed into the random generator. - - -Custom :class:`BaseFuzzyAttribute` subclasses **SHOULD** -use :obj:`factory.fuzzy._random` as a randomness source; this ensures that -data they generate can be regenerated using the simple state from -:meth:`get_random_state`. + Custom :class:`BaseFuzzyAttribute` subclasses **MUST** + use :obj:`factory.fuzzy._random` as a randomness source; this ensures that + data they generate can be regenerated using the simple state from + :meth:`get_random_state`. diff --git a/docs/recipes.rst b/docs/recipes.rst index 86631580..bd0e12de 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -455,6 +455,56 @@ Forcing the initial value for all projects >>> AccountFactory.create() # Sets up the account number based on the latest uid +.. _recipe-random-management: + +Using reproducible randomness +----------------------------- + +Although using random values is great, it can provoke test flakyness. +factory_boy provides a few helpers for this. + +.. note:: Those methods will seed the random engine used in both :class:`factory.Faker` and :mod:`factory.fuzzy` objects. + + +Seeding the random engine + The simplest way to manage randomness is to push a selected seed when starting tests: + + .. code-block:: python + + import factory.random + # Pass in any value + factory.random.reseed_random('my awesome project') + + +Reproducing unseeded tests + A project might choose not to use an explicit random seed (for better fuzzing), + but still wishes to have reproducible tests. + + For such cases, use a combination of :meth:`factory.random.get_random_state()` + and :meth:`factory.random.set_random_state()`. + + Since the random state structure is implementation-specific, we recommand passing it around + as a base64-encoded pickle dump. + + .. code-block:: python + + class MyTestRunner: + + def setup_test_environment(self): + state = os.environ.get('TEST_RANDOM_STATE') + if state: + try: + decoded_state = pickle.loads(base64.b64decode(state.encode('ascii'))) + except ValueError: + decoded_state = None + if decoded_state: + factory.random.set_random_state(decoded_state) + else: + encoded_state = base64.b64encode(pickle.dumps(factory.random.get_random_state())) + print("Current random state: %s" % encoded_state.decode('ascii')) + super().setup_test_environment() + + Converting a factory's output to a dict --------------------------------------- diff --git a/docs/reference.rst b/docs/reference.rst index 0ee461e3..32d53ede 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -2036,3 +2036,33 @@ extracting instances from them: :param int size: Number of instances to generate :param kwargs: Declarations to use for the generated factory :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`) + + +Randomness management +--------------------- + +.. currentmodule:: factory.random + +Using :mod:`random` in factories allows to "fuzz" a program efficiently. +However, it's sometimes required to *reproduce* a failing test. + +:mod:`factory.fuzzy` and :class:`factory.Faker` share a dedicated instance +of :class:`random.Random`, which can be managed through the :mod:`factory.random` module: + +.. method:: get_random_state() + + Call :meth:`get_random_state` to retrieve the random generator's current + state. + The returned object is implementation-specific. + +.. method:: set_random_state(state) + + Use :meth:`set_random_state` to set a custom state into the random generator + (fetched from :meth:`get_random_state` in a previous run, for instance) + +.. method:: reseed_random(seed) + + The :meth:`reseed_random` function allows to load a chosen seed into the random generator. + That seed can be anything accepted by :meth:`random.seed`. + +See :ref:`recipe-random-management` for help in using those methods in a test setup. From e76cf2a1ec91290626ebd7393b76542bf3ca4941 Mon Sep 17 00:00:00 2001 From: Kirill Goncharov Date: Tue, 30 Oct 2018 15:04:11 +0300 Subject: [PATCH 355/714] Fix error in documentation for _create method --- docs/reference.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/reference.rst b/docs/reference.rst index 32d53ede..42d01a7a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -311,6 +311,7 @@ Attributes and methods class Meta: abstract = True # Optional + @classmethod def _create(cls, model_class, *args, **kwargs): obj = model_class(*args, **kwargs) obj.save() From 79ef1461ca070db6e18eade4e07c61c8742b1b62 Mon Sep 17 00:00:00 2001 From: Jasper Koops Date: Mon, 5 Nov 2018 15:29:21 +0100 Subject: [PATCH 356/714] Update recipes.rst Faker('username') throws an error, but Faker('user_name') works as expected --- docs/recipes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index bd0e12de..c0f98c71 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -529,7 +529,7 @@ In order to get a dict, we'll just have to swap the model; the easiest way is to model = models.User first_name = factory.Sequence(lambda n: "Agent %03d" % n) - username = factory.Faker('username') + username = factory.Faker('user_name') .. code-block:: pycon From c42bad4a176c7e0f3c01aebd7765979bd8a26764 Mon Sep 17 00:00:00 2001 From: Charles-Axel Dein Date: Thu, 10 Jan 2019 16:11:31 +0100 Subject: [PATCH 357/714] Clarify first argument of post_generation decorated `self` usually means the current class instance, which in this case would be `UserFactory`, instead of `User`. Replacing with `obj`, which makes it clear that the arguments of the decorated method are indeed the same as for `PostGeneration`. --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 42d01a7a..6b074e63 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1778,7 +1778,7 @@ A decorator is also provided, decorating a single method accepting the same login = 'john' @factory.post_generation - def mbox(self, create, extracted, **kwargs): + def mbox(obj, create, extracted, **kwargs): if not create: return path = extracted or os.path.join('/tmp/mbox/', self.login) From bf5a65dd5849b1daebeac210cd369f6c901f5e13 Mon Sep 17 00:00:00 2001 From: Evgeny Mozhaev Date: Thu, 14 Feb 2019 15:54:39 +0300 Subject: [PATCH 358/714] Fix typo --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 6b074e63..7909c657 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1766,7 +1766,7 @@ Decorator .. function:: post_generation A decorator is also provided, decorating a single method accepting the same -``obj``, ``created``, ``extracted`` and keyword arguments as :class:`PostGeneration`. +``obj``, ``create``, ``extracted`` and keyword arguments as :class:`PostGeneration`. .. code-block:: python From 0a0f8682cc3aeb36b6f40c83efcdaddcc3a48de0 Mon Sep 17 00:00:00 2001 From: Arne de Laat Date: Tue, 30 Oct 2018 14:58:26 +0100 Subject: [PATCH 359/714] Fix final List order for lists longer than 10 items. Add comment about changes after dropping Python <3.6 support. --- factory/base.py | 5 ++++- tests/test_using.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/factory/base.py b/factory/base.py index ab89db38..92128885 100644 --- a/factory/base.py +++ b/factory/base.py @@ -733,7 +733,10 @@ def _build(cls, model_class, *args, **kwargs): raise ValueError( "ListFactory %r does not support Meta.inline_args.", cls) - values = [v for k, v in sorted(kwargs.items())] + # When support for Python <3.6 is dropped sorting will no longer be required + # because dictionaries will already be ordered, this can then be changed to: + # values = kwargs.values() + values = [v for k, v in sorted(kwargs.items(), key=lambda item: int(item[0]))] return model_class(values) @classmethod diff --git a/tests/test_using.py b/tests/test_using.py index f3d8f034..14809223 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2725,6 +2725,15 @@ class Meta: o = TestObjectFactory() self.assertEqual([1], o.one) + def test_long_list(self): + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = factory.List(list(range(100))) + + o = TestObjectFactory() + self.assertEqual(list(range(100)), o.one) + def test_sequence_list(self): class TestObjectFactory(factory.Factory): class Meta: From a0c6debb4cb6088ff878129b1fb24ec1dd6d8bc2 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 25 Mar 2019 15:59:20 -0300 Subject: [PATCH 360/714] Fix string formatting in exception arguments --- factory/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/base.py b/factory/base.py index 92128885..145e518f 100644 --- a/factory/base.py +++ b/factory/base.py @@ -709,7 +709,7 @@ class Meta: def _build(cls, model_class, *args, **kwargs): if args: raise ValueError( - "DictFactory %r does not support Meta.inline_args.", cls) + "DictFactory %r does not support Meta.inline_args." % cls) return model_class(**kwargs) @classmethod @@ -731,7 +731,7 @@ class Meta: def _build(cls, model_class, *args, **kwargs): if args: raise ValueError( - "ListFactory %r does not support Meta.inline_args.", cls) + "ListFactory %r does not support Meta.inline_args." % cls) # When support for Python <3.6 is dropped sorting will no longer be required # because dictionaries will already be ordered, this can then be changed to: From c24709164cf29d2ad7b91eb54c517d992e233999 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 15 Feb 2019 00:53:30 -0300 Subject: [PATCH 361/714] Add support for getter argument in FuzzyChoice --- factory/fuzzy.py | 9 +++++++-- tests/test_fuzzy.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index dfd962c0..ccb95e4a 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -116,17 +116,22 @@ class FuzzyChoice(BaseFuzzyAttribute): Args: choices (iterable): An iterable yielding options; will only be unrolled on the first call. + getter (callable or None): a function to parse returned values """ - def __init__(self, choices, **kwargs): + def __init__(self, choices, getter=None, **kwargs): self.choices = None self.choices_generator = choices + self.getter = getter super(FuzzyChoice, self).__init__(**kwargs) def fuzz(self): if self.choices is None: self.choices = list(self.choices_generator) - return random.randgen.choice(self.choices) + value = random.randgen.choice(self.choices) + if self.getter is None: + return value + return self.getter(value) class FuzzyInteger(BaseFuzzyAttribute): diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 65426f20..10416c2d 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -76,6 +76,12 @@ def __iter__(self): self.assertIn(res, [1, 2, 3]) self.assertTrue(opts.unrolled) + def test_getter(self): + options = [('a', 1), ('b', 2), ('c', 3)] + d = fuzzy.FuzzyChoice(options, getter=lambda x: x[1]) + res = utils.evaluate_declaration(d) + self.assertIn(res, [1, 2, 3]) + class FuzzyIntegerTestCase(unittest.TestCase): def test_definition(self): From b3038de8843355ba9524e0fb6fda39a169bd0fb5 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 15 Feb 2019 01:04:17 -0300 Subject: [PATCH 362/714] Update changelog --- docs/changelog.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 09f4f83b..9cc90d54 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,8 +4,12 @@ ChangeLog 2.11.2 (unreleased) ------------------- -- Added support for Python 3.7 -- Added support for Django 2.1 +*New:* + + - Added support for Python 3.7 + - Added support for Django 2.1 + - Add :attr:`~factory.fuzzy.FuzzyChoice.getter` to :class:`~factory.fuzzy.FuzzyChoice` that mimics + the behavior of ``getter`` in :class:`~factory.Iterator` 2.11.1 (2018-05-05) From 35d25a86e6432e32737a68e170ad917c12b76fdb Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 25 Mar 2019 14:36:45 -0300 Subject: [PATCH 363/714] Add recipe for fuzzying Django model field choices --- docs/recipes.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index c0f98c71..d278bfac 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -536,6 +536,24 @@ In order to get a dict, we'll just have to swap the model; the easiest way is to >>> factory.build(dict, FACTORY_CLASS=UserFactory) {'first_name': "Agent 001", 'username': 'john_doe'} + +Fuzzying Django model field choices +----------------------------------- + +When defining a :class:`~factory.fuzzy.FuzzyChoice` you can reuse the same choice list from the model field descriptor. + +Use the ``getter`` kwarg to select the first element from each choice tuple. + +.. code-block:: python + + class UserFactory(factory.Factory): + class Meta: + model = User + + # CATEGORY_CHOICES is a list of (key, title) tuples + category = factory.fuzzy.FuzzyChoice(User.CATEGORY_CHOICES, getter=lambda c: c[0]) + + Django models with `GenericForeignKeys` --------------------------------------- From dd97510ba557a1c06f03eef0f2aa25bf1951a2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 4 Dec 2018 14:33:30 +0100 Subject: [PATCH 364/714] Avoid calling `_get_manager()` twice --- factory/django.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/factory/django.py b/factory/django.py index d88db7af..38173f3b 100644 --- a/factory/django.py +++ b/factory/django.py @@ -158,11 +158,10 @@ def _get_or_create(cls, model_class, *args, **kwargs): @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" - manager = cls._get_manager(model_class) - if cls._meta.django_get_or_create: return cls._get_or_create(model_class, *args, **kwargs) + manager = cls._get_manager(model_class) return manager.create(*args, **kwargs) @classmethod From ae780a38c2311cf6d306797889f09db4d395a6d4 Mon Sep 17 00:00:00 2001 From: Justin Crown Date: Mon, 20 Aug 2018 16:38:00 -0700 Subject: [PATCH 365/714] Allow renamed kwargs to be optional --- docs/changelog.rst | 4 ++++ factory/base.py | 3 ++- tests/test_using.py | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9cc90d54..e24ac6ec 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,10 @@ ChangeLog - Add :attr:`~factory.fuzzy.FuzzyChoice.getter` to :class:`~factory.fuzzy.FuzzyChoice` that mimics the behavior of ``getter`` in :class:`~factory.Iterator` +*Bugfix:* + + - Allow renamed arguments to be optional, thanks to `Justin Crown `_. + 2.11.1 (2018-05-05) ------------------- diff --git a/factory/base.py b/factory/base.py index 145e518f..3b13d317 100644 --- a/factory/base.py +++ b/factory/base.py @@ -295,7 +295,8 @@ def prepare_arguments(self, attributes): # 3. Rename fields for old_name, new_name in self.rename.items(): - kwargs[new_name] = kwargs.pop(old_name) + if old_name in kwargs: + kwargs[new_name] = kwargs.pop(old_name) # 4. Extract inline args args = tuple( diff --git a/tests/test_using.py b/tests/test_using.py index 14809223..02c8632a 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1138,6 +1138,22 @@ class Meta: obj = TestObjectFactory.build() self.assertEqual(42, obj.attributes) + def test_rename_non_existent_kwarg(self): + # see https://github.com/FactoryBoy/factory_boy/issues/504 + class TestObject(object): + def __init__(self, attributes=None): + self.attributes = attributes + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + rename = {'form_attributes': 'attributes'} + + try: + TestObjectFactory() + except KeyError: + self.fail('should not raise KeyError for missing renamed attributes') + class MaybeTestCase(unittest.TestCase): def test_simple_maybe(self): From 06697c212d22789c7a192eda9f9b0f778c95a76d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 30 Mar 2019 12:28:43 +0100 Subject: [PATCH 366/714] Restore tests for Python 2.7 Regression introduced in 239bdeac12fba5b0abb3599be3165d91343923ef. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 48bbaa80..71ce0390 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ testall: # DOC: Run tests for the currently installed version test: - python -Wdefault -m unittest + python -Wdefault -m unittest discover # DOC: Test the examples example-test: From 711e171ba10ae8b8c44e8568eb2320f39f2fbd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 30 Mar 2019 12:11:54 +0100 Subject: [PATCH 367/714] Skip factory_boy install for doc and lint CI builds These builds do not need the factory package, installing it is a waste of time and resources. The skip_install option was introduced with tox 1.9. --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index ab025d07..2e78d109 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +minversion = 1.9 envlist = py{27,34,35,36,37}-django111-alchemy12-mongoengine015, py{34,35,36,37,nightly}-django20-alchemy12-mongoengine015, @@ -29,6 +30,7 @@ commands = make test basepython = python3.7 deps = -rrequirements_dev.txt +skip_install = true whitelist_externals = make commands = make doc @@ -45,6 +47,7 @@ commands = make example-test [testenv:linkcheck] deps = -rrequirements_dev.txt +skip_install = true whitelist_externals = make commands = make linkcheck @@ -53,6 +56,7 @@ commands = make linkcheck deps = flake8 check_manifest +skip_install = true whitelist_externals = make commands = make lint From 3b504dccb12d747a4a9e4484bf3973ff3af8eef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 19 Jul 2018 15:51:32 +0200 Subject: [PATCH 368/714] Remove Sequence ``type`` argument Deprecated since 1.3.0, was scheduled to be removed in 2.0. --- docs/reference.rst | 7 +------ factory/declarations.py | 9 +++------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 7909c657..52ca2781 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -867,7 +867,7 @@ return value of the method: Sequence """""""" -.. class:: Sequence(lambda, type=int) +.. class:: Sequence(lambda) If a field should be unique, and thus different for all built instances, use a :class:`Sequence`. @@ -875,11 +875,6 @@ use a :class:`Sequence`. This declaration takes a single argument, a function accepting a single parameter - the current sequence counter - and returning the related value. - -.. note:: An extra kwarg argument, ``type``, may be provided. - This feature was deprecated in 1.3.0 and will be removed in 2.0.0. - - .. code-block:: python class UserFactory(factory.Factory) diff --git a/factory/declarations.py b/factory/declarations.py index fa18799b..844a5f00 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -212,17 +212,14 @@ class Sequence(BaseDeclaration): Attributes: function (function): A function, expecting the current sequence counter and returning the computed value. - type (function): A function converting an integer into the expected kind - of counter for the 'function' attribute. """ - def __init__(self, function, type=int): + def __init__(self, function): super(Sequence, self).__init__() self.function = function - self.type = type def evaluate(self, instance, step, extra): logger.debug("Sequence: Computing next value of %r for seq=%s", self.function, step.sequence) - return self.function(self.type(step.sequence)) + return self.function(int(step.sequence)) class LazyAttributeSequence(Sequence): @@ -238,7 +235,7 @@ def evaluate(self, instance, step, extra): logger.debug( "LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%s", self.function, step.sequence, utils.log_repr(instance)) - return self.function(instance, self.type(step.sequence)) + return self.function(instance, int(step.sequence)) class ContainerAttribute(BaseDeclaration): From b0a80fa9210d57068f3aa769b57d9244acbf6fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 30 Mar 2019 12:19:08 +0100 Subject: [PATCH 369/714] Run lint first on CI Gives quicker feedback if a change is not up to quality expectations. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2e78d109..e6714755 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] minversion = 1.9 envlist = + lint py{27,34,35,36,37}-django111-alchemy12-mongoengine015, py{34,35,36,37,nightly}-django20-alchemy12-mongoengine015, py{35,36,37,nightly}-django21-alchemy12-mongoengine015, @@ -9,7 +10,6 @@ envlist = docs examples linkcheck - lint toxworkdir = {env:TOX_WORKDIR:.tox} From e6412c3b872bcb5d3173c296f65c469d3a7e2363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 30 Mar 2019 12:08:14 +0100 Subject: [PATCH 370/714] Only install docs dependencies on travis doc builds Saves resources on travis, speed up CI builds. --- requirements_dev.txt | 4 +--- requirements_docs.txt | 2 ++ tox.ini | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 requirements_docs.txt diff --git a/requirements_dev.txt b/requirements_dev.txt index c6cccc56..8841bad0 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,5 @@ -e . +-r requirements_docs.txt -r requirements_test.txt -r examples/requirements.txt @@ -10,6 +11,3 @@ mongoengine wheel tox zest.releaser[recommended] - -Sphinx -sphinx_rtd_theme diff --git a/requirements_docs.txt b/requirements_docs.txt new file mode 100644 index 00000000..ab3f3dd4 --- /dev/null +++ b/requirements_docs.txt @@ -0,0 +1,2 @@ +Sphinx +sphinx_rtd_theme diff --git a/tox.ini b/tox.ini index e6714755..64de3d63 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ commands = make test [testenv:docs] basepython = python3.7 deps = - -rrequirements_dev.txt + -rrequirements_docs.txt skip_install = true whitelist_externals = make @@ -46,7 +46,7 @@ commands = make example-test [testenv:linkcheck] deps = - -rrequirements_dev.txt + -rrequirements_docs.txt skip_install = true whitelist_externals = make From 9501f9ed13bf3ca2952017f104dc178ae729efea Mon Sep 17 00:00:00 2001 From: Schwart Date: Fri, 25 May 2018 15:12:43 +0300 Subject: [PATCH 371/714] Make the extra_kwargs parameter for Faker.generate optional --- docs/changelog.rst | 1 + factory/faker.py | 6 +++--- tests/test_faker.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e24ac6ec..3a1744f1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog - Added support for Django 2.1 - Add :attr:`~factory.fuzzy.FuzzyChoice.getter` to :class:`~factory.fuzzy.FuzzyChoice` that mimics the behavior of ``getter`` in :class:`~factory.Iterator` + - Make the ``extra_kwargs`` parameter of :meth:`~factory.faker.Faker.generate` optional *Bugfix:* diff --git a/factory/faker.py b/factory/faker.py index 4ca3e301..f1847cfc 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -45,15 +45,15 @@ def __init__(self, provider, locale=None, **kwargs): self.provider_kwargs = kwargs self.locale = locale - def generate(self, extra_kwargs): + def generate(self, extra_kwargs=None): kwargs = {} kwargs.update(self.provider_kwargs) - kwargs.update(extra_kwargs) + kwargs.update(extra_kwargs or {}) subfaker = self._get_faker(self.locale) return subfaker.format(self.provider, **kwargs) def evaluate(self, instance, step, extra): - return self.generate(extra or {}) + return self.generate(extra) _FAKER_REGISTRY = {} _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE diff --git a/tests/test_faker.py b/tests/test_faker.py index 14f9fa63..202b8c9a 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -34,7 +34,7 @@ def _setup_mock_faker(self, locale=None, **definitions): def test_simple_biased(self): self._setup_mock_faker(name="John Doe") faker_field = factory.Faker('name') - self.assertEqual("John Doe", faker_field.generate({})) + self.assertEqual("John Doe", faker_field.generate()) def test_full_factory(self): class Profile(object): From c46c6fe2a4929e5b81f6e6572390f98bfaaeeff6 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 1 Apr 2019 16:32:53 -0300 Subject: [PATCH 372/714] Reword ChangeLog for consistency --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3a1744f1..be3356cf 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,8 +6,8 @@ ChangeLog *New:* - - Added support for Python 3.7 - - Added support for Django 2.1 + - Add support for Python 3.7 + - Add support for Django 2.1 - Add :attr:`~factory.fuzzy.FuzzyChoice.getter` to :class:`~factory.fuzzy.FuzzyChoice` that mimics the behavior of ``getter`` in :class:`~factory.Iterator` - Make the ``extra_kwargs`` parameter of :meth:`~factory.faker.Faker.generate` optional From d74b4ff65f78fbd14de5b576bfe30392b8ab799f Mon Sep 17 00:00:00 2001 From: Sean Harrington Date: Wed, 10 May 2017 18:24:00 -0400 Subject: [PATCH 373/714] Add RelatedFactoryList class for one-to-many support The RelatedFactoryList class allows us to define a declaration which returns a list of RelatedFactoryList(s). This is different than the current RelatedFactory, which only generates 1 related object when it's parent Factory is 'created'. This is useful for one-to-many relations. For instance, let's pretend we have a ``Student`` model which has *many* ``Exams``. We don't want to strictly generate *one* exam per student in our test suite, we want to cover the case of many exams per student. So, we'd tell the ``Student.exams`` declaration to be a ``RelatedFactoryList``, and could either tell it to generate ``length`` exams per student, or provide a ``lambda`` which would lazily get evaluated; this allows for a dynamic number of related objects being generated for each parent object. This lambda can be used to create deterministic counts of related objects per parent object, or to randomly generate "n" related objects, where "n" is a random int. (See added test coverage for the former case, and latter case). This has been brought up in a few StackOverflow posts. I wasn't satisfied so I opened this PR! http://stackoverflow.com/questions/27120685/one-to-many-relationships-in-factory-boy/ http://stackoverflow.com/questions/14589362/creating-multiple-objects-with-foreign-key/ --- docs/reference.rst | 53 +++++++++++++++++ factory/__init__.py | 1 + factory/declarations.py | 22 +++++++ tests/test_using.py | 129 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 205 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 52ca2781..30265031 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1562,6 +1562,7 @@ To support this pattern, factory_boy provides the following tools: - :class:`PostGeneration`: this class allows calling a given function with the generated object as argument - :func:`post_generation`: decorator performing the same functions as :class:`PostGeneration` - :class:`RelatedFactory`: this builds or creates a given factory *after* building/creating the first Factory. + - :class:`RelatedFactoryList`: this builds or creates a *list* of the given factory *after* building/creating the first Factory. Post-generation hooks are called in the same order they are declared in the factory class, so that functions can rely on the side effects applied by the previous post-generation hook. @@ -1720,6 +1721,58 @@ If a value is passed for the :class:`RelatedFactory` attribute, this disables main_lang=factory.SelfAttribute('..lang'), ) +RelatedFactoryList +"""""""""""""""""" + +.. class:: RelatedFactoryList(factory, factory_related_name='', size=2, **kwargs) + + .. OHAI_VIM** + + A :class:`RelatedFactoryList` behaves like a :class:`RelatedFactory`, only it returns a + list of factories. This is useful for simulating one-to-many relations, rather than the + one-to-one relation generated by :class:`RelatedFactory`. + + + .. attribute:: factory + + As for :class:`SubFactory`, the :attr:`factory` argument can be: + + - A :class:`Factory` subclass + - Or the fully qualified path to a :class:`Factory` subclass + (see :ref:`subfactory-circular` for details) + + .. attribute:: name + + The generated object (where the :class:`RelatedFactory` attribute will + set) may be passed to the related factories if the :attr:`factory_related_name` parameter + is set. + + It will be passed as a keyword argument, using the :attr:`name` value as + keyword: + + .. attribute:: size + + Either an ``int``, or a ``lambda`` that returns an ``int``, which will define the number + of related Factories to be generated for each parent object. + +.. note:: + Note that using a ``lambda`` for :attr:`size` allows the number of related objects per + parents object to vary. This is useful for testing, when you likely don't want your mock + data to have parent objects with the exact same, static number of related objects. + + .. code-block:: python + + LIST_SIZES = [1, 2, 3, 4, 5] + + class FooFactory(factory.Factory): + class Meta: + model = Foo + # Generate a list of `factory` objects of random size, ranging from 1 -> 5 + bar = factory.RelatedFactoryList(BarFactory, + size=lambda: LIST_SIZES[random.randint(0,5)]) + # Each Foo object will have exactly 3 Bar objects generated for its foobar attribute. + foobar = factory.RelatedFactoryList(BarFactory, size=3]) + PostGeneration """""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index 801ab2b2..1d3c87bd 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -41,6 +41,7 @@ PostGeneration, PostGenerationMethodCall, RelatedFactory, + RelatedFactoryList, ) from .helpers import ( diff --git a/factory/declarations.py b/factory/declarations.py index 844a5f00..11002335 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -672,6 +672,28 @@ def call(self, instance, step, context): return step.recurse(factory, passed_kwargs) +class RelatedFactoryList(RelatedFactory): + """Calls a factory 'size' times once the object has been generated. + + Attributes: + factory (Factory): the factory to call "size-times" + defaults (dict): extra declarations for calling the related factory + factory_related_name (str): the name to use to refer to the generated + object when calling the related factory + size (int|lambda): the number of times 'factory' is called, ultimately + returning a list of 'factory' objects w/ size 'size'. + """ + + def __init__(self, factory, factory_related_name='', size=2, **defaults): + self.size = size + super(RelatedFactoryList, self).__init__(factory, factory_related_name, **defaults) + + def call(self, instance, step, context): + return [super(RelatedFactoryList, self).call(instance, step, context) + for i in range(self.size if isinstance(self.size, int) + else self.size())] + + class NotProvided: pass diff --git a/tests/test_using.py b/tests/test_using.py index 02c8632a..448f443c 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2370,6 +2370,135 @@ def register(self, create, extracted, reference=0, **kwargs): book = BookFactory.build() self.assertEqual({0: book}, LIBRARY) + def test_related_factory_list_of_varying_size(self): + # Create our list of expected "related object counts" + related_list_sizes = [5, 5, 4, 4, 3, 3, 2, 2, 1, 1] + RELATED_LIST_SIZE = lambda: related_list_sizes.pop() + + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + # Mock out the 'List of Related Objects' generated by RelatedFactoryList + if hasattr(obj, 'related_list'): + obj.related_list.append(self) + else: + obj.related_list = [self] + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactoryList(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + # RELATED_LIST_SIZE is a lambda, this allows flexibility, as opposed + # to creating "n" related objects for every parent object... + three = factory.RelatedFactoryList(TestRelatedObjectFactoryList, + 'obj', + size=RELATED_LIST_SIZE) + # Create 5 TestObjectFactories: Each with 1, 2, ... 5 related objs + for related_list_size in reversed(related_list_sizes[1::2]): + obj = TestObjectFactory.build() + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was built + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + + for related_obj in obj.related_list: + self.assertEqual(1, related_obj.one) + self.assertEqual(2, related_obj.two) + # Each RelatedFactory in the RelatedFactoryList was passed the "parent" object + self.assertEqual(related_list_size, len(obj.related_list)) + # obj.related is the list of TestRelatedObject(s) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + obj = TestObjectFactory.build(three__one=3) + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was build + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + # three__one was correctly parse + for related_obj in obj.related_list: + self.assertEqual(3, related_obj.one) + self.assertEqual(4, related_obj.two) + # Each RelatedFactory in RelatedFactoryList received "parent" object + self.assertEqual(related_list_size, len(obj.related_list)) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + def test_related_factory_list_of_static_size(self): + RELATED_LIST_SIZE = 4 + + class TestRelatedObject(object): + def __init__(self, obj=None, one=None, two=None): + # Mock out the 'List of Related Objects' generated by RelatedFactoryList + if hasattr(obj, 'related_list'): + obj.related_list.append(self) + else: + obj.related_list = [self] + + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactoryList(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + three = factory.RelatedFactoryList(TestRelatedObjectFactoryList, 'obj', + size=RELATED_LIST_SIZE) + + obj = TestObjectFactory.build() + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was built + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + + for related_obj in obj.related_list: + self.assertEqual(1, related_obj.one) + self.assertEqual(2, related_obj.two) + # Each RelatedFactory in the RelatedFactoryList was passed the "parent" object + self.assertEqual(RELATED_LIST_SIZE, len(obj.related_list)) + # obj.related is the list of TestRelatedObject(s) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + obj = TestObjectFactory.build(three__one=3) + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was build + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + # three__one was correctly parse + for related_obj in obj.related_list: + self.assertEqual(3, related_obj.one) + self.assertEqual(4, related_obj.two) + # Each RelatedFactory in RelatedFactoryList received "parent" object + self.assertEqual(RELATED_LIST_SIZE, len(obj.related_list)) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + def test_related_factory(self): class TestRelatedObject(object): def __init__(self, obj=None, one=None, two=None): From 2481d411cde311bb7edd51b6dff3b345c2c14bc6 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 1 Apr 2019 16:59:38 -0300 Subject: [PATCH 374/714] Update ChangeLog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index be3356cf..61f2108c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Add :attr:`~factory.fuzzy.FuzzyChoice.getter` to :class:`~factory.fuzzy.FuzzyChoice` that mimics the behavior of ``getter`` in :class:`~factory.Iterator` - Make the ``extra_kwargs`` parameter of :meth:`~factory.faker.Faker.generate` optional + - Add :class:`~factory.RelatedFactoryList` class for one-to-many support, thanks `Sean Harrington `_. *Bugfix:* From 61444ed6c29974a65c4347e9d8d090f5c282e100 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 1 Apr 2019 23:37:20 -0300 Subject: [PATCH 375/714] Update contributors list --- CREDITS | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS b/CREDITS index ef4011f0..596b6e06 100644 --- a/CREDITS +++ b/CREDITS @@ -35,6 +35,7 @@ The project has received contributions from (in alphabetical order): * David Baumgold * Demur Nodia (https://github.com/demonno) * Eduard Iskandarov +* Federico Bond (https://github.com/federicobond) * Flavio Curella * François Freitag * George Hickman From 8337a873a043a4a1bd1b8f1c867900db6e9ea996 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Tue, 2 Apr 2019 11:58:11 -0300 Subject: [PATCH 376/714] Add Django 2.2 to test matrix --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 64de3d63..4afcfdd7 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py{27,34,35,36,37}-django111-alchemy12-mongoengine015, py{34,35,36,37,nightly}-django20-alchemy12-mongoengine015, py{35,36,37,nightly}-django21-alchemy12-mongoengine015, + py{35,36,37,nightly}-django22-alchemy12-mongoengine015, pypy-django{111}-alchemy12-mongoengine015, pypy3-django{111,20}-alchemy12-mongoengine015, docs @@ -19,7 +20,8 @@ deps = django111: Django>=1.11,<1.12 django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 - django{111,20,21}: Pillow + django22: Django>=2.2,<2.3 + django{111,20,21,22}: Pillow alchemy12: SQLAlchemy>=1.2,<1.3 mongoengine015: mongoengine>=0.15,<0.16 From a4c30d6529d3e613f5c2ac4cced7f985fda21041 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Tue, 2 Apr 2019 14:02:00 -0300 Subject: [PATCH 377/714] Fix test for Django 2.2 --- tests/test_django.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_django.py b/tests/test_django.py index 9a7827c3..32b05393 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -137,6 +137,8 @@ class Meta: class ModelTests(django_test.TestCase): + databases = {'default', 'replica'} + def test_unset_model(self): class UnsetModelFactory(factory.django.DjangoModelFactory): pass From 077174d1a21d702daed2fc9e77e4f46bc099ca0e Mon Sep 17 00:00:00 2001 From: YPCrumble Date: Thu, 2 Feb 2017 17:20:46 -0500 Subject: [PATCH 378/714] Pass initial keyword arguments to DjangoModelFactory._get_or_create to deal with possible IntegrityError. Fixes #239 --- docs/changelog.rst | 1 + factory/django.py | 22 +++++++++++++++++++++- tests/djapp/models.py | 5 +++++ tests/test_django.py | 21 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 61f2108c..d3cce2f0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,7 @@ ChangeLog *Bugfix:* - Allow renamed arguments to be optional, thanks to `Justin Crown `_. + - Fix `django_get_or_create` behavior when using multiple fields with `unique=True`, thanks to `@YPCrumble ` 2.11.1 (2018-05-05) diff --git a/factory/django.py b/factory/django.py index 38173f3b..7cb0e5c2 100644 --- a/factory/django.py +++ b/factory/django.py @@ -15,6 +15,7 @@ try: import django from django.core import files as django_files + from django.db import IntegrityError except ImportError as e: # pragma: no cover django = None django_files = None @@ -132,6 +133,13 @@ def _get_manager(cls, model_class): manager = manager.using(cls._meta.database) return manager + @classmethod + def _generate(cls, strategy, params): + # Original params are used in _get_or_create if it cannot build an + # object initially due to an IntegrityError being raised + cls._original_params = params + return super(DjangoModelFactory, cls)._generate(strategy, params) + @classmethod def _get_or_create(cls, model_class, *args, **kwargs): """Create an instance of the model through objects.get_or_create.""" @@ -152,7 +160,19 @@ def _get_or_create(cls, model_class, *args, **kwargs): key_fields[field] = kwargs.pop(field) key_fields['defaults'] = kwargs - instance, _created = manager.get_or_create(*args, **key_fields) + try: + instance, _created = manager.get_or_create(*args, **key_fields) + except IntegrityError: + try: + instance = manager.get(**cls._original_params) + except manager.model.DoesNotExist: + raise ValueError( + "django_get_or_create - Unable to create a new object " + "due an IntegrityError raised based on " + "your model's uniqueness constraints. " + "DoesNotExist: Unable to find an existing object based on " + "the fields specified in your factory instance.") + return instance @classmethod diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 7691284b..2585ff3b 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -32,6 +32,11 @@ class MultifieldModel(models.Model): text = models.CharField(max_length=20) +class MultifieldUniqueModel(models.Model): + slug = models.SlugField(max_length=20, unique=True) + text = models.CharField(max_length=20, unique=True) + + class AbstractBase(models.Model): foo = models.CharField(max_length=20) diff --git a/tests/test_django.py b/tests/test_django.py index 32b05393..f244a40a 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -136,6 +136,15 @@ class Meta: foo = factory.Sequence(lambda n: "foo%d" % n) +class WithMultipleGetOrCreateFieldsFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.MultifieldUniqueModel + django_get_or_create = ("slug", "text",) + + slug = factory.Sequence(lambda n: "slug%s" % n) + text = factory.Sequence(lambda n: "text%s" % n) + + class ModelTests(django_test.TestCase): databases = {'default', 'replica'} @@ -214,6 +223,18 @@ def test_multicall(self): self.assertEqual(2, len(set(objs))) self.assertEqual(2, models.MultifieldModel.objects.count()) + def test_multiple_get_or_create_fields_one_defined(self): + obj1 = WithMultipleGetOrCreateFieldsFactory() + obj2 = WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug) + self.assertEqual(obj1, obj2) + + def test_multiple_get_or_create_fields_both_defined(self): + obj1 = WithMultipleGetOrCreateFieldsFactory() + self.assertRaises( + ValueError, + lambda: WithMultipleGetOrCreateFieldsFactory( + slug=obj1.slug, text="alt")) + class DjangoPkForceTestCase(django_test.TestCase): def setUp(self): From 50b0de3ac198ea5d854cd96322435ecc89c599e0 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Tue, 2 Apr 2019 18:33:16 -0300 Subject: [PATCH 379/714] Use latest SQLAlchemy and Mongoengine versions in tests --- README.rst | 12 ++++-------- tox.ini | 16 ++++++++-------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index b8a385cf..1924ea18 100644 --- a/README.rst +++ b/README.rst @@ -375,15 +375,11 @@ To test with a specific framework version, you may use a ``tox`` target: .. code-block:: sh + # list all tox environments $ tox --listenvs - py27-django111-alchemy12-mongoengine015 - py27-django20-alchemy12-mongoengine015 - # ... - pypy3-django20-alchemy12-mongoengine015 - examples - lint - - $ tox -e py36-django20-alchemy12-mongoengine015 + + # run tests inside a specific environment + $ tox -e py36-django20-alchemy13-mongoengine017 Valid options are: diff --git a/tox.ini b/tox.ini index 4afcfdd7..75fe32ad 100644 --- a/tox.ini +++ b/tox.ini @@ -2,12 +2,12 @@ minversion = 1.9 envlist = lint - py{27,34,35,36,37}-django111-alchemy12-mongoengine015, - py{34,35,36,37,nightly}-django20-alchemy12-mongoengine015, - py{35,36,37,nightly}-django21-alchemy12-mongoengine015, - py{35,36,37,nightly}-django22-alchemy12-mongoengine015, - pypy-django{111}-alchemy12-mongoengine015, - pypy3-django{111,20}-alchemy12-mongoengine015, + py{27,34,35,36,37}-django111-alchemy13-mongoengine017, + py{34,35,36,37,nightly}-django20-alchemy13-mongoengine017, + py{35,36,37,nightly}-django21-alchemy13-mongoengine017, + py{35,36,37,nightly}-django22-alchemy13-mongoengine017, + pypy-django{111}-alchemy13-mongoengine017, + pypy3-django{111,20,21,22}-alchemy13-mongoengine017, docs examples linkcheck @@ -22,8 +22,8 @@ deps = django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 django{111,20,21,22}: Pillow - alchemy12: SQLAlchemy>=1.2,<1.3 - mongoengine015: mongoengine>=0.15,<0.16 + alchemy13: SQLAlchemy>=1.3,<1.4 + mongoengine017: mongoengine>=0.17,<0.18 whitelist_externals = make commands = make test From fdb49e7f9193f3ee7de1b260ddda7d55faf6c0dd Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 4 Apr 2019 12:13:15 -0300 Subject: [PATCH 380/714] docs: Update random source import location Fixes #404 --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 34a174ab..ab9c3d3b 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -362,6 +362,6 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its .. warning:: Custom :class:`BaseFuzzyAttribute` subclasses **MUST** - use :obj:`factory.fuzzy._random` as a randomness source; this ensures that + use :obj:`factory.random.randgen` as a randomness source; this ensures that data they generate can be regenerated using the simple state from :meth:`get_random_state`. From 541f6e8c6e7ea16fbff7907338e82afc17de3d95 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 4 Apr 2019 12:27:29 -0300 Subject: [PATCH 381/714] Make locale argument of factory.Faker keyword-only --- docs/changelog.rst | 1 + factory/faker.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index d3cce2f0..a553ed58 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,7 @@ ChangeLog the behavior of ``getter`` in :class:`~factory.Iterator` - Make the ``extra_kwargs`` parameter of :meth:`~factory.faker.Faker.generate` optional - Add :class:`~factory.RelatedFactoryList` class for one-to-many support, thanks `Sean Harrington `_. + - Make the `locale` argument for :class:`~factory.faker.Faker` keyword-only *Bugfix:* diff --git a/factory/faker.py b/factory/faker.py index f1847cfc..7bb2752c 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -39,11 +39,11 @@ class Faker(declarations.BaseDeclaration): Usage: >>> foo = factory.Faker('name') """ - def __init__(self, provider, locale=None, **kwargs): + def __init__(self, provider, **kwargs): super(Faker, self).__init__() self.provider = provider self.provider_kwargs = kwargs - self.locale = locale + self.locale = kwargs.pop('locale', None) def generate(self, extra_kwargs=None): kwargs = {} From edaa7c7f5a14065b229927903bd7989cc93cd069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ionu=C8=9B=20Cioc=C3=AErlan?= Date: Mon, 22 Apr 2019 18:15:29 +0300 Subject: [PATCH 382/714] update example models to match factories --- docs/examples.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 6f26b7ed..c8c5cb4e 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -13,9 +13,10 @@ First, let's define a couple of objects: .. code-block:: python class Account(object): - def __init__(self, username, email): + def __init__(self, username, email, date_joined): self.username = username self.email = email + self.date_joined = date_joined def __str__(self): return '%s (%s)' % (self.username, self.email) @@ -38,7 +39,7 @@ First, let's define a couple of objects: return u'%s %s (%s)' % ( unicode(self.firstname), unicode(self.lastname), - unicode(self.account.accountname), + unicode(self.account.username), ) Factories From 0fc6394e156246367bdee26b03ca94bfebb21545 Mon Sep 17 00:00:00 2001 From: Youssef Moussaoui Date: Tue, 30 Apr 2019 18:29:03 -0700 Subject: [PATCH 383/714] Fix GenericForeignKey typo in examples --- examples/django_demo/generic_foreignkey/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/django_demo/generic_foreignkey/models.py b/examples/django_demo/generic_foreignkey/models.py index efaa23dc..78fd427d 100644 --- a/examples/django_demo/generic_foreignkey/models.py +++ b/examples/django_demo/generic_foreignkey/models.py @@ -6,7 +6,7 @@ class TaggedItem(models.Model): - """Example GenericForeinKey model from django docs""" + """Example GenericForeignKey model from django docs""" tag = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() From 46c45d57396df40180e065f2156ddf3c4abb1b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 11 May 2019 16:38:36 +0200 Subject: [PATCH 384/714] Add ..versionadded on RelatedFactoryList. --- docs/reference.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 30265031..8ce22518 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1755,6 +1755,11 @@ RelatedFactoryList Either an ``int``, or a ``lambda`` that returns an ``int``, which will define the number of related Factories to be generated for each parent object. + .. versionadded:: 2.12 + + Note that the API for :class:`RelatedFactoryList` is considered experimental, and might change + in a future version for increased consistency with other declarations. + .. note:: Note that using a ``lambda`` for :attr:`size` allows the number of related objects per parents object to vary. This is useful for testing, when you likely don't want your mock From 06d50e45c32251e752f4588beccc6a7d60591634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 11 May 2019 16:39:30 +0200 Subject: [PATCH 385/714] Preparing release 2.12.0 --- docs/changelog.rst | 2 +- factory/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a553ed58..79137a8a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -2.11.2 (unreleased) +2.12.0 (2019-05-11) ------------------- *New:* diff --git a/factory/__init__.py b/factory/__init__.py index 1d3c87bd..c1b56974 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -75,7 +75,7 @@ from . import mongoengine -__version__ = '2.11.2.dev0' +__version__ = '2.12.0' __author__ = 'Raphaël Barrois ' From 20aae84dec3397079ae0699e4161ece59aec4574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 11 May 2019 16:39:47 +0200 Subject: [PATCH 386/714] Back to development: 2.12.1 --- docs/changelog.rst | 6 ++++++ factory/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 79137a8a..8e02f33e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +2.12.1 (unreleased) +------------------- + +- Nothing changed yet. + + 2.12.0 (2019-05-11) ------------------- diff --git a/factory/__init__.py b/factory/__init__.py index c1b56974..b4ea92b8 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -75,7 +75,7 @@ from . import mongoengine -__version__ = '2.12.0' +__version__ = '2.12.1.dev0' __author__ = 'Raphaël Barrois ' From 68e05a280fddebc7ab0aed34c253181d54678093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 16 May 2019 17:02:18 +0200 Subject: [PATCH 387/714] Remove trailing whitespace from docs/reference.rst --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 8ce22518..0c97a80a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1752,7 +1752,7 @@ RelatedFactoryList .. attribute:: size - Either an ``int``, or a ``lambda`` that returns an ``int``, which will define the number + Either an ``int``, or a ``lambda`` that returns an ``int``, which will define the number of related Factories to be generated for each parent object. .. versionadded:: 2.12 From d36eabe542f446ce38d17636b31f792ea89e8f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Mon, 8 Apr 2019 17:37:30 +0200 Subject: [PATCH 388/714] Bug test case --- tests/test_using.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/test_using.py b/tests/test_using.py index 448f443c..cee2b4ca 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1331,6 +1331,21 @@ class EvenObjectFactory(TestObjectFactory): dict(one=None, two=None, three=None, four=None, five=None), ) + def test_traits_override_params(self): + """Override a Params value in a trait""" + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + + one = factory.LazyAttribute(lambda o: o.zero + 1) + + class Params: + zero = 0 + plus_one = factory.Trait(zero=1) + + obj = TestObjectFactory(plus_one=True) + self.assertEqual(obj.one, 2) + def test_traits_override(self): """Override a trait in a subclass.""" class TestObjectFactory(factory.Factory): From 2706fcbdf11f958d2226463491955ed5b34741f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Mon, 8 Apr 2019 17:38:07 +0200 Subject: [PATCH 389/714] Touch creation counter in SimpleParameter.wrap Ensure the creation order is kept --- factory/declarations.py | 1 + factory/utils.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 11002335..18c54520 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -545,6 +545,7 @@ def as_declarations(self, field_name, declarations): def wrap(cls, value): if not isinstance(value, Parameter): return cls(value) + value.touch_creation_counter() return value diff --git a/factory/utils.py b/factory/utils.py index a3ad2a26..f2b7aa54 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -98,13 +98,16 @@ class OrderedBase(object): def __init__(self, **kwargs): super(OrderedBase, self).__init__(**kwargs) if type(self) is not OrderedBase: - bases = type(self).__mro__ - root = bases[bases.index(OrderedBase) - 1] - if not hasattr(root, self.CREATION_COUNTER_FIELD): - setattr(root, self.CREATION_COUNTER_FIELD, 0) - next_counter = getattr(self, self.CREATION_COUNTER_FIELD) - setattr(self, self.CREATION_COUNTER_FIELD, next_counter) - setattr(root, self.CREATION_COUNTER_FIELD, next_counter + 1) + self.touch_creation_counter() + + def touch_creation_counter(self): + bases = type(self).__mro__ + root = bases[bases.index(OrderedBase) - 1] + if not hasattr(root, self.CREATION_COUNTER_FIELD): + setattr(root, self.CREATION_COUNTER_FIELD, 0) + next_counter = getattr(root, self.CREATION_COUNTER_FIELD) + setattr(self, self.CREATION_COUNTER_FIELD, next_counter) + setattr(root, self.CREATION_COUNTER_FIELD, next_counter + 1) def sort_ordered_objects(items, getter=lambda x: x): From caf0a9d9ff3bfd61f4c9a5ed8de1fb042bafced4 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Thu, 4 Apr 2019 12:07:12 -0300 Subject: [PATCH 390/714] Remove Python nightly build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TravisCI’s version of nightly Python is not representative of Python next version. It has been Python 3.8.0a3+ for several months. --- .travis.yml | 4 ---- tox.ini | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index a46bad8a..e7889f3e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,8 +20,6 @@ matrix: - python: "pypy2.7-6.0" - python: "pypy3.5-6.0" - - python: "nightly" - # Documentation - python: "3.7" env: TOXENV=docs @@ -33,8 +31,6 @@ matrix: env: TOXENV=examples - python: "3.7" env: TOXENV=lint - allow_failures: - - python: "nightly" services: - mongodb diff --git a/tox.ini b/tox.ini index 75fe32ad..a97b70ee 100644 --- a/tox.ini +++ b/tox.ini @@ -3,9 +3,9 @@ minversion = 1.9 envlist = lint py{27,34,35,36,37}-django111-alchemy13-mongoengine017, - py{34,35,36,37,nightly}-django20-alchemy13-mongoengine017, - py{35,36,37,nightly}-django21-alchemy13-mongoengine017, - py{35,36,37,nightly}-django22-alchemy13-mongoengine017, + py{34,35,36,37}-django20-alchemy13-mongoengine017, + py{35,36,37}-django21-alchemy13-mongoengine017, + py{35,36,37}-django22-alchemy13-mongoengine017, pypy-django{111}-alchemy13-mongoengine017, pypy3-django{111,20,21,22}-alchemy13-mongoengine017, docs From 6df3e0f86f49a6507ca51aab66b5ca3535993922 Mon Sep 17 00:00:00 2001 From: PFStein Date: Fri, 26 Apr 2019 16:48:33 -0500 Subject: [PATCH 391/714] Fix issue with mute signals not respecting their decorated classes --- factory/django.py | 21 +++++++++++---------- tests/test_django.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/factory/django.py b/factory/django.py index 7cb0e5c2..d2a83360 100644 --- a/factory/django.py +++ b/factory/django.py @@ -327,16 +327,8 @@ def copy(self): def __call__(self, callable_obj): if isinstance(callable_obj, base.FactoryMetaClass): # Retrieve __func__, the *actual* callable object. - generate_method = callable_obj._generate.__func__ - - @classmethod - @functools.wraps(generate_method) - def wrapped_generate(*args, **kwargs): - # A mute_signals() object is not reentrant; use a copy every time. - with self.copy(): - return generate_method(*args, **kwargs) - - callable_obj._generate = wrapped_generate + callable_obj._create = self.wrap_method(callable_obj._create.__func__) + callable_obj._generate = self.wrap_method(callable_obj._generate.__func__) return callable_obj else: @@ -346,3 +338,12 @@ def wrapper(*args, **kwargs): with self.copy(): return callable_obj(*args, **kwargs) return wrapper + + def wrap_method(self, method): + @classmethod + @functools.wraps(method) + def wrapped_method(*args, **kwargs): + # A mute_signals() object is not reentrant; use a copy every time. + with self.copy(): + return method(*args, **kwargs) + return wrapped_method diff --git a/tests/test_django.py b/tests/test_django.py index f244a40a..acacd0fd 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -18,6 +18,7 @@ from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner # noqa: E402 from django.test import utils as django_test_utils # noqa: E402 from django.db.models import signals # noqa: E402 +from django.dispatch import receiver # noqa: E402 from .djapp import models # noqa: E402 try: from PIL import Image @@ -929,6 +930,43 @@ def generate(cls): self.assertSignalsReactivated() +class PreventChainedSignalsTestCase(django_test.TestCase): + + def test_class_decorator_with_muted_subfactory(self): + @receiver(signals.post_save, sender=models.PointedModel) + def boom(instance, created, raw, **kwargs): + raise Exception("BOOM!") + + @factory.django.mute_signals(signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointedModel + + class UndecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointerModel + pointed = factory.SubFactory(WithSignalsDecoratedFactory) + + UndecoratedFactory() + + def test_class_decorator_with_muted_related_factory(self): + @receiver(signals.post_save, sender=models.PointedModel) + def boom(instance, created, raw, **kwargs): + raise Exception("BOOM!") + + @factory.django.mute_signals(signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointedModel + + class UndecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointerModel + pointed = factory.RelatedFactory(WithSignalsDecoratedFactory) + + UndecoratedFactory() + + class DjangoCustomManagerTestCase(django_test.TestCase): def test_extra_args(self): From c126d6d96f6c0e18a4c0af44764f0d2f61f00e5e Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Fri, 17 May 2019 14:04:06 -0300 Subject: [PATCH 392/714] Update changelog --- docs/changelog.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8e02f33e..37cd6b52 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,10 @@ ChangeLog 2.12.1 (unreleased) ------------------- -- Nothing changed yet. +*Bugfix:* + + - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. + - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. 2.12.0 (2019-05-11) From 2cb616dc7b57bc4760d0dcd7218d2d69c1bc7180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 16 May 2019 16:57:55 +0200 Subject: [PATCH 393/714] Synchronize faker random seed with factory_boy on get_random_state `random.set_random_state` seeds both faker and factory_boy random generators. It would make sense that the state returned by `get_random_state()` represents both faker and factory_boy random state. Reviewed-by: Federico Bond --- docs/reference.rst | 1 + factory/random.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 0c97a80a..ccd0cc67 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -2106,6 +2106,7 @@ of :class:`random.Random`, which can be managed through the :mod:`factory.random .. method:: get_random_state() Call :meth:`get_random_state` to retrieve the random generator's current + state. This method synchronizes both Faker’s and `factory_boy`’s random state. The returned object is implementation-specific. diff --git a/factory/random.py b/factory/random.py index 8a6b0e88..fc07ae30 100644 --- a/factory/random.py +++ b/factory/random.py @@ -10,7 +10,10 @@ def get_random_state(): """Retrieve the state of factory.fuzzy's random generator.""" - return randgen.getstate() + state = randgen.getstate() + # Returned state must represent both Faker and factory_boy. + faker.generator.random.setstate(state) + return state def set_random_state(state): From 6c85e784ba53a26fc6e682f93a88f9cffdb2e87a Mon Sep 17 00:00:00 2001 From: PFStein Date: Mon, 20 May 2019 13:36:52 -0500 Subject: [PATCH 394/714] Updated chained signal test case to explicit asserts --- tests/test_django.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index acacd0fd..392fe6c0 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -18,7 +18,6 @@ from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner # noqa: E402 from django.test import utils as django_test_utils # noqa: E402 from django.db.models import signals # noqa: E402 -from django.dispatch import receiver # noqa: E402 from .djapp import models # noqa: E402 try: from PIL import Image @@ -932,39 +931,35 @@ def generate(cls): class PreventChainedSignalsTestCase(django_test.TestCase): - def test_class_decorator_with_muted_subfactory(self): - @receiver(signals.post_save, sender=models.PointedModel) - def boom(instance, created, raw, **kwargs): - raise Exception("BOOM!") + def setUp(self): + self.post_save_mock = mock.Mock(side_effect=Exception('BOOM!')) + signals.post_save.connect(self.post_save_mock, models.PointedModel) - @factory.django.mute_signals(signals.post_save) - class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.PointedModel + def tearDown(self): + signals.post_save.disconnect(self.post_save_mock, models.PointedModel) + + @factory.django.mute_signals(signals.post_save) + class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.PointedModel + def test_class_decorator_with_muted_subfactory(self): class UndecoratedFactory(factory.django.DjangoModelFactory): class Meta: model = models.PointerModel - pointed = factory.SubFactory(WithSignalsDecoratedFactory) + pointed = factory.SubFactory(self.WithSignalsDecoratedFactory) UndecoratedFactory() + self.post_save_mock.assert_not_called() def test_class_decorator_with_muted_related_factory(self): - @receiver(signals.post_save, sender=models.PointedModel) - def boom(instance, created, raw, **kwargs): - raise Exception("BOOM!") - - @factory.django.mute_signals(signals.post_save) - class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): - class Meta: - model = models.PointedModel - class UndecoratedFactory(factory.django.DjangoModelFactory): class Meta: model = models.PointerModel - pointed = factory.RelatedFactory(WithSignalsDecoratedFactory) + pointed = factory.RelatedFactory(self.WithSignalsDecoratedFactory) UndecoratedFactory() + self.post_save_mock.assert_not_called() class DjangoCustomManagerTestCase(django_test.TestCase): From fae055112df531315b161e5771e9b892e57220f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 25 May 2019 10:57:13 +0200 Subject: [PATCH 395/714] Use assertRaises as a context manager Test instructions read easier under the context manager. --- tests/test_base.py | 27 ++++++++----- tests/test_declarations.py | 24 ++++++++---- tests/test_django.py | 31 +++++++-------- tests/test_fuzzy.py | 77 ++++++++++---------------------------- tests/test_using.py | 6 ++- tests/test_utils.py | 9 +++-- 6 files changed, 78 insertions(+), 96 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 963aaa37..892e213f 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -45,7 +45,8 @@ class TestModel(FakeDjangoModel): class SafetyTestCase(unittest.TestCase): def test_base_factory(self): - self.assertRaises(errors.FactoryError, base.BaseFactory) + with self.assertRaises(errors.FactoryError): + base.BaseFactory() class AbstractFactoryTestCase(unittest.TestCase): @@ -70,8 +71,10 @@ def test_abstract_factory_cannot_be_called(self): class TestObjectFactory(base.Factory): pass - self.assertRaises(errors.FactoryError, TestObjectFactory.build) - self.assertRaises(errors.FactoryError, TestObjectFactory.create) + with self.assertRaises(errors.FactoryError): + TestObjectFactory.build() + with self.assertRaises(errors.FactoryError): + TestObjectFactory.create() def test_abstract_factory_not_inherited(self): """abstract=True isn't propagated to child classes.""" @@ -92,8 +95,10 @@ class Meta: abstract = False model = None - self.assertRaises(errors.FactoryError, TestObjectFactory.build) - self.assertRaises(errors.FactoryError, TestObjectFactory.create) + with self.assertRaises(errors.FactoryError): + TestObjectFactory.build() + with self.assertRaises(errors.FactoryError): + TestObjectFactory.create() class OptionsTests(unittest.TestCase): @@ -246,7 +251,8 @@ class Meta: one = declarations.LazyAttribute(lambda a: a.does_not_exist) - self.assertRaises(AttributeError, TestObjectFactory) + with self.assertRaises(AttributeError): + TestObjectFactory() def test_inheritance_with_sequence(self): """Tests that sequence IDs are shared between parent and son.""" @@ -308,7 +314,8 @@ def test_reset_sequence_subclass_fails(self): class SubTestObjectFactory(self.TestObjectFactory): pass - self.assertRaises(ValueError, SubTestObjectFactory.reset_sequence) + with self.assertRaises(ValueError): + SubTestObjectFactory.reset_sequence() def test_reset_sequence_subclass_force(self): """Tests that reset_sequence(force=True) works.""" @@ -403,7 +410,8 @@ class Meta: one = 'one' - self.assertRaises(base.Factory.UnknownStrategy, TestModelFactory) + with self.assertRaises(base.Factory.UnknownStrategy): + TestModelFactory() def test_stub_with_create_strategy(self): class TestModelFactory(base.StubFactory): @@ -414,7 +422,8 @@ class Meta: TestModelFactory._meta.strategy = enums.CREATE_STRATEGY - self.assertRaises(base.StubFactory.UnsupportedStrategy, TestModelFactory) + with self.assertRaises(base.StubFactory.UnsupportedStrategy): + TestModelFactory() def test_stub_with_build_strategy(self): class TestModelFactory(base.StubFactory): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 66c9955d..3fdc2846 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -31,11 +31,14 @@ def test_chaining(self): obj.a.b.c = self.MyObj(4) self.assertEqual(2, declarations.deepgetattr(obj, 'a').n) - self.assertRaises(AttributeError, declarations.deepgetattr, obj, 'b') + with self.assertRaises(AttributeError): + declarations.deepgetattr(obj, 'b') self.assertEqual(2, declarations.deepgetattr(obj, 'a.n')) self.assertEqual(3, declarations.deepgetattr(obj, 'a.c', 3)) - self.assertRaises(AttributeError, declarations.deepgetattr, obj, 'a.c.n') - self.assertRaises(AttributeError, declarations.deepgetattr, obj, 'a.d') + with self.assertRaises(AttributeError): + declarations.deepgetattr(obj, 'a.c.n') + with self.assertRaises(AttributeError): + declarations.deepgetattr(obj, 'a.d') self.assertEqual(3, declarations.deepgetattr(obj, 'a.b').n) self.assertEqual(3, declarations.deepgetattr(obj, 'a.b.n')) self.assertEqual(4, declarations.deepgetattr(obj, 'a.b.c').n) @@ -87,7 +90,8 @@ def test_no_cycling(self): it = declarations.Iterator([1, 2], cycle=False) self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) - self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) + with self.assertRaises(StopIteration): + utils.evaluate_declaration(it, force_sequence=2) def test_initial_reset(self): it = declarations.Iterator([1, 2]) @@ -108,11 +112,13 @@ def test_reset_no_cycling(self): it = declarations.Iterator([1, 2], cycle=False) self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) - self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) + with self.assertRaises(StopIteration): + utils.evaluate_declaration(it, force_sequence=2) it.reset() self.assertEqual(1, utils.evaluate_declaration(it, force_sequence=0)) self.assertEqual(2, utils.evaluate_declaration(it, force_sequence=1)) - self.assertRaises(StopIteration, utils.evaluate_declaration, it, force_sequence=2) + with self.assertRaises(StopIteration): + utils.evaluate_declaration(it, force_sequence=2) def test_getter(self): it = declarations.Iterator([(1, 2), (1, 3)], getter=lambda p: p[1]) @@ -165,8 +171,10 @@ def foo(*args, **kwargs): class FactoryWrapperTestCase(unittest.TestCase): def test_invalid_path(self): - self.assertRaises(ValueError, declarations._FactoryWrapper, 'UnqualifiedSymbol') - self.assertRaises(ValueError, declarations._FactoryWrapper, 42) + with self.assertRaises(ValueError): + declarations._FactoryWrapper('UnqualifiedSymbol') + with self.assertRaises(ValueError): + declarations._FactoryWrapper(42) def test_class(self): w = declarations._FactoryWrapper(datetime.date) diff --git a/tests/test_django.py b/tests/test_django.py index 392fe6c0..b0f07546 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -152,7 +152,8 @@ def test_unset_model(self): class UnsetModelFactory(factory.django.DjangoModelFactory): pass - self.assertRaises(factory.FactoryError, UnsetModelFactory.create) + with self.assertRaises(factory.FactoryError): + UnsetModelFactory.create() def test_cross_database(self): class OtherDBFactory(factory.django.DjangoModelFactory): @@ -230,10 +231,8 @@ def test_multiple_get_or_create_fields_one_defined(self): def test_multiple_get_or_create_fields_both_defined(self): obj1 = WithMultipleGetOrCreateFieldsFactory() - self.assertRaises( - ValueError, - lambda: WithMultipleGetOrCreateFieldsFactory( - slug=obj1.slug, text="alt")) + with self.assertRaises(ValueError): + WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug, text="alt") class DjangoPkForceTestCase(django_test.TestCase): @@ -544,12 +543,11 @@ def test_with_path_empty_file(self): self.assertEqual('django/example.data', o.afile.name) def test_error_both_file_and_path(self): - self.assertRaises( - ValueError, - WithFileFactory.build, - afile__from_file='fakefile', - afile__from_path=testdata.TESTFILE_PATH, - ) + with self.assertRaises(ValueError): + WithFileFactory.build( + afile__from_file='fakefile', + afile__from_path=testdata.TESTFILE_PATH, + ) def test_override_filename_with_path(self): o = WithFileFactory.build( @@ -754,12 +752,11 @@ def test_with_path_empty_file(self): 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, - ) + with 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( diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 10416c2d..5b6675ef 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -287,20 +287,13 @@ def test_partial_definition(self): self.assertLessEqual(res, self.jan3) def test_invalid_definition(self): - self.assertRaises( - ValueError, - fuzzy.FuzzyDate, - self.jan31, - self.jan1, - ) + with self.assertRaises(ValueError): + fuzzy.FuzzyDate(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, - ) + with self.assertRaises(ValueError): + fuzzy.FuzzyDate(self.jan31) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -354,21 +347,13 @@ def test_partial_definition(self): 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, - ) + with 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), - ) + with 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) @@ -420,20 +405,13 @@ def test_force_microsecond(self): self.assertEqual(4, res.microsecond) def test_invalid_definition(self): - self.assertRaises( - ValueError, - fuzzy.FuzzyNaiveDateTime, - self.jan31, - self.jan1, - ) + with 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, - ) + with self.assertRaises(ValueError): + fuzzy.FuzzyNaiveDateTime(self.jan31) def test_biased(self): """Tests a FuzzyDate with a biased random.randint.""" @@ -486,38 +464,23 @@ def test_partial_definition(self): self.assertLessEqual(res, self.jan3) def test_invalid_definition(self): - self.assertRaises( - ValueError, - fuzzy.FuzzyDateTime, - self.jan31, - self.jan1, - ) + with 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, - ) + with 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, - ) + with 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), - ) + with 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) diff --git a/tests/test_using.py b/tests/test_using.py index cee2b4ca..40a419f7 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1892,7 +1892,8 @@ class Meta: self.assertEqual(1, obj.descendant.sample_int) self.assertEqual(1, obj.descendant.container_len) - self.assertRaises(TypeError, TestModelFactory.build) + with self.assertRaises(TypeError): + TestModelFactory.build() def test_function_container_attribute(self): class TestModel2(FakeModel): @@ -1950,7 +1951,8 @@ class Meta: # Scope bleeding: j will end up in TestObjectFactory's scope. - self.assertRaises(TypeError, TestObjectFactory.build) + with self.assertRaises(TypeError): + TestObjectFactory.build() @utils.disable_warnings def test_iterator_list_comprehension_protected(self): diff --git a/tests/test_utils.py b/tests/test_utils.py index 43ba1222..f10ec0a7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -19,10 +19,12 @@ def test_datetime(self): self.assertEqual(d, imported) def test_unknown_attribute(self): - self.assertRaises(AttributeError, utils.import_object, 'datetime', 'foo') + with self.assertRaises(AttributeError): + utils.import_object('datetime', 'foo') def test_invalid_module(self): - self.assertRaises(ImportError, utils.import_object, 'this-is-an-invalid-module', '__name__') + with self.assertRaises(ImportError): + utils.import_object('this-is-an-invalid-module', '__name__') class LogPPrintTestCase(unittest.TestCase): @@ -129,7 +131,8 @@ def test_reset_after_end(self): self.assertEqual(1, next(iterator)) self.assertEqual(2, next(iterator)) self.assertEqual(3, next(iterator)) - self.assertRaises(StopIteration, next, iterator) + with self.assertRaises(StopIteration): + next(iterator) i.reset() # Previous iter() has stopped From 06177c0548ccdf410329c0d4676d216a116cb406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 25 May 2019 11:50:55 +0200 Subject: [PATCH 396/714] Specify ORM support policy Tests are only run against the supported versions of `Django`, latest `mongoengine` and latest `SQLAlchemy`. Explicit support policy and adjust Travis to follow it. --- README.rst | 7 +++++++ tox.ini | 16 ++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 1924ea18..2bf15900 100644 --- a/README.rst +++ b/README.rst @@ -347,6 +347,13 @@ This will yield messages similar to those (artificial indentation): LazyStub: Computed values, got tests.test_using.TestModel2Factory(two=) BaseFactory: Generating tests.test_using.TestModel2Factory(two=) +Support Policy +-------------- + +- **Django**'s [supported + versions](https://www.djangoproject.com/download/#supported-versions). +- **SQLAlchemy**: [latest version on PyPI](https://pypi.org/project/SQLAlchemy/). +- **mongoengine**: [latest version on PyPI](https://pypi.org/project/mongoengine/). Contributing ------------ diff --git a/tox.ini b/tox.ini index a97b70ee..6d7a4e18 100644 --- a/tox.ini +++ b/tox.ini @@ -2,12 +2,12 @@ minversion = 1.9 envlist = lint - py{27,34,35,36,37}-django111-alchemy13-mongoengine017, - py{34,35,36,37}-django20-alchemy13-mongoengine017, - py{35,36,37}-django21-alchemy13-mongoengine017, - py{35,36,37}-django22-alchemy13-mongoengine017, - pypy-django{111}-alchemy13-mongoengine017, - pypy3-django{111,20,21,22}-alchemy13-mongoengine017, + py{27,34,35,36,37}-django111-alchemy-mongoengine, + py{34,35,36,37}-django20-alchemy-mongoengine, + py{35,36,37}-django21-alchemy-mongoengine, + py{35,36,37}-django22-alchemy-mongoengine, + pypy-django{111}-alchemy-mongoengine, + pypy3-django{111,20,21,22}-alchemy-mongoengine, docs examples linkcheck @@ -22,8 +22,8 @@ deps = django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 django{111,20,21,22}: Pillow - alchemy13: SQLAlchemy>=1.3,<1.4 - mongoengine017: mongoengine>=0.17,<0.18 + alchemy: SQLAlchemy + mongoengine: mongoengine whitelist_externals = make commands = make test From 55fbaa71fa85ea65313a07866e6563e4dffe28c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 26 May 2019 21:39:51 +0200 Subject: [PATCH 397/714] Drop support for Python 3.4 and Django 2.0 Both are unmaintained since April 2019. --- .travis.yml | 1 - README.rst | 2 +- docs/changelog.rst | 4 ++++ setup.py | 4 +--- tox.ini | 8 +++----- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index e7889f3e..c99f535e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ install: matrix: include: - python: "2.7" - - python: "3.4" - python: "3.5" - python: "3.6" - python: "3.7" diff --git a/README.rst b/README.rst index 2bf15900..9157c6fa 100644 --- a/README.rst +++ b/README.rst @@ -80,7 +80,7 @@ Links * Package: https://pypi.org/project/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.4 to 3.7, as well as PyPy 2.7 and 5.8. +factory_boy supports Python 2.7, 3.5 to 3.7, as well as PyPy 2.7 and 5.8. Download diff --git a/docs/changelog.rst b/docs/changelog.rst index 37cd6b52..63ac5f00 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,10 @@ ChangeLog - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. +*Removed:* + - Drop support for Python 3.4. This version [is not maintained anymore](https://www.python.org/downloads/release/python-3410/). + - Drop support for Django 2.0. This version [is not maintained anymore](https://www.djangoproject.com/download/#supported-versions). + 2.12.0 (2019-05-11) ------------------- diff --git a/setup.py b/setup.py index 07fb60a4..d0f91e13 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ def get_version(package_name): packages=['factory'], zip_safe=False, license='MIT', - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", install_requires=[ 'Faker>=0.7.0', ], @@ -53,7 +53,6 @@ def get_version(package_name): "Development Status :: 5 - Production/Stable", "Framework :: Django", "Framework :: Django :: 1.11", - "Framework :: Django :: 2.0", "Framework :: Django :: 2.1", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", @@ -62,7 +61,6 @@ def get_version(package_name): "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", diff --git a/tox.ini b/tox.ini index 6d7a4e18..a0d3f7a2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,12 +2,11 @@ minversion = 1.9 envlist = lint - py{27,34,35,36,37}-django111-alchemy-mongoengine, - py{34,35,36,37}-django20-alchemy-mongoengine, + py{27,35,36,37}-django111-alchemy-mongoengine, py{35,36,37}-django21-alchemy-mongoengine, py{35,36,37}-django22-alchemy-mongoengine, pypy-django{111}-alchemy-mongoengine, - pypy3-django{111,20,21,22}-alchemy-mongoengine, + pypy3-django{111,21,22}-alchemy-mongoengine, docs examples linkcheck @@ -18,10 +17,9 @@ toxworkdir = {env:TOX_WORKDIR:.tox} deps = -rrequirements_test.txt django111: Django>=1.11,<1.12 - django20: Django>=2.0,<2.1 django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 - django{111,20,21,22}: Pillow + django{111,21,22}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 28cbc6635a74a85776e6724df5b2e4456174ec74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 26 May 2019 21:45:56 +0200 Subject: [PATCH 398/714] Add Django 2.2 to trove classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d0f91e13..48b4a63d 100755 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ def get_version(package_name): "Framework :: Django", "Framework :: Django :: 1.11", "Framework :: Django :: 2.1", + "Framework :: Django :: 2.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", From 4bb2c58a21232324ff12bd2a5cd0ed5d2cd5ccb2 Mon Sep 17 00:00:00 2001 From: Jules Robichaud-Gagnon Date: Fri, 25 May 2018 23:09:39 -0400 Subject: [PATCH 399/714] Add test with RelatedFactory and factory.Trait --- tests/test_django.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_django.py b/tests/test_django.py index b0f07546..68809cc8 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -409,11 +409,18 @@ class PointerExtraFactory(PointerFactory): class PointedRelatedExtraFactory(PointedRelatedFactory): pointer__bar = 'extra_new_bar' + class PointedRelatedWithTraitFactory(PointedFactory): + class Params: + with_pointer = factory.Trait( + pointer=factory.RelatedFactory(PointerFactory, 'pointed', bar='with_trait') + ) + cls.PointedFactory = PointedFactory cls.PointerFactory = PointerFactory cls.PointedRelatedFactory = PointedRelatedFactory cls.PointerExtraFactory = PointerExtraFactory cls.PointedRelatedExtraFactory = PointedRelatedExtraFactory + cls.PointedRelatedWithTraitFactory = PointedRelatedWithTraitFactory def test_create_pointed(self): pointed = self.PointedFactory() @@ -460,6 +467,15 @@ def test_create_pointed_related_extra(self): self.assertEqual(pointed.pointer, models.PointerModel.objects.get()) self.assertEqual(pointed.pointer.bar, 'extra_new_bar') + def test_create_pointed_related_with_trait(self): + pointed = self.PointedRelatedWithTraitFactory( + with_pointer=True + ) + self.assertEqual(pointed, models.PointedModel.objects.get()) + self.assertEqual(pointed.foo, 'foo') + self.assertEqual(pointed.pointer, models.PointerModel.objects.get()) + self.assertEqual(pointed.pointer.bar, 'with_trait') + class DjangoFileFieldTestCase(django_test.TestCase): From d19173478df0e3daaef61c3621ca991276a7913b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 25 May 2019 11:15:59 +0200 Subject: [PATCH 400/714] Move tests for get_or_create_multiple to their own test case --- tests/test_django.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 68809cc8..a7daac2e 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -224,12 +224,14 @@ def test_multicall(self): self.assertEqual(2, len(set(objs))) self.assertEqual(2, models.MultifieldModel.objects.count()) - def test_multiple_get_or_create_fields_one_defined(self): + +class MultipleGetOrCreateFieldsTest(django_test.TestCase): + def test_one_defined(self): obj1 = WithMultipleGetOrCreateFieldsFactory() obj2 = WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug) self.assertEqual(obj1, obj2) - def test_multiple_get_or_create_fields_both_defined(self): + def test_both_defined(self): obj1 = WithMultipleGetOrCreateFieldsFactory() with self.assertRaises(ValueError): WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug, text="alt") From e440d2ea511ae50c4c150e3909b915f6954e49bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 25 May 2019 11:16:53 +0200 Subject: [PATCH 401/714] django_get_or_create: Limit get_or_create to specified fields Fixes #598 Refs #239 --- docs/changelog.rst | 1 + factory/django.py | 28 ++++++++++++++++++---------- tests/djapp/models.py | 1 + tests/test_django.py | 5 +++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 63ac5f00..9cd78746 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ ChangeLog - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. + - :issue:`598`: Limit ``get_or_create`` behavior to fields specified in ``django_get_or_create``. *Removed:* - Drop support for Python 3.4. This version [is not maintained anymore](https://www.python.org/downloads/release/python-3410/). diff --git a/factory/django.py b/factory/django.py index d2a83360..061a34a4 100644 --- a/factory/django.py +++ b/factory/django.py @@ -162,16 +162,24 @@ def _get_or_create(cls, model_class, *args, **kwargs): try: instance, _created = manager.get_or_create(*args, **key_fields) - except IntegrityError: - try: - instance = manager.get(**cls._original_params) - except manager.model.DoesNotExist: - raise ValueError( - "django_get_or_create - Unable to create a new object " - "due an IntegrityError raised based on " - "your model's uniqueness constraints. " - "DoesNotExist: Unable to find an existing object based on " - "the fields specified in your factory instance.") + except IntegrityError as e: + get_or_create_params = { + lookup: value + for lookup, value in cls._original_params.items() + if lookup in cls._meta.django_get_or_create + } + if get_or_create_params: + try: + instance = manager.get(**get_or_create_params) + except manager.model.DoesNotExist: + raise ValueError( + "django_get_or_create - Unable to create a new object " + "due an IntegrityError raised based on " + "your model's uniqueness constraints. " + "DoesNotExist: Unable to find an existing object based on " + "the fields specified in your factory instance.") + else: + raise e return instance diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 2585ff3b..11d24bf5 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -35,6 +35,7 @@ class MultifieldModel(models.Model): class MultifieldUniqueModel(models.Model): slug = models.SlugField(max_length=20, unique=True) text = models.CharField(max_length=20, unique=True) + title = models.CharField(max_length=20, unique=True) class AbstractBase(models.Model): diff --git a/tests/test_django.py b/tests/test_django.py index a7daac2e..22804a4f 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -236,6 +236,11 @@ def test_both_defined(self): with self.assertRaises(ValueError): WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug, text="alt") + def test_unique_field_not_in_get_or_create(self): + WithMultipleGetOrCreateFieldsFactory(title="Title") + with self.assertRaises(django.db.IntegrityError): + WithMultipleGetOrCreateFieldsFactory(title="Title") + class DjangoPkForceTestCase(django_test.TestCase): def setUp(self): From 58c25dacf3665f34d717b1d00fa045d4d82f65f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 26 May 2019 16:12:49 +0200 Subject: [PATCH 402/714] Fix remaining warnings in test suite Refs #365 --- factory/django.py | 8 ++++---- tests/test_django.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/factory/django.py b/factory/django.py index 061a34a4..bec29faa 100644 --- a/factory/django.py +++ b/factory/django.py @@ -225,8 +225,8 @@ def _make_content(self, params): if params.get('from_path'): path = params['from_path'] - f = open(path, 'rb') - content = django_files.File(f, name=path) + with open(path, 'rb') as f: + content = django_files.base.ContentFile(f.read()) elif params.get('from_file'): f = params['from_file'] @@ -275,9 +275,9 @@ def _make_data(self, params): color = params.get('color', 'blue') image_format = params.get('format', 'JPEG') - thumb = Image.new('RGB', (width, height), color) thumb_io = io.BytesIO() - thumb.save(thumb_io, format=image_format) + with Image.new('RGB', (width, height), color) as thumb: + thumb.save(thumb_io, format=image_format) return thumb_io.getvalue() diff --git a/tests/test_django.py b/tests/test_django.py index 22804a4f..8ed8ffb7 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -707,8 +707,8 @@ def test_with_content(self): self.assertEqual(13, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) - i = Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name)) - colors = i.getcolors() + with Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name)) as i: + colors = i.getcolors() # 169 pixels with rgb(254, 0, 0) self.assertEqual([(169, (254, 0, 0))], colors) self.assertEqual('JPEG', i.format) @@ -722,8 +722,8 @@ def test_gif(self): self.assertEqual(13, o.animage.height) self.assertEqual('django/example.jpg', o.animage.name) - i = Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name)) - colors = i.convert('RGB').getcolors() + with Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name)) as i: + colors = i.convert('RGB').getcolors() # 169 pixels with rgb(0, 0, 255) self.assertEqual([(169, (0, 0, 255))], colors) self.assertEqual('GIF', i.format) @@ -798,9 +798,10 @@ def test_existing_file(self): o1 = WithImageFactory.build(animage__from_path=testdata.TESTIMAGE_PATH) o1.save() - o2 = WithImageFactory.build(animage__from_file=o1.animage) - self.assertIsNone(o2.pk) - o2.save() + with o1.animage as f: + o2 = WithImageFactory.build(animage__from_file=f) + self.assertIsNone(o2.pk) + o2.save() with o2.animage as f: # Image file for a 42x42 green jpeg: 301 bytes long. From 9e4fc846cd0c89e4256aca6f83323536ac2ddb66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 26 May 2019 18:24:04 +0200 Subject: [PATCH 403/714] Turn warnings into errors during tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecation warnings about importing ABCs from the 'collections' module instead of 'collections.abc' are globally ignored. AFAICT there’s no way to ignore them only for external dependencies. They’ll turn into errors from Python 3.8 anyway. Fixes #365 --- Makefile | 13 ++++++++++++- tests/test_alchemy.py | 3 +++ tests/test_fuzzy.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 71ce0390..8935fde0 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,18 @@ testall: # DOC: Run tests for the currently installed version test: - python -Wdefault -m unittest discover + # imp warning is a PendingDeprecationWarning for Python 3.4 and Python 3.5 + # and a DeprecationWarning for later versions. + # Change PendingDeprecationWarning to distutils modules when dropping + # support for Python 3.4. + python \ + -b \ + -Werror \ + -Wdefault:"'U' mode is deprecated":DeprecationWarning:site: \ + -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ + -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":PendingDeprecationWarning:: \ + -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ + -m unittest discover # DOC: Test the examples example-test: diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index edfa1104..db9f2c8c 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -128,6 +128,9 @@ class Meta: def test_force_flush_deprecation(self): with warnings.catch_warnings(record=True) as warning_list: + # Do not turn expected warning into an error. + warnings.filterwarnings("default", category=DeprecationWarning, module=r"tests\.test_alchemy") + class OutdatedPersistenceFactory(StandardFactory): class Meta: force_flush = True diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 5b6675ef..1cae6230 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -606,6 +606,8 @@ def test_seeding(self): def test_seeding_warning(self): with warnings.catch_warnings(record=True) as w: + # Do not turn expected warning into an error. + warnings.filterwarnings("default", category=UserWarning, module=r"tests\.test_fuzzy") fuzz = fuzzy.FuzzyDate(datetime.date(2013, 1, 1)) utils.evaluate_declaration(fuzz) self.assertEqual(1, len(w)) From 04e4d0a721107254d46ff27eca36a50db54ce0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 5 Jun 2019 22:03:22 +0200 Subject: [PATCH 404/714] Drop if __name__ == '__main__' from tests Tests are using unittest autodiscover feature, making these entrypoints useless and confusing. --- examples/flask_alchemy/runtests.sh | 2 +- examples/flask_alchemy/test_demoapp.py | 3 --- tests/alter_time.py | 4 ---- tests/test_base.py | 4 ---- tests/test_declarations.py | 4 ---- tests/test_django.py | 4 ---- tests/test_using.py | 4 ---- 7 files changed, 1 insertion(+), 24 deletions(-) diff --git a/examples/flask_alchemy/runtests.sh b/examples/flask_alchemy/runtests.sh index f1a6b5d8..24fedf2d 100755 --- a/examples/flask_alchemy/runtests.sh +++ b/examples/flask_alchemy/runtests.sh @@ -2,5 +2,5 @@ cd $(dirname $0) for f in test_*.py; do - python "$f"; + python -m unittest discover done diff --git a/examples/flask_alchemy/test_demoapp.py b/examples/flask_alchemy/test_demoapp.py index b485a924..2f6b47bf 100644 --- a/examples/flask_alchemy/test_demoapp.py +++ b/examples/flask_alchemy/test_demoapp.py @@ -30,6 +30,3 @@ def test_userlog_factory(self): self.assertIsNotNone(userlog.user.id) self.assertEqual(1, len(demoapp.User.query.all())) self.assertEqual(1, len(demoapp.UserLog.query.all())) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/alter_time.py b/tests/alter_time.py index d08e79f6..69d8381c 100644 --- a/tests/alter_time.py +++ b/tests/alter_time.py @@ -109,7 +109,3 @@ def main(): # pragma: no cover print("- today ->", datetime.date.today()) print("- isinstance(now, date) ->", isinstance(datetime.date.today(), datetime.date)) print("- isinstance(target, date) ->", isinstance(target_date, datetime.date)) - - -if __name__ == '__main__': # pragma: no cover - main() diff --git a/tests/test_base.py b/tests/test_base.py index 892e213f..cb5c4bcb 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -542,7 +542,3 @@ class Meta: self.assertIn('foo', TestObjectFactory._meta.post_declarations.as_dict()) self.assertIn('foo__bar', TestObjectFactory._meta.post_declarations.as_dict()) - - -if __name__ == '__main__': # pragma: no cover - unittest.main() diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 3fdc2846..7da7fc99 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -304,7 +304,3 @@ def aa(*args, **kwargs): # Test generation happens in desired order Ordered() self.assertEqual(postgen_results, ['a1', 'zz', 'aa']) - - -if __name__ == '__main__': # pragma: no cover - unittest.main() diff --git a/tests/test_django.py b/tests/test_django.py index 8ed8ffb7..dfbdeaa3 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -997,7 +997,3 @@ class Meta: # Our CustomManager will remove the 'arg=' argument, # invalid for the actual model. ObjFactory.create(arg='invalid') - - -if __name__ == '__main__': # pragma: no cover - unittest.main() diff --git a/tests/test_using.py b/tests/test_using.py index 40a419f7..7a1417aa 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2967,7 +2967,3 @@ class Meta: 1, ], ], o.two) - - -if __name__ == '__main__': # pragma: no cover - unittest.main() From af3525bf174d0774b61464f9cc8ab8441babc7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 5 Jun 2019 22:08:01 +0200 Subject: [PATCH 405/714] Remove useless imports from flask alchemy demo --- examples/flask_alchemy/test_demoapp.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/flask_alchemy/test_demoapp.py b/examples/flask_alchemy/test_demoapp.py index 2f6b47bf..04658c2f 100644 --- a/examples/flask_alchemy/test_demoapp.py +++ b/examples/flask_alchemy/test_demoapp.py @@ -1,10 +1,9 @@ -import os import unittest -import tempfile import demoapp import demoapp_factories + class DemoAppTestCase(unittest.TestCase): def setUp(self): From c32a135442b45f3b60a327c53734d40a8ee1dc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 6 Jun 2019 09:55:36 +0200 Subject: [PATCH 406/714] Sort imports with isort --- Makefile | 2 + docs/conf.py | 3 +- .../generic_foreignkey/factories.py | 5 +- .../migrations/0001_initial.py | 2 +- .../django_demo/generic_foreignkey/models.py | 4 +- .../django_demo/generic_foreignkey/tests.py | 4 +- examples/flask_alchemy/demoapp_factories.py | 3 +- factory/__init__.py | 73 +++++++------------ factory/alchemy.py | 3 +- factory/base.py | 6 +- factory/builder.py | 6 +- factory/declarations.py | 6 +- factory/django.py | 15 ++-- factory/faker.py | 3 +- factory/fuzzy.py | 4 +- factory/helpers.py | 3 +- factory/mongoengine.py | 1 - factory/random.py | 3 +- requirements_dev.txt | 1 + setup.cfg | 9 +++ tests/alter_time.py | 2 +- tests/djapp/models.py | 7 +- tests/test_alchemy.py | 7 +- tests/test_base.py | 5 +- tests/test_declarations.py | 7 +- tests/test_django.py | 28 +++---- tests/test_fuzzy.py | 6 +- tests/test_mongoengine.py | 5 +- tests/test_using.py | 2 +- tox.ini | 1 + 30 files changed, 96 insertions(+), 130 deletions(-) diff --git a/Makefile b/Makefile index 8935fde0..a10c4f8a 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ SETUP_PY=setup.py # Use current python binary instead of system default. COVERAGE = python $(shell which coverage) FLAKE8 = flake8 +ISORT = isort all: default @@ -75,6 +76,7 @@ example-test: lint: $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py + $(ISORT) --recursive --check-only --diff $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) check-manifest coverage: diff --git a/docs/conf.py b/docs/conf.py index 64629229..8cef794a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/examples/django_demo/generic_foreignkey/factories.py b/examples/django_demo/generic_foreignkey/factories.py index 725bc0fc..688dd557 100644 --- a/examples/django_demo/generic_foreignkey/factories.py +++ b/examples/django_demo/generic_foreignkey/factories.py @@ -1,7 +1,8 @@ -import factory -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType +import factory + from .models import TaggedItem diff --git a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py index dbf0b0b7..9e55d8f9 100644 --- a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py +++ b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py @@ -2,8 +2,8 @@ # Generated by Django 1.10 on 2017-04-06 14:33 from __future__ import unicode_literals -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/examples/django_demo/generic_foreignkey/models.py b/examples/django_demo/generic_foreignkey/models.py index 78fd427d..49e87a7c 100644 --- a/examples/django_demo/generic_foreignkey/models.py +++ b/examples/django_demo/generic_foreignkey/models.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals -from django.db import models -from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models class TaggedItem(models.Model): diff --git a/examples/django_demo/generic_foreignkey/tests.py b/examples/django_demo/generic_foreignkey/tests.py index 430200ad..c1950a24 100644 --- a/examples/django_demo/generic_foreignkey/tests.py +++ b/examples/django_demo/generic_foreignkey/tests.py @@ -1,8 +1,8 @@ -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from .factories import UserFactory, GroupFactory, TaggedUserFactory, TaggedGroupFactory +from .factories import GroupFactory, TaggedGroupFactory, TaggedUserFactory, UserFactory class GenericFactoryTest(TestCase): diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py index f32f8c39..9f56d6d0 100644 --- a/examples/flask_alchemy/demoapp_factories.py +++ b/examples/flask_alchemy/demoapp_factories.py @@ -1,8 +1,7 @@ +import demoapp import factory import factory.fuzzy -import demoapp - class BaseFactory(factory.alchemy.SQLAlchemyModelFactory): class Meta: diff --git a/factory/__init__.py b/factory/__init__.py index b4ea92b8..fcaccca5 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,80 +1,59 @@ # -*- coding: utf-8 -*- # Copyright: See the LICENSE file. +# Backward compatibility; this should be removed soon. +from . import alchemy, django, mogo, mongoengine from .base import ( - Factory, BaseDictFactory, - DictFactory, BaseListFactory, + DictFactory, + Factory, ListFactory, StubFactory, - use_strategy, ) - -from .enums import ( - BUILD_STRATEGY, - CREATE_STRATEGY, - STUB_STRATEGY, -) - - -from .errors import ( - FactoryError, -) - -from .faker import Faker - from .declarations import ( - LazyFunction, - LazyAttribute, - Iterator, - Sequence, - LazyAttributeSequence, - SelfAttribute, - Trait, ContainerAttribute, - SubFactory, Dict, + Iterator, + LazyAttribute, + LazyAttributeSequence, + LazyFunction, List, Maybe, PostGeneration, PostGenerationMethodCall, RelatedFactory, RelatedFactoryList, + SelfAttribute, + Sequence, + SubFactory, + Trait, ) - +from .enums import BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY +from .errors import FactoryError +from .faker import Faker from .helpers import ( - debug, - build, - create, - stub, - generate, - simple_generate, - make_factory, - build_batch, + container_attribute, + create, create_batch, - stub_batch, + debug, + generate, generate_batch, - simple_generate_batch, - - lazy_attribute, iterator, - sequence, + lazy_attribute, lazy_attribute_sequence, - container_attribute, + make_factory, post_generation, + sequence, + simple_generate, + simple_generate_batch, + stub, + stub_batch, ) -# Backward compatibility; this should be removed soon. -from . import alchemy -from . import django -from . import mogo -from . import mongoengine - - __version__ = '2.12.1.dev0' __author__ = 'Raphaël Barrois ' diff --git a/factory/alchemy.py b/factory/alchemy.py index d3e7ba66..e12c4cd4 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -3,9 +3,10 @@ from __future__ import unicode_literals -from . import base import warnings +from . import base + SESSION_PERSISTENCE_COMMIT = 'commit' SESSION_PERSISTENCE_FLUSH = 'flush' VALID_SESSION_PERSISTENCE_TYPES = [ diff --git a/factory/base.py b/factory/base.py index 3b13d317..da7aa554 100644 --- a/factory/base.py +++ b/factory/base.py @@ -7,11 +7,7 @@ import logging import warnings -from . import builder -from . import declarations -from . import enums -from . import errors -from . import utils +from . import builder, declarations, enums, errors, utils logger = logging.getLogger('factory.generate') diff --git a/factory/builder.py b/factory/builder.py index 189b7d63..a4798bd7 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -2,11 +2,7 @@ import collections -from . import declarations -from . import enums -from . import errors -from . import utils - +from . import declarations, enums, errors, utils DeclarationWithContext = collections.namedtuple( 'DeclarationWithContext', diff --git a/factory/declarations.py b/factory/declarations.py index 18c54520..1cbeb8c5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -6,11 +6,7 @@ import itertools import logging -from . import enums -from . import compat -from . import errors -from . import utils - +from . import compat, enums, errors, utils logger = logging.getLogger('factory.generate') diff --git a/factory/django.py b/factory/django.py index bec29faa..050de195 100644 --- a/factory/django.py +++ b/factory/django.py @@ -4,13 +4,15 @@ """factory_boy extensions for use with the Django framework.""" -from __future__ import absolute_import -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals +import functools import io -import os import logging -import functools +import os + +from . import base, declarations, errors +from .compat import is_string try: import django @@ -22,11 +24,6 @@ import_failure = e -from . import base -from . import declarations -from . import errors -from .compat import is_string - logger = logging.getLogger('factory.generate') diff --git a/factory/faker.py b/factory/faker.py index 7bb2752c..76ee6a18 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -14,8 +14,7 @@ class Meta: """ -from __future__ import absolute_import -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals import contextlib diff --git a/factory/fuzzy.py b/factory/fuzzy.py index ccb95e4a..1442fe33 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -11,9 +11,7 @@ import string import warnings -from . import compat -from . import declarations -from . import random +from . import compat, declarations, random random_seed_warning = ( "Setting a specific random seed for {} can still have varying results " diff --git a/factory/helpers.py b/factory/helpers.py index 15e84711..01866ec8 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -7,8 +7,7 @@ import contextlib import logging -from . import base -from . import declarations +from . import base, declarations @contextlib.contextmanager diff --git a/factory/mongoengine.py b/factory/mongoengine.py index ce430bf7..10cc1deb 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -7,7 +7,6 @@ from __future__ import unicode_literals - from . import base diff --git a/factory/random.py b/factory/random.py index fc07ae30..75a546a3 100644 --- a/factory/random.py +++ b/factory/random.py @@ -1,8 +1,9 @@ from __future__ import absolute_import -import faker.generator import random +import faker.generator + randgen = random.Random() randgen.state_set = False diff --git a/requirements_dev.txt b/requirements_dev.txt index 8841bad0..ba60c746 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,6 +5,7 @@ coverage Django +isort Pillow SQLAlchemy mongoengine diff --git a/setup.cfg b/setup.cfg index 6954392d..dddea03f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,3 +16,12 @@ python-file-with-version = factory/__init__.py [distutils] index-servers = pypi + +[isort] +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +line_length = 88 +known_first_party = factory +known_third_party = django,faker,mongoengine diff --git a/tests/alter_time.py b/tests/alter_time.py index 69d8381c..9cf391b1 100644 --- a/tests/alter_time.py +++ b/tests/alter_time.py @@ -7,8 +7,8 @@ from __future__ import print_function import datetime -from .compat import mock +from .compat import mock real_datetime_class = datetime.datetime diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 11d24bf5..3a60d0da 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- # Copyright: See the LICENSE file. - """Helpers for testing django apps.""" import os.path +from django.conf import settings +from django.db import models + try: from PIL import Image except ImportError: @@ -14,9 +16,6 @@ except ImportError: Image = None -from django.conf import settings -from django.db import models - class StandardModel(models.Model): foo = models.CharField(max_length=20) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index db9f2c8c..72544e84 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -3,13 +3,14 @@ """Tests for factory_boy/SQLAlchemy interactions.""" -import factory -from .compat import mock -import warnings import unittest +import warnings +import factory from factory.alchemy import SQLAlchemyModelFactory + from .alchemyapp import models +from .compat import mock class StandardFactory(SQLAlchemyModelFactory): diff --git a/tests/test_base.py b/tests/test_base.py index cb5c4bcb..09dd1e6b 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -3,10 +3,7 @@ import unittest -from factory import base -from factory import declarations -from factory import enums -from factory import errors +from factory import base, declarations, enums, errors class TestObject(object): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 7da7fc99..889080ab 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -4,13 +4,10 @@ import datetime import unittest -from factory import base -from factory import declarations -from factory import errors -from factory import helpers +from factory import base, declarations, errors, helpers -from .compat import mock from . import utils +from .compat import mock class OrderedDeclarationTestCase(unittest.TestCase): diff --git a/tests/test_django.py b/tests/test_django.py index dfbdeaa3..bc6c63af 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -8,20 +8,20 @@ import unittest import django +from django import test as django_test +from django.conf import settings +from django.db.models import signals +from django.test import utils as django_test_utils +from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner -# Setup Django as soon as possible -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') -django.setup() +import factory + +from . import testdata +from .compat import mock -from django import test as django_test # noqa: E402 -from django.conf import settings # noqa: E402 -from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner # noqa: E402 -from django.test import utils as django_test_utils # noqa: E402 -from django.db.models import signals # noqa: E402 -from .djapp import models # noqa: E402 try: from PIL import Image -except ImportError: # pragma: no cover +except ImportError: # Try PIL alternate name try: import Image @@ -29,11 +29,11 @@ # OK, not installed Image = None -import factory # noqa: E402 - -from . import testdata # noqa: E402 -from .compat import mock # noqa: E402 +# Setup Django before importing Django models. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') +django.setup() +from .djapp import models # noqa:E402 isort:skip test_state = {} diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index 1cae6230..d6344a46 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -7,12 +7,10 @@ import unittest import warnings -from factory import compat -from factory import fuzzy -from factory import random +from factory import compat, fuzzy, random -from .compat import mock from . import utils +from .compat import mock class FuzzyAttributeTestCase(unittest.TestCase): diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index d4333838..b82a84af 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -3,13 +3,12 @@ """Tests for factory_boy/MongoEngine interactions.""" -import unittest - -import factory import os +import unittest import mongoengine +import factory from factory.mongoengine import MongoEngineFactory diff --git a/tests/test_using.py b/tests/test_using.py index 7a1417aa..2899e1cc 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -13,8 +13,8 @@ import factory from factory import errors -from .compat import is_python2 from . import utils +from .compat import is_python2 class TestObject(object): diff --git a/tox.ini b/tox.ini index a0d3f7a2..14190fc5 100644 --- a/tox.ini +++ b/tox.ini @@ -55,6 +55,7 @@ commands = make linkcheck [testenv:lint] deps = flake8 + isort check_manifest skip_install = true From d670d0625500aeb26120d28494d91b4f07f4b014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 6 Jun 2019 08:41:44 +0200 Subject: [PATCH 407/714] Lint examples with flake8 --- Makefile | 2 +- .../generic_foreignkey/migrations/0001_initial.py | 8 +++++++- examples/django_demo/manage.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a10c4f8a..e0b74083 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ example-test: # Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude # DOC: Perform code quality tasks lint: - $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) + $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py $(ISORT) --recursive --check-only --diff $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) check-manifest diff --git a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py index 9e55d8f9..93af4287 100644 --- a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py +++ b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py @@ -21,7 +21,13 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('tag', models.SlugField()), ('object_id', models.PositiveIntegerField()), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ( + 'content_type', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='contenttypes.ContentType' + ) + ), ], ), ] diff --git a/examples/django_demo/manage.py b/examples/django_demo/manage.py index bc7ced9a..bd1a003b 100755 --- a/examples/django_demo/manage.py +++ b/examples/django_demo/manage.py @@ -11,7 +11,7 @@ # issue is really that Django is missing to avoid masking other # exceptions on Python 2. try: - import django + import django # noqa: F401 except ImportError: raise ImportError( "Couldn't import Django. Are you sure it's installed and " From b197e3a5d859bbf329fdcf034ad7ab8b8a45d750 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 5 Jun 2019 21:26:40 +0200 Subject: [PATCH 408/714] Re-raise IntegrityError when django_get_or_create fails When: * several fields are specified in `django_get_or_create`, and * user specifies a subset of kwargs, and * other kwargs listed in `django_get_or_create` have values generated by the factory, a `get_or_create` lookup may trigger an `IntegrityError`. Indeed, a generated value that was not part of the original lookup is passed to `get_or_create()`, causing a failed `get()` and leading to a `create()` with more than user specified kwargs and triggering an `IntegrityError`. The `IntegrityError` is handled by factory_boy, and the model is looked up based on user specified kwargs. When the lookup fails, factory_boy raised a `ValueError`. Re-raise the original `IntegrityError` instead to align behavior with Django. Fixes #606 Refs #345 --- docs/changelog.rst | 1 + factory/django.py | 9 +++------ tests/test_django.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9cd78746..6cdc92a8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,6 +9,7 @@ ChangeLog - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. - :issue:`598`: Limit ``get_or_create`` behavior to fields specified in ``django_get_or_create``. + - :issue:`606`: Re-raise :class:`~django.db.IntegrityError` when `django_get_or_create` with multiple fields fails to lookup model using user provided keyword arguments. *Removed:* - Drop support for Python 3.4. This version [is not maintained anymore](https://www.python.org/downloads/release/python-3410/). diff --git a/factory/django.py b/factory/django.py index 050de195..da41c3da 100644 --- a/factory/django.py +++ b/factory/django.py @@ -169,12 +169,9 @@ def _get_or_create(cls, model_class, *args, **kwargs): try: instance = manager.get(**get_or_create_params) except manager.model.DoesNotExist: - raise ValueError( - "django_get_or_create - Unable to create a new object " - "due an IntegrityError raised based on " - "your model's uniqueness constraints. " - "DoesNotExist: Unable to find an existing object based on " - "the fields specified in your factory instance.") + # Original params are not a valid lookup and triggered a create(), + # that resulted in an IntegrityError. Follow Django’s behavior. + raise e else: raise e diff --git a/tests/test_django.py b/tests/test_django.py index bc6c63af..850f8980 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -233,7 +233,7 @@ def test_one_defined(self): def test_both_defined(self): obj1 = WithMultipleGetOrCreateFieldsFactory() - with self.assertRaises(ValueError): + with self.assertRaises(django.db.IntegrityError): WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug, text="alt") def test_unique_field_not_in_get_or_create(self): From 88ca848eeafbf6f956e340fa4745adf8fcb7be79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 11 Jun 2019 10:08:48 +0200 Subject: [PATCH 409/714] README: Move supported Python version under support policy --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9157c6fa..7297b728 100644 --- a/README.rst +++ b/README.rst @@ -80,8 +80,6 @@ Links * Package: https://pypi.org/project/factory_boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy -factory_boy supports Python 2.7, 3.5 to 3.7, as well as PyPy 2.7 and 5.8. - Download -------- @@ -350,6 +348,8 @@ This will yield messages similar to those (artificial indentation): Support Policy -------------- +``factory_boy`` supports Python 2.7, 3.5 to 3.7, as well as PyPy 2.7 and 5.8. + - **Django**'s [supported versions](https://www.djangoproject.com/download/#supported-versions). - **SQLAlchemy**: [latest version on PyPI](https://pypi.org/project/SQLAlchemy/). From fc4f7aed84ce044f0a30e7e33511651f74b7d021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 12 Jun 2019 14:54:17 +0200 Subject: [PATCH 410/714] Update release-related PHONY make targets --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e0b74083..499d9fb7 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ release: fullrelease -.PHONY: clean update release-patch release-minor release-major +.PHONY: clean update release # Tests and quality From bc84ff8bfd1b0e745fb6401c06edc7478bbf90b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 13 Jun 2019 10:07:07 +0200 Subject: [PATCH 411/714] Remove double connection to mongodb Fixes travis error: ``` Traceback (most recent call last): File "/home/travis/build/FactoryBoy/factory_boy/tests/test_mongoengine.py", line 68, in setUp mongoengine.connect('factory_boy_test') File "/home/travis/build/FactoryBoy/factory_boy/.tox/py27-django111-alchemy-mongoengine/lib/python2.7/site-packages/mongoengine/connection.py", line 363, in connect 'A different connection with alias `%s` was already registered. Use disconnect() first' % alias) MongoEngineConnectionError: A different connection with alias `default` was already registered. Use disconnect() first ``` --- tests/test_mongoengine.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index b82a84af..2b4cf2c7 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -64,9 +64,6 @@ def setUpClass(cls): def tearDownClass(cls): cls.db.drop_database(cls.db_name) - def setUp(self): - mongoengine.connect('factory_boy_test') - def test_build(self): std = PersonFactory.build() self.assertEqual('name0', std.name) From f6dcad0d0fc0fb80e1b9e12bec63a6dd9be74219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 10 Jun 2019 16:51:18 +0200 Subject: [PATCH 412/714] Using tuple literals in PostGenerationMethodCall Faster instantiation and more readable. --- factory/declarations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 1cbeb8c5..bdb44ab1 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -722,11 +722,11 @@ def __init__(self, method_name, *args, **kwargs): def call(self, instance, step, context): if not context.value_provided: if self.method_arg is NotProvided: - args = tuple() + args = () else: - args = tuple([self.method_arg]) + args = (self.method_arg,) else: - args = tuple([context.value]) + args = (context.value,) kwargs = dict(self.method_kwargs) kwargs.update(context.extra) From 0584080cb0baf4c8894e6934e79b1d4d28723736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 10 Jun 2019 19:55:28 +0200 Subject: [PATCH 413/714] =?UTF-8?q?Simplify=20LazyFunction=E2=80=99s=20doc?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index ccd0cc67..4a204d78 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -752,9 +752,8 @@ LazyFunction The :class:`LazyFunction` is the simplest case where the value of an attribute does not depend on the object being built. -It takes as argument a method to call (function, lambda...); that method should -not take any argument, though keyword arguments are safe but unused, -and return a value. +It takes as argument a function to call; that should not take any arguments and +return a value. .. code-block:: python From d136b185de5f641aca3078adce7be74b9d333868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 5 Jun 2019 22:12:49 +0200 Subject: [PATCH 414/714] Verify database entries in Django get_or_create tests Be more specific about expected objects in the database. --- tests/test_django.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 850f8980..a26bfc2c 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -209,7 +209,14 @@ def test_simple_call(self): MultifieldModelFactory(slug='alt') self.assertEqual(obj1, obj2) - self.assertEqual(2, models.MultifieldModel.objects.count()) + self.assertEqual( + list( + models.MultifieldModel.objects.order_by("slug").values_list( + "slug", flat=True + ) + ), + ["alt", "slug1"], + ) def test_missing_arg(self): with self.assertRaises(factory.FactoryError): @@ -222,7 +229,14 @@ def test_multicall(self): ) self.assertEqual(6, len(objs)) self.assertEqual(2, len(set(objs))) - self.assertEqual(2, models.MultifieldModel.objects.count()) + self.assertEqual( + list( + models.MultifieldModel.objects.order_by("slug").values_list( + "slug", flat=True + ) + ), + ["alt", "main"], + ) class MultipleGetOrCreateFieldsTest(django_test.TestCase): From 4d2aca14723126adf47f1482c3c9f2f0e662debe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 21 Jun 2019 18:24:59 +0200 Subject: [PATCH 415/714] =?UTF-8?q?Move=20=E2=80=9CDebugging=20factory=5Fb?= =?UTF-8?q?oy=E2=80=9D=20section=20next=20to=20=E2=80=9CContributing?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encourage users investigating factory_boy behavior to contribute fixes. --- README.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 7297b728..a7777977 100644 --- a/README.rst +++ b/README.rst @@ -312,8 +312,19 @@ factory_boy has specific support for a few ORMs, through specific ``factory.Fact * SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` + +Support Policy +-------------- + +``factory_boy`` supports Python 2.7, 3.5 to 3.7, as well as PyPy 2.7 and 5.8. + +- **Django**'s [supported + versions](https://www.djangoproject.com/download/#supported-versions). +- **SQLAlchemy**: [latest version on PyPI](https://pypi.org/project/SQLAlchemy/). +- **mongoengine**: [latest version on PyPI](https://pypi.org/project/mongoengine/). + 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. @@ -345,16 +356,6 @@ This will yield messages similar to those (artificial indentation): LazyStub: Computed values, got tests.test_using.TestModel2Factory(two=) BaseFactory: Generating tests.test_using.TestModel2Factory(two=) -Support Policy --------------- - -``factory_boy`` supports Python 2.7, 3.5 to 3.7, as well as PyPy 2.7 and 5.8. - -- **Django**'s [supported - versions](https://www.djangoproject.com/download/#supported-versions). -- **SQLAlchemy**: [latest version on PyPI](https://pypi.org/project/SQLAlchemy/). -- **mongoengine**: [latest version on PyPI](https://pypi.org/project/mongoengine/). - Contributing ------------ From 2bbe97bbd2c5a444ff4457cbcaea46911429c1f9 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Mon, 24 Jun 2019 18:31:28 -0300 Subject: [PATCH 416/714] Fix Maybe constructor TypeError masked by __repr__ AttributeError Closes #630 --- factory/declarations.py | 17 +++++++++-------- tests/test_declarations.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index bdb44ab1..18c1fd51 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -444,6 +444,15 @@ def __bool__(self): class Maybe(BaseDeclaration): def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): super(Maybe, self).__init__() + + if enums.get_builder_phase(decider) is None: + # No builder phase => flat value + decider = SelfAttribute(decider, default=None) + + self.decider = decider + self.yes = yes_declaration + self.no = no_declaration + phases = { 'yes_declaration': enums.get_builder_phase(yes_declaration), 'no_declaration': enums.get_builder_phase(no_declaration), @@ -455,14 +464,6 @@ def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): self.FACTORY_BUILDER_PHASE = used_phases.pop() if used_phases else enums.BuilderPhase.ATTRIBUTE_RESOLUTION - if enums.get_builder_phase(decider) is None: - # No builder phase => flat value - decider = SelfAttribute(decider, default=None) - - self.decider = decider - self.yes = yes_declaration - self.no = no_declaration - def call(self, instance, step, context): decider_phase = enums.get_builder_phase(self.decider) if decider_phase == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 889080ab..2e6474d9 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -43,6 +43,21 @@ def test_chaining(self): self.assertEqual(42, declarations.deepgetattr(obj, 'a.b.c.n.x', 42)) +class MaybeTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + # remove after dropping python 2 + import sys + if int(sys.version[0]) == 2: + cls.assertRaisesRegex = cls.assertRaisesRegexp + + def test_init(self): + declarations.Maybe('foo', 1, 2) + + with self.assertRaisesRegex(TypeError, 'Inconsistent phases'): + declarations.Maybe('foo', declarations.LazyAttribute(None), declarations.PostGenerationDeclaration()) + + class SelfAttributeTestCase(unittest.TestCase): def test_standard(self): a = declarations.SelfAttribute('foo.bar.baz') From 1236e078af73d80613352041a953b87c5db47387 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 26 Jun 2019 18:38:25 -0300 Subject: [PATCH 417/714] Update changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6cdc92a8..9d4c3a40 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. - :issue:`598`: Limit ``get_or_create`` behavior to fields specified in ``django_get_or_create``. - :issue:`606`: Re-raise :class:`~django.db.IntegrityError` when `django_get_or_create` with multiple fields fails to lookup model using user provided keyword arguments. + - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. *Removed:* - Drop support for Python 3.4. This version [is not maintained anymore](https://www.python.org/downloads/release/python-3410/). From 9237f8c06407b824a27476eaa102c9bf5a7dd3a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 13 Jul 2019 12:24:47 +0200 Subject: [PATCH 418/714] Remove ORM note for unsupported Django version --- docs/orms.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 261e2ef0..68210983 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -40,14 +40,6 @@ All factories for a Django :class:`~django.db.models.Model` should use the once all post-generation hooks have run. -.. note:: With Django versions 1.8.0 to 1.8.3, it was no longer possible to call ``.build()`` - on a factory if this factory used a :class:`~factory.SubFactory` pointing - to another model: Django refused to set a :class:`~djang.db.models.ForeignKey` - to an unsaved :class:`~django.db.models.Model` instance. - - See https://code.djangoproject.com/ticket/10811 and https://code.djangoproject.com/ticket/25160 for details. - - .. class:: DjangoOptions(factory.base.FactoryOptions) The ``class Meta`` on a :class:`~DjangoModelFactory` supports extra parameters: From 28e7102b86e3fc0bb195a825506a6a4ca96aeb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 21 Jun 2019 18:58:04 +0200 Subject: [PATCH 419/714] Precise that an ORM subclass is needed to create() objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The “Using factories” section of the README was clearly targeted at ORMs. `.create()` does not make sense for plain Python objects. That was misleading for users who just read the factory definition from “Defining factories”. Thanks @x-yuri for the report. --- README.rst | 32 +++++++++++++++++--------------- docs/orms.rst | 2 ++ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/README.rst b/README.rst index a7777977..2d230ea0 100644 --- a/README.rst +++ b/README.rst @@ -109,7 +109,7 @@ Usage Defining factories """""""""""""""""" -Factories declare a set of attributes used to instantiate an object. +Factories declare a set of attributes used to instantiate a Python object. The class of the object must be defined in the ``model`` field of a ``class Meta:`` attribute: .. code-block:: python @@ -135,6 +135,20 @@ The class of the object must be defined in the ``model`` field of a ``class Meta admin = True +ORM integration +""""""""""""""" + +factory_boy integration with Object Relational Mapping (ORM) tools is provided +through specific ``factory.Factory`` subclasses: + +* Django, with ``factory.django.DjangoModelFactory`` +* Mogo, with ``factory.mogo.MogoFactory`` +* MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` +* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` + +More details can be found in the :ref:`ORM section `. + + Using factories """"""""""""""" @@ -145,7 +159,8 @@ factory_boy supports several different build strategies: build, create, and stub # Returns a User instance that's not saved user = UserFactory.build() - # Returns a saved User instance + # Returns a saved User instance. + # UserFactory must subclass an ORM base class, such as DjangoModelFactory. user = UserFactory.create() # Returns a stub object (just a bunch of attributes) @@ -300,19 +315,6 @@ The associated object's strategy will be used: >>> post.author.id is None True - -ORM Support -""""""""""" - -factory_boy has specific support for a few ORMs, through specific ``factory.Factory`` subclasses: - -* Django, with ``factory.django.DjangoModelFactory`` -* Mogo, with ``factory.mogo.MogoFactory`` -* MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` -* SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` - - - Support Policy -------------- diff --git a/docs/orms.rst b/docs/orms.rst index 68210983..9106f0a0 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -1,3 +1,5 @@ +.. _orm: + Using factory_boy with ORMs =========================== From 2a2e7b3de5566a8fcd99258bfc9f4762eca2d051 Mon Sep 17 00:00:00 2001 From: dafinguzman Date: Thu, 25 Jul 2019 14:59:10 -0400 Subject: [PATCH 420/714] Fix docs example of an object variant The example crashed because there were no "user" attributes in the ProfileFactory --- docs/examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index c8c5cb4e..f9182687 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -87,7 +87,7 @@ If we commonly use a specific variant of our objects, we can refine a factory ac class FemaleProfileFactory(ProfileFactory): gender = objects.Profile.GENDER_FEMALE firstname = u'Jane' - user__username = factory.Sequence(lambda n: 'jane%s' % n) + account__username = factory.Sequence(lambda n: 'jane%s' % n) From 325ec6ffcf7723ffd8c70bf46e16c1c081c80b13 Mon Sep 17 00:00:00 2001 From: Youssef Moussaoui Date: Sat, 17 Aug 2019 15:05:50 -0700 Subject: [PATCH 421/714] Fix @factory.post_generation documentation example Discussion here: https://github.com/FactoryBoy/factory_boy/pull/637 --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 4a204d78..1d8880e4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1833,7 +1833,7 @@ A decorator is also provided, decorating a single method accepting the same def mbox(obj, create, extracted, **kwargs): if not create: return - path = extracted or os.path.join('/tmp/mbox/', self.login) + path = extracted or os.path.join('/tmp/mbox/', obj.login) os.path.makedirs(path) return path From a698ad147cadf512a15bfa1bb9f7690e0258d1a5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 20 Aug 2019 05:29:04 -0700 Subject: [PATCH 422/714] Remove unused test_* lines from setup.py factory_boy uses unittest's discover command to run tests, not setup.py. The declarations are unused (and often empty). --- setup.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/setup.py b/setup.py index 48b4a63d..7a30e0c8 100755 --- a/setup.py +++ b/setup.py @@ -46,9 +46,6 @@ def get_version(package_name): setup_requires=[ 'setuptools>=0.8', ], - tests_require=[ - # 'mock', - ], classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Django", @@ -69,6 +66,4 @@ def get_version(package_name): "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", ], - test_suite='', - test_loader='unittest:TestLoader', ) From 82fd6f9f48284874c3959095a502ed0504a3fd01 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 20 Aug 2019 05:21:27 -0700 Subject: [PATCH 423/714] Stop using deprecated Travis container environment https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c99f535e..746c2978 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ dist: xenial -sudo: false language: python cache: pip From c70d1843acdfa11f6b9a571aff6068b60ebe3ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 28 Aug 2019 15:35:02 +0200 Subject: [PATCH 424/714] Add GitHub SUPPORT file https://help.github.com/en/articles/adding-support-resources-to-your-project --- .github/SUPPORT.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/SUPPORT.md diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..1336c012 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,9 @@ +# Getting support + +Most questions should be asked with the `factory-boy` tag on +[StackOverflow](https://stackoverflow.com/questions/tagged/factory-boy). +Alternatively, a discussion group exists at +https://groups.google.com/d/forum/factoryboy. + +Please **do not open issues for support requests**. Issues are meant for bug +reports and improvement suggestions. From 4dd78d9febd374619e3a06ec0dc7ec77022bfcfb Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Wed, 11 Sep 2019 16:45:56 +0200 Subject: [PATCH 425/714] Allow configuring the colour palette of ImageField This allows, for example, to create configure an ImageField to be populated with RBGA or CMYK images. --- CREDITS | 1 + docs/orms.rst | 1 + factory/django.py | 3 ++- tests/test_django.py | 10 ++++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index 596b6e06..fe426d35 100644 --- a/CREDITS +++ b/CREDITS @@ -40,6 +40,7 @@ The project has received contributions from (in alphabetical order): * François Freitag * George Hickman * Hervé Cauwelier +* Hugo Osvaldo Barrera * Ilya Baryshev * Ilya Pirogov * Ionuț Arțăriși diff --git a/docs/orms.rst b/docs/orms.rst index 9106f0a0..cee76924 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -144,6 +144,7 @@ Extra fields :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'``) + :param str palette: The image palette (as supported by PIL) (default: ``'RGB'``) .. note:: If the value ``None`` was passed for the :class:`FileField` field, this will disable field generation: diff --git a/factory/django.py b/factory/django.py index da41c3da..e5a5aaa7 100644 --- a/factory/django.py +++ b/factory/django.py @@ -268,9 +268,10 @@ def _make_data(self, params): height = params.get('height', width) color = params.get('color', 'blue') image_format = params.get('format', 'JPEG') + image_palette = params.get('palette', 'RGB') thumb_io = io.BytesIO() - with Image.new('RGB', (width, height), color) as thumb: + with Image.new(image_palette, (width, height), color) as thumb: thumb.save(thumb_io, format=image_format) return thumb_io.getvalue() diff --git a/tests/test_django.py b/tests/test_django.py index a26bfc2c..240c5bf7 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -727,6 +727,16 @@ def test_with_content(self): self.assertEqual([(169, (254, 0, 0))], colors) self.assertEqual('JPEG', i.format) + def test_rgba_image(self): + o = WithImageFactory.create( + animage__palette='RGBA', + animage__format='PNG', + ) + self.assertIsNotNone(o.pk) + + with Image.open(os.path.join(settings.MEDIA_ROOT, o.animage.name)) as i: + self.assertEqual('RGBA', i.mode) + def test_gif(self): o = WithImageFactory.build(animage__width=13, animage__color='blue', animage__format='GIF') self.assertIsNone(o.pk) From df5f96d1e128d69a685ab02fad941fcf8236e611 Mon Sep 17 00:00:00 2001 From: Lukas Anzinger Date: Fri, 13 Sep 2019 12:28:03 +0200 Subject: [PATCH 426/714] Fix doc string so that it is in line with doc and implementation. --- factory/fuzzy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 1442fe33..a979c3f3 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -278,7 +278,7 @@ def fuzz(self): class FuzzyNaiveDateTime(BaseFuzzyDateTime): """Random naive datetime within a given range. - If no upper bound is given, will default to datetime.datetime.utcnow(). + If no upper bound is given, will default to datetime.datetime.now(). """ def _now(self): From 4997770cf4a5dc09d2d2428c1bd94a63bc0d71a4 Mon Sep 17 00:00:00 2001 From: Etty Date: Fri, 25 Oct 2019 11:50:42 +0200 Subject: [PATCH 427/714] Fix RelatedFactoryList example syntax error There is a "]" > foobar = factory.RelatedFactoryList(BarFactory, size=3]) --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 1d8880e4..1ebd2fcc 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1775,7 +1775,7 @@ RelatedFactoryList bar = factory.RelatedFactoryList(BarFactory, size=lambda: LIST_SIZES[random.randint(0,5)]) # Each Foo object will have exactly 3 Bar objects generated for its foobar attribute. - foobar = factory.RelatedFactoryList(BarFactory, size=3]) + foobar = factory.RelatedFactoryList(BarFactory, size=3) PostGeneration From 52b4f675007551c982e3f87828e7d94bcf3c0316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 13:05:51 +0100 Subject: [PATCH 428/714] Remove unused tag-format from setup.cfg Last release with the `v` prefix in the tag was done on Jul 30, 2017. --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index dddea03f..08a05494 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,9 +8,6 @@ license_file = LICENSE ; semver-style versions version-levels = 3 -; tags: vX.Y.Z; the double-percent is a guard against ConfigParser. -#tag-format = v%%(version)s - ; Version flag location (we use __version__) python-file-with-version = factory/__init__.py From 328cedf295a0beeb26968c856e68fb9a4d71102b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 13:09:51 +0100 Subject: [PATCH 429/714] Remove warning exclusion for Python3.4 Python 3.4 support was dropped in 55fbaa71fa85ea65313a07866e6563e4dffe28c4. --- Makefile | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 499d9fb7..66af6bdd 100644 --- a/Makefile +++ b/Makefile @@ -52,16 +52,14 @@ testall: # DOC: Run tests for the currently installed version test: - # imp warning is a PendingDeprecationWarning for Python 3.4 and Python 3.5 - # and a DeprecationWarning for later versions. - # Change PendingDeprecationWarning to distutils modules when dropping - # support for Python 3.4. + # imp warning is a PendingDeprecationWarning for Python 3.5 and a + # DeprecationWarning for later versions. python \ -b \ -Werror \ -Wdefault:"'U' mode is deprecated":DeprecationWarning:site: \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ - -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":PendingDeprecationWarning:: \ + -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":PendingDeprecationWarning:distutils: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ -m unittest discover From 27353d8dea3c28ef063b11fe179fc6e3f83f5bbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 13:44:24 +0100 Subject: [PATCH 430/714] Document support for Django 2.2 in release note Missed in 28cbc6635a74a85776e6724df5b2e4456174ec74. --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9d4c3a40..746d9852 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ ChangeLog 2.12.1 (unreleased) ------------------- +*New:* + + - Add support for Django 2.2 + *Bugfix:* - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. From 0a2ed8912f9d96fe33f4f6fac437ad3fb7150d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 13:46:14 +0100 Subject: [PATCH 431/714] Drop dist: xenial from .travis.yml xenial is now the default. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 746c2978..b1cf9cd5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -dist: xenial language: python cache: pip From 93bbd0317092c8e804d039ed60413f4b75fd7e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 13:23:57 +0100 Subject: [PATCH 432/714] Move version to setup.cfg Enforces a declarative style for version. Be closer to the default setuptools behavior. --- factory/__init__.py | 1 - setup.cfg | 4 +--- setup.py | 17 ----------------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index fcaccca5..0cffcd4d 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -54,7 +54,6 @@ stub_batch, ) -__version__ = '2.12.1.dev0' __author__ = 'Raphaël Barrois ' diff --git a/setup.cfg b/setup.cfg index 08a05494..72d8e290 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,14 +3,12 @@ universal = 1 [metadata] license_file = LICENSE +version = 2.12.1.dev0 [zest.releaser] ; semver-style versions version-levels = 3 -; Version flag location (we use __version__) -python-file-with-version = factory/__init__.py - [distutils] index-servers = pypi diff --git a/setup.py b/setup.py index 7a30e0c8..9270c6e6 100755 --- a/setup.py +++ b/setup.py @@ -3,31 +3,14 @@ import codecs import os -import re from setuptools import setup root_dir = os.path.abspath(os.path.dirname(__file__)) -def get_version(package_name): - version_re = re.compile(r"^__version__ = [\"']([\w_.-]+)[\"']$") - package_components = package_name.split('.') - init_path = os.path.join(root_dir, *(package_components + ['__init__.py'])) - with codecs.open(init_path, 'r', 'utf-8') as f: - for line in f: - match = version_re.match(line[:-1]) - if match: - return match.groups()[0] - return '0.1.0' - - -PACKAGE = 'factory' - - setup( name='factory_boy', - version=get_version(PACKAGE), description="A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby.", long_description=codecs.open(os.path.join(root_dir, 'README.rst'), 'r', 'utf-8').read(), author='Mark Sandstrom', From 9fbfe6a3e53cc934cc34c89750ad5502e13b9c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 20:17:52 +0100 Subject: [PATCH 433/714] Remove setuptools from setup_requires The package is imported at the top of the module, so it is present on the machine. Version 0.8 was released on Jul 5, 2013. Can assume all machines installing factory_boy have setuptools available. --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 9270c6e6..952c3a25 100755 --- a/setup.py +++ b/setup.py @@ -26,9 +26,6 @@ install_requires=[ 'Faker>=0.7.0', ], - setup_requires=[ - 'setuptools>=0.8', - ], classifiers=[ "Development Status :: 5 - Production/Stable", "Framework :: Django", From 9de982c8059f4bf3f3021a36db606d98193f6bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 18:50:32 +0100 Subject: [PATCH 434/714] Simplify tests/compat.py a bit Oldest support version of Python is 3.5, the alternative import path is only kept for Python 2. --- tests/compat.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/compat.py b/tests/compat.py index 87066eeb..08fdf06e 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -9,10 +9,7 @@ if is_python2: import StringIO as io -else: - import io # noqa: F401 - -if sys.version_info[0:2] < (3, 3): import mock else: + import io # noqa: F401 from unittest import mock # noqa: F401 From d99c89ad7e8855a02d69c447becbe70ea29dc505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 19:34:44 +0100 Subject: [PATCH 435/714] Remove `know_first_party` and `known_third_party` from isort config `isort` sorts these libraries correctly without this option when they are installed. The `known_third_party` list was incomplete. --- setup.cfg | 2 -- tox.ini | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 72d8e290..3462e627 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,5 +18,3 @@ include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True line_length = 88 -known_first_party = factory -known_third_party = django,faker,mongoengine diff --git a/tox.ini b/tox.ini index 14190fc5..24389630 100644 --- a/tox.ini +++ b/tox.ini @@ -54,8 +54,7 @@ commands = make linkcheck [testenv:lint] deps = - flake8 - isort + -rrequirements_dev.txt check_manifest skip_install = true From 72492f420cb1960165a41058484af06302965b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 18:33:19 +0100 Subject: [PATCH 436/714] Remove requirements_test.txt Lint dependencies are managed by tox, flake8 was installed but not used. Tests are managed by tox, declare test requirements in tox.ini. Mock was added as a development dependency, to avoid disruption for Python 2.7 developers (if some remain :)) --- requirements_dev.txt | 3 ++- requirements_test.txt | 2 -- tox.ini | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 requirements_test.txt diff --git a/requirements_dev.txt b/requirements_dev.txt index ba60c746..2811d297 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,11 +1,12 @@ -e . -r requirements_docs.txt --r requirements_test.txt -r examples/requirements.txt coverage Django +flake8 isort +mock;python_version<"3" Pillow SQLAlchemy mongoengine diff --git a/requirements_test.txt b/requirements_test.txt deleted file mode 100644 index 41fb52dc..00000000 --- a/requirements_test.txt +++ /dev/null @@ -1,2 +0,0 @@ -mock -flake8 diff --git a/tox.ini b/tox.ini index 24389630..f0fa7471 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = - -rrequirements_test.txt + mock;python_version<"3" django111: Django>=1.11,<1.12 django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 @@ -38,7 +38,6 @@ commands = make doc [testenv:examples] basepython = python3.7 deps = - -rrequirements_test.txt -rexamples/requirements.txt whitelist_externals = make From 3febefefcfb75fc451a4ac9664d8a6c9915bd4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 13:43:13 +0100 Subject: [PATCH 437/714] Python 3.8 support Most warnings can be reinstated on the next pymongo release. See https://github.com/mongodb/mongo-python-driver/commit/9cf0fbd785086ef1702bbafd2ad92a279fe406e4. --- .travis.yml | 9 +++++---- Makefile | 4 ++++ README.rst | 2 +- docs/changelog.rst | 1 + setup.py | 1 + tox.ini | 10 +++++----- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index b1cf9cd5..a8407420 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,20 +13,21 @@ matrix: - python: "3.5" - python: "3.6" - python: "3.7" + - python: "3.8" - python: "pypy2.7-6.0" - python: "pypy3.5-6.0" # Documentation - - python: "3.7" + - python: "3.8" env: TOXENV=docs - - python: "3.7" + - python: "3.8" env: TOXENV=linkcheck # Linting - - python: "3.7" + - python: "3.8" env: TOXENV=examples - - python: "3.7" + - python: "3.8" env: TOXENV=lint services: diff --git a/Makefile b/Makefile index 66af6bdd..5dc4a9ae 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,10 @@ test: -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":PendingDeprecationWarning:distutils: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ + -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ + -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ + -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ + -Wdefault:"PY_SSIZE_T_CLEAN will be required for '#' formats":DeprecationWarning:: \ -m unittest discover # DOC: Test the examples diff --git a/README.rst b/README.rst index 2d230ea0..94db5a0f 100644 --- a/README.rst +++ b/README.rst @@ -318,7 +318,7 @@ The associated object's strategy will be used: Support Policy -------------- -``factory_boy`` supports Python 2.7, 3.5 to 3.7, as well as PyPy 2.7 and 5.8. +``factory_boy`` supports Python 2.7, 3.5 to 3.8, as well as PyPy 2.7 and 5.8. - **Django**'s [supported versions](https://www.djangoproject.com/download/#supported-versions). diff --git a/docs/changelog.rst b/docs/changelog.rst index 746d9852..870db38f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ ChangeLog *New:* + - Add support for Django 3.8 - Add support for Django 2.2 *Bugfix:* diff --git a/setup.py b/setup.py index 952c3a25..96dc55a9 100755 --- a/setup.py +++ b/setup.py @@ -42,6 +42,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tox.ini b/tox.ini index f0fa7471..ab89974f 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,9 @@ minversion = 1.9 envlist = lint - py{27,35,36,37}-django111-alchemy-mongoengine, - py{35,36,37}-django21-alchemy-mongoengine, - py{35,36,37}-django22-alchemy-mongoengine, + py{27,35,36,37,38}-django111-alchemy-mongoengine, + py{35,36,37,38}-django21-alchemy-mongoengine, + py{35,36,37,38}-django22-alchemy-mongoengine, pypy-django{111}-alchemy-mongoengine, pypy3-django{111,21,22}-alchemy-mongoengine, docs @@ -27,7 +27,7 @@ whitelist_externals = make commands = make test [testenv:docs] -basepython = python3.7 +basepython = python3.8 deps = -rrequirements_docs.txt skip_install = true @@ -36,7 +36,7 @@ whitelist_externals = make commands = make doc [testenv:examples] -basepython = python3.7 +basepython = python3.8 deps = -rexamples/requirements.txt From ca7b0f59137b501f25233a330a26640af60fa9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 28 Nov 2019 19:20:45 +0100 Subject: [PATCH 438/714] Fix typo in changelog Typo from 3febefefcfb75fc451a4ac9664d8a6c9915bd4b2. --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 870db38f..6513930f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,7 @@ ChangeLog *New:* - - Add support for Django 3.8 + - Add support for Python 3.8 - Add support for Django 2.2 *Bugfix:* From 92ccdafe18d5fd9a8e7a97bb022ca6ef3b91620d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 23 Nov 2019 20:10:07 +0100 Subject: [PATCH 439/714] Move setup information to setup.cfg Use a declarative syntax, avoids mixing code and configuration. Simplifies handling of long description file. Reviewed-by: Federico Bond --- setup.cfg | 38 +++++++++++++++++++++++++++++++++++++- setup.py | 46 +--------------------------------------------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/setup.cfg b/setup.cfg index 3462e627..6a0b1866 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,8 +2,44 @@ universal = 1 [metadata] -license_file = LICENSE +name = factory_boy version = 2.12.1.dev0 +description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. +long_description = file: README.rst +author = Mark Sandstrom +author_email = mark@deliciouslynerdy.com +maintainer = Raphaël Barrois +maintainer_email = raphael.barrois+fboy@polytechnique.org +url = https://github.com/FactoryBoy/factory_boy +keywords = factory_boy, factory, fixtures +license = MIT +license_file = LICENSE +classifiers = + Development Status :: 5 - Production/Stable + Framework :: Django + Framework :: Django :: 1.11 + Framework :: Django :: 2.1 + Framework :: Django :: 2.2 + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: Implementation :: PyPy + Topic :: Software Development :: Testing + Topic :: Software Development :: Libraries :: Python Modules + +[options] +zip_safe = false +packages = factory +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +install_requires = Faker>=0.7.0 [zest.releaser] ; semver-style versions diff --git a/setup.py b/setup.py index 96dc55a9..8b95793a 100755 --- a/setup.py +++ b/setup.py @@ -1,50 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import codecs -import os - from setuptools import setup -root_dir = os.path.abspath(os.path.dirname(__file__)) - - -setup( - name='factory_boy', - description="A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby.", - long_description=codecs.open(os.path.join(root_dir, 'README.rst'), 'r', 'utf-8').read(), - author='Mark Sandstrom', - author_email='mark@deliciouslynerdy.com', - maintainer='Raphaël Barrois', - maintainer_email='raphael.barrois+fboy@polytechnique.org', - url='https://github.com/FactoryBoy/factory_boy', - keywords=['factory_boy', 'factory', 'fixtures'], - packages=['factory'], - zip_safe=False, - license='MIT', - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", - install_requires=[ - 'Faker>=0.7.0', - ], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Framework :: Django", - "Framework :: Django :: 1.11", - "Framework :: Django :: 2.1", - "Framework :: Django :: 2.2", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Software Development :: Testing", - "Topic :: Software Development :: Libraries :: Python Modules", - ], -) +setup() From 10d5384ea23dfbe739b1440280c762d4856c7cf7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 7 Dec 2019 06:54:56 -0800 Subject: [PATCH 440/714] Remove end of life Django 2.1 from the test matrix Django 2.1 went EOL on December 2, 2019. https://www.djangoproject.com/download/#supported-versions --- docs/changelog.rst | 2 +- setup.cfg | 1 - tox.ini | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6513930f..69fd3be8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -19,7 +19,7 @@ ChangeLog *Removed:* - Drop support for Python 3.4. This version [is not maintained anymore](https://www.python.org/downloads/release/python-3410/). - - Drop support for Django 2.0. This version [is not maintained anymore](https://www.djangoproject.com/download/#supported-versions). + - Drop support for Django 2.0 and 2.1. These versions [are not maintained anymore](https://www.djangoproject.com/download/#supported-versions). 2.12.0 (2019-05-11) diff --git a/setup.cfg b/setup.cfg index 6a0b1866..ca30c1d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,6 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 1.11 - Framework :: Django :: 2.1 Framework :: Django :: 2.2 Intended Audience :: Developers License :: OSI Approved :: MIT License diff --git a/tox.ini b/tox.ini index ab89974f..c58c76c9 100644 --- a/tox.ini +++ b/tox.ini @@ -3,10 +3,9 @@ minversion = 1.9 envlist = lint py{27,35,36,37,38}-django111-alchemy-mongoengine, - py{35,36,37,38}-django21-alchemy-mongoengine, py{35,36,37,38}-django22-alchemy-mongoengine, pypy-django{111}-alchemy-mongoengine, - pypy3-django{111,21,22}-alchemy-mongoengine, + pypy3-django{111,22}-alchemy-mongoengine, docs examples linkcheck @@ -17,9 +16,8 @@ toxworkdir = {env:TOX_WORKDIR:.tox} deps = mock;python_version<"3" django111: Django>=1.11,<1.12 - django21: Django>=2.1,<2.2 django22: Django>=2.2,<2.3 - django{111,21,22}: Pillow + django{111,22}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 6259c7ab3c9520ff058216bf1b83ad790b65edb7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 7 Dec 2019 06:44:30 -0800 Subject: [PATCH 441/714] Add Django 3.0 to the test matrix Django 3.0 was released December 2, 2019. --- docs/changelog.rst | 2 +- setup.cfg | 1 + tox.ini | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 69fd3be8..f5be7849 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,7 +7,7 @@ ChangeLog *New:* - Add support for Python 3.8 - - Add support for Django 2.2 + - Add support for Django 2.2 and 3.0 *Bugfix:* diff --git a/setup.cfg b/setup.cfg index ca30c1d9..cda8ee3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,7 @@ classifiers = Framework :: Django Framework :: Django :: 1.11 Framework :: Django :: 2.2 + Framework :: Django :: 3.0 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index c58c76c9..60e88b36 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = lint py{27,35,36,37,38}-django111-alchemy-mongoengine, py{35,36,37,38}-django22-alchemy-mongoengine, + py{36,37,38}-django30-alchemy-mongoengine, pypy-django{111}-alchemy-mongoengine, pypy3-django{111,22}-alchemy-mongoengine, docs @@ -17,7 +18,8 @@ deps = mock;python_version<"3" django111: Django>=1.11,<1.12 django22: Django>=2.2,<2.3 - django{111,22}: Pillow + django30: Django>=3.0,<3.1 + django{111,22,30}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 1e26638ee410c42b7e193b215ed0d9480a9a9212 Mon Sep 17 00:00:00 2001 From: Mateusz Marczak Date: Thu, 21 Nov 2019 21:17:09 +0100 Subject: [PATCH 442/714] Prevent Factory model from being a Factory The `model` option in the Factory Meta is expected to be the class constructed by the factory. Passing a Factory as the model is not intended and indicates a user error. Closes #572 --- docs/reference.rst | 18 ++++++++++++++---- factory/base.py | 10 +++++++++- tests/test_base.py | 10 ++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 1ebd2fcc..9f057607 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -2018,13 +2018,23 @@ Instance building """"""""""""""""" The :mod:`factory` module provides a bunch of shortcuts for creating a factory and -extracting instances from them: +extracting instances from them. Helper methods can be used to create factories +in a dynamic way based on parameters. + +Internally, helper methods use :func:`make_factory` to create a new +:class:`Factory` and perform additional calls on the newly created +:class:`Factory` according to the method name. + +Please note, that all Factories created with this methods inherit from the +:class:`factory.base.Factory` class. For full support of your ``ORM``, specify +a base class with the ``FACTORY_CLASS`` parameter as shown in +:func:`make_factory` examples. .. function:: build(klass, FACTORY_CLASS=None, **kwargs) .. function:: build_batch(klass, size, FACTORY_CLASS=None, **kwargs) Create a factory for :obj:`klass` using declarations passed in kwargs; - return an instance built from that factory, + return an instance built from that factory with :data:`BUILD_STRATEGY`, or a list of :obj:`size` instances (for :func:`build_batch`). :param class klass: Class of the instance to build @@ -2038,7 +2048,7 @@ extracting instances from them: .. function:: create_batch(klass, size, FACTORY_CLASS=None, **kwargs) Create a factory for :obj:`klass` using declarations passed in kwargs; - return an instance created from that factory, + return an instance created from that factory with :data:`CREATE_STRATEGY`, or a list of :obj:`size` instances (for :func:`create_batch`). :param class klass: Class of the instance to create @@ -2052,7 +2062,7 @@ extracting instances from them: .. function:: stub_batch(klass, size, FACTORY_CLASS=None, **kwargs) Create a factory for :obj:`klass` using declarations passed in kwargs; - return an instance stubbed from that factory, + return an instance stubbed from that factory with :data:`STUB_STRATEGY`, or a list of :obj:`size` instances (for :func:`stub_batch`). :param class klass: Class of the instance to stub diff --git a/factory/base.py b/factory/base.py index da7aa554..9ed6c68e 100644 --- a/factory/base.py +++ b/factory/base.py @@ -162,8 +162,16 @@ def _build_default_options(self): Custom FactoryOptions classes should override this method to update() its return value. """ + + def is_model(meta, value): + if isinstance(value, FactoryMetaClass): + raise TypeError( + "%s is already a %s" + % (repr(value), Factory.__name__) + ) + return [ - OptionDefault('model', None, inherit=True), + OptionDefault('model', None, inherit=True, checker=is_model), OptionDefault('abstract', False, inherit=False), OptionDefault('strategy', enums.CREATE_STRATEGY, inherit=True), OptionDefault('inline_args', (), inherit=True), diff --git a/tests/test_base.py b/tests/test_base.py index 09dd1e6b..6752d256 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -206,6 +206,16 @@ class OtherFactory(AbstractFactory): OtherFactory._meta.post_declarations.as_dict(), ) + def test_factory_as_meta_model_raises_exception(self): + class FirstFactory(base.Factory): + pass + + class Meta: + model = FirstFactory + + with self.assertRaises(TypeError): + type("SecondFactory", (base.Factory,), {"Meta": Meta}) + class DeclarationParsingTests(unittest.TestCase): def test_classmethod(self): From fb634a9fd4641356b964d98c481c2b0f829655b9 Mon Sep 17 00:00:00 2001 From: Markus Amalthea Magnuson Date: Mon, 23 Dec 2019 15:47:16 +0100 Subject: [PATCH 443/714] Fix typo in changelog. --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f5be7849..4403fbc8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -100,7 +100,7 @@ ChangeLog ------------------ This version brings massive changes to the core engine, thus reducing the number of -corner cases and weird behaviourrs. +corner cases and weird behaviours. *New:* From 871d49d71b1a444bd1f4a6afde1031991144af04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 18 Jan 2020 12:19:38 +0100 Subject: [PATCH 444/714] Replace RelatedFactory usage note with comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of creating a block with a note about incorrect usage and introducing another example, indicate the potential gotcha with a comment. This improves conciseness and readability, and is more consistent with SubFactory’s documentation of the issue. --- docs/reference.rst | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 9f057607..c9c35ae6 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1641,21 +1641,6 @@ 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): - class Meta: - model = Foo - - bar = factory.RelatedFactory(BarFactory) # Not BarFactory() - - .. code-block:: python class CityFactory(factory.Factory): @@ -1670,7 +1655,11 @@ RelatedFactory model = Country lang = 'fr' - capital_city = factory.RelatedFactory(CityFactory, 'capital_of', name="Paris") + capital_city = factory.RelatedFactory( + CityFactory, # Not CityFactory() + 'capital_of', + name="Paris", + ) .. code-block:: pycon From f2a58d3e2548a778edbee8e9cfcb67d8d1ed56e5 Mon Sep 17 00:00:00 2001 From: kobayashi Date: Fri, 24 Jan 2020 11:56:08 -0500 Subject: [PATCH 445/714] Fixes #688: Describe Many-to-many for SQLAlchemy --- docs/recipes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index d278bfac..2fd4e01e 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -199,6 +199,9 @@ But when ``UserFactory.create(groups=(group1, group2, group3))`` is called, the ``groups`` declaration will add passed in groups to the set of groups for the user. +For SQLAlchemy, ``list`` is for many-to-many relationship. +Modify ``self.groups.add(group)`` to ``self.groups.append(group)`` from the above example, +then create ``UserFactory.create(groups=[group1, group2, group3])`` Many-to-many relation with a 'through' -------------------------------------- From 628133cad71d6121a7f554dbf296a0c38a51a98e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 26 Jan 2020 06:48:11 -0800 Subject: [PATCH 446/714] Correct reStructuredText syntax in docs/changelog.rst --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4403fbc8..4bd7f6e3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,8 +18,8 @@ ChangeLog - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. *Removed:* - - Drop support for Python 3.4. This version [is not maintained anymore](https://www.python.org/downloads/release/python-3410/). - - Drop support for Django 2.0 and 2.1. These versions [are not maintained anymore](https://www.djangoproject.com/download/#supported-versions). + - Drop support for Python 3.4. This version `is not maintained anymore `_. + - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `_. 2.12.0 (2019-05-11) From 79161b0409842b5fc8a43d5844669f233403e14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 4 Feb 2020 13:53:41 +0100 Subject: [PATCH 447/714] Fix typo in README.rst --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 94db5a0f..877a2236 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ factory_boy factory_boy is a fixtures replacement based on thoughtbot's `factory_bot `_. As a fixtures replacement tool, it aims to replace static, hard to maintain fixtures -with easy-to-use factories for complex object. +with easy-to-use factories for complex objects. Instead of building an exhaustive test setup with every possible combination of corner cases, ``factory_boy`` allows you to use objects customized for the current test, From 3eb79173eee147ec5cf827d20b1a1caf5aec915e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 7 Dec 2019 17:28:27 +0100 Subject: [PATCH 448/714] Restore __version__ attribute on factory module Allows third party libraries (such as django-factory_boy or pytest-factoryboy) to work around breaking changes in factory_boy. Partially reverts 93bbd0317092c8e804d039ed60413f4b75fd7e77. Reviewed-by: Federico Bond Reviewed-by: Jon Dufresne --- docs/conf.py | 17 +++-------------- factory/__init__.py | 9 +++++++++ requirements_docs.txt | 1 + 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8cef794a..23aee946 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,6 +14,8 @@ import os import sys +import factory + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -59,21 +61,8 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -root = os.path.abspath(os.path.dirname(__file__)) -def get_version(*module_dir_components): - import re - version_re = re.compile(r"^__version__ = ['\"](.*)['\"]$") - module_root = os.path.join(root, os.pardir, *module_dir_components) - module_init = os.path.join(module_root, '__init__.py') - with open(module_init, 'r') as f: - for line in f: - match = version_re.match(line[:-1]) - if match: - return match.groups()[0] - return '0.1.0' - # The full version, including alpha/beta/rc tags. -release = get_version('factory') +release = factory.__version__ # The short X.Y version. version = '.'.join(release.split('.')[:2]) diff --git a/factory/__init__.py b/factory/__init__.py index 0cffcd4d..20929fac 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -55,6 +55,15 @@ ) __author__ = 'Raphaël Barrois ' +try: + # Python 3.8+ + from importlib.metadata import version + + __version__ = version("factory_boy") +except ImportError: + import pkg_resources + + __version__ = pkg_resources.get_distribution("factory_boy").version MogoFactory = mogo.MogoFactory diff --git a/requirements_docs.txt b/requirements_docs.txt index ab3f3dd4..583ab863 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,2 +1,3 @@ +-e . Sphinx sphinx_rtd_theme From b54dd4aa9abbca090b58e949f9cf011478dcd205 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 26 Feb 2020 18:18:11 +1100 Subject: [PATCH 449/714] Fix simple typo: charactes -> characters Closes #705 --- docs/fuzzy.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index ab9c3d3b..dbcd9fdf 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -39,7 +39,7 @@ 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 + the given :attr:`prefix`, followed by :attr:`length` characters chosen from the :attr:`chars` character set, and ending with the given :attr:`suffix`. From 80933bcefcb686f7fa49cf91283b1f6a619d77c3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 03:46:37 -0800 Subject: [PATCH 450/714] =?UTF-8?q?Fix=20typo:=20GenericForenikeyConfig=20?= =?UTF-8?q?=E2=86=92=20GenericForeignKeyConfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/django_demo/generic_foreignkey/apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/django_demo/generic_foreignkey/apps.py b/examples/django_demo/generic_foreignkey/apps.py index 64526770..09c30b26 100644 --- a/examples/django_demo/generic_foreignkey/apps.py +++ b/examples/django_demo/generic_foreignkey/apps.py @@ -3,5 +3,5 @@ from django.apps import AppConfig -class GenericForenikeyConfig(AppConfig): +class GenericForeignKeyConfig(AppConfig): name = 'generic_foreignkey' From 9dd7828c8d0c3548d205ef6d2bce027cfa44285f Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 04:07:52 -0800 Subject: [PATCH 451/714] Use Travis alias to latest PyPy releases Helps ensure factory_boy remains compatible as new PyPy releases roll out. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a8407420..3050490c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,8 +15,8 @@ matrix: - python: "3.7" - python: "3.8" - - python: "pypy2.7-6.0" - - python: "pypy3.5-6.0" + - python: "pypy" + - python: "pypy3" # Documentation - python: "3.8" From bf8604dbf056bdaba982c202246770218197c89d Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 04:10:02 -0800 Subject: [PATCH 452/714] Add trove classifier for CPython support Serves in contrast to PyPy support. Both implementations are tested and supported. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index cda8ee3e..298cfd60 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing Topic :: Software Development :: Libraries :: Python Modules From c24fd848370220f7ab8429d28c6a5934a69a7d3a Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 03:48:40 -0800 Subject: [PATCH 453/714] Remove deprecated license_file from setup.cfg Starting with wheel 0.32.0 (2018-09-29), the "license_file" option is deprecated. https://wheel.readthedocs.io/en/stable/news.html The wheel will continue to include LICENSE, it is now included automatically: https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file --- requirements_dev.txt | 2 +- setup.cfg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 2811d297..2bd13a5f 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,6 +10,6 @@ mock;python_version<"3" Pillow SQLAlchemy mongoengine -wheel +wheel>=0.32.0 tox zest.releaser[recommended] diff --git a/setup.cfg b/setup.cfg index 298cfd60..7674445e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,6 @@ maintainer_email = raphael.barrois+fboy@polytechnique.org url = https://github.com/FactoryBoy/factory_boy keywords = factory_boy, factory, fixtures license = MIT -license_file = LICENSE classifiers = Development Status :: 5 - Production/Stable Framework :: Django From 5a8bd0497a4662b88ea4ff9306cfffe4cd6385e3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 05:18:27 -0800 Subject: [PATCH 454/714] Correct reStructuredText syntax in "Support Policy" section --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 877a2236..bb8a30f1 100644 --- a/README.rst +++ b/README.rst @@ -320,10 +320,10 @@ Support Policy ``factory_boy`` supports Python 2.7, 3.5 to 3.8, as well as PyPy 2.7 and 5.8. -- **Django**'s [supported - versions](https://www.djangoproject.com/download/#supported-versions). -- **SQLAlchemy**: [latest version on PyPI](https://pypi.org/project/SQLAlchemy/). -- **mongoengine**: [latest version on PyPI](https://pypi.org/project/mongoengine/). +- **Django**'s `supported + versions `__. +- **SQLAlchemy**: `latest version on PyPI `__. +- **mongoengine**: `latest version on PyPI `__. Debugging factory_boy --------------------- From 4493a40b0a7f431e9c58c7148c7757cfc3d14c79 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 04:24:48 -0800 Subject: [PATCH 455/714] Remove fallback for deprecated, unsupported Pillow package "Image" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From https://pillow.readthedocs.io/en/stable/installation.html > Pillow >= 1.0 no longer supports “import Image”. Please use “from PIL > import Image” instead. --- factory/django.py | 5 +---- tests/djapp/models.py | 5 +---- tests/test_django.py | 7 +------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/factory/django.py b/factory/django.py index e5a5aaa7..de0c3ba1 100644 --- a/factory/django.py +++ b/factory/django.py @@ -259,10 +259,7 @@ class ImageField(FileField): 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 + from PIL import Image width = params.get('width', 100) height = params.get('height', width) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 3a60d0da..bba53d57 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -11,10 +11,7 @@ try: from PIL import Image except ImportError: - try: - import Image - except ImportError: - Image = None + Image = None class StandardModel(models.Model): diff --git a/tests/test_django.py b/tests/test_django.py index 240c5bf7..14b2e9d7 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -22,12 +22,7 @@ try: from PIL import Image except ImportError: - # Try PIL alternate name - try: - import Image - except ImportError: - # OK, not installed - Image = None + Image = None # Setup Django before importing Django models. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') From 17b8ba420a30bf6fcbc2afc76a9066a90639d3fa Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 2 Mar 2020 17:36:42 -0800 Subject: [PATCH 456/714] Remove outdated warning filters --- Makefile | 2 -- 1 file changed, 2 deletions(-) diff --git a/Makefile b/Makefile index 5dc4a9ae..ef54a982 100644 --- a/Makefile +++ b/Makefile @@ -57,14 +57,12 @@ test: python \ -b \ -Werror \ - -Wdefault:"'U' mode is deprecated":DeprecationWarning:site: \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":PendingDeprecationWarning:distutils: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ - -Wdefault:"PY_SSIZE_T_CLEAN will be required for '#' formats":DeprecationWarning:: \ -m unittest discover # DOC: Test the examples From e844578c261200035d398becf8e0c8c58191cd6b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 2 Mar 2020 17:20:13 -0800 Subject: [PATCH 457/714] Restore original logging state on exception in factory.debug() --- factory/helpers.py | 9 +++++---- tests/test_helpers.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/factory/helpers.py b/factory/helpers.py index 01866ec8..5433b319 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -20,10 +20,11 @@ def debug(logger='factory', stream=None): logger_obj.addHandler(handler) logger_obj.setLevel(logging.DEBUG) - yield - - logger_obj.setLevel(old_level) - logger_obj.removeHandler(handler) + try: + yield + finally: + logger_obj.setLevel(old_level) + logger_obj.removeHandler(handler) def make_factory(klass, **kwargs): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index c2a4f5f1..57b45edd 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -55,3 +55,16 @@ def test_alternate_logger(self): self.assertEqual("", stream1.getvalue()) self.assertEqual("Test2\n", stream2.getvalue()) + + def test_restores_logging_on_error(self): + class MyException(Exception): + pass + + stream = io.StringIO() + try: + with helpers.debug(stream=stream): + raise MyException + except MyException: + logger = logging.getLogger('factory') + self.assertEqual(logger.level, logging.NOTSET) + self.assertEqual(logger.handlers, []) From 2474a1b725e9ab46feb27d9ae36f21b5dd04b17e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 3 Mar 2020 11:33:50 +0100 Subject: [PATCH 458/714] Fix grammar mistake in readme --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index bb8a30f1..c23834dc 100644 --- a/README.rst +++ b/README.rst @@ -366,7 +366,7 @@ factory_boy is distributed under the MIT License. Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. Questions and suggestions are welcome on the `mailing-list `_. -All pull request should pass the test suite, which can be launched simply with: +All pull requests should pass the test suite, which can be launched simply with: .. code-block:: sh From a0d2e24225c85f48e01972a7411c875d4be8d701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 3 Mar 2020 10:57:25 +0100 Subject: [PATCH 459/714] Declare documentation as extras_require --- requirements_dev.txt | 3 +-- requirements_docs.txt | 3 --- setup.cfg | 5 +++++ tox.ini | 8 ++------ 4 files changed, 8 insertions(+), 11 deletions(-) delete mode 100644 requirements_docs.txt diff --git a/requirements_dev.txt b/requirements_dev.txt index 2bd13a5f..313d25f7 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,4 @@ --e . --r requirements_docs.txt +-e .[doc] -r examples/requirements.txt coverage diff --git a/requirements_docs.txt b/requirements_docs.txt deleted file mode 100644 index 583ab863..00000000 --- a/requirements_docs.txt +++ /dev/null @@ -1,3 +0,0 @@ --e . -Sphinx -sphinx_rtd_theme diff --git a/setup.cfg b/setup.cfg index 7674445e..88dd92f1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,6 +41,11 @@ packages = factory python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* install_requires = Faker>=0.7.0 +[options.extras_require] +doc = + Sphinx + sphinx_rtd_theme + [zest.releaser] ; semver-style versions version-levels = 3 diff --git a/tox.ini b/tox.ini index 60e88b36..77888d43 100644 --- a/tox.ini +++ b/tox.ini @@ -28,9 +28,7 @@ commands = make test [testenv:docs] basepython = python3.8 -deps = - -rrequirements_docs.txt -skip_install = true +extras = doc whitelist_externals = make commands = make doc @@ -44,9 +42,7 @@ whitelist_externals = make commands = make example-test [testenv:linkcheck] -deps = - -rrequirements_docs.txt -skip_install = true +extras = doc whitelist_externals = make commands = make linkcheck From 7a23a28eb89455b940f3c262179d7c7e54b45a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 3 Mar 2020 11:09:55 +0100 Subject: [PATCH 460/714] Remove unused requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ca80dfc1..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -Faker>=0.7.0 From 65bf7a7967dc788c7ef7d0e294b822334c9a7f44 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 04:32:10 -0800 Subject: [PATCH 461/714] Run pyupgrade across the project pyupgrade is a tool to automatically upgrade syntax for newer versions of the language. https://github.com/asottile/pyupgrade --- README.rst | 4 ++-- factory/base.py | 8 ++++---- factory/declarations.py | 4 ++-- tests/test_base.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index c23834dc..93bcba3b 100644 --- a/README.rst +++ b/README.rst @@ -249,7 +249,7 @@ These "lazy" attributes can be added as follows: first_name = 'Joe' last_name = 'Blow' - email = factory.LazyAttribute(lambda a: '{0}.{1}@example.com'.format(a.first_name, a.last_name).lower()) + email = factory.LazyAttribute(lambda a: '{}.{}@example.com'.format(a.first_name, a.last_name).lower()) date_joined = factory.LazyFunction(datetime.now) .. code-block:: pycon @@ -273,7 +273,7 @@ Unique values in a specific format (for example, e-mail addresses) can be genera class Meta: model = models.User - email = factory.Sequence(lambda n: 'person{0}@example.com'.format(n)) + email = factory.Sequence(lambda n: 'person{}@example.com'.format(n)) >>> UserFactory().email 'person0@example.com' diff --git a/factory/base.py b/factory/base.py index 9ed6c68e..95904d98 100644 --- a/factory/base.py +++ b/factory/base.py @@ -43,7 +43,7 @@ def __call__(cls, **kwargs): elif cls._meta.strategy == enums.STUB_STRATEGY: return cls.stub(**kwargs) else: - raise errors.UnknownStrategy('Unknown Meta.strategy: {0}'.format( + raise errors.UnknownStrategy('Unknown Meta.strategy: {}'.format( cls._meta.strategy)) def __new__(mcs, class_name, bases, attrs): @@ -184,11 +184,11 @@ def _fill_from_meta(self, meta, base_meta): if meta is None: meta_attrs = {} else: - meta_attrs = dict( - (k, v) + meta_attrs = { + k: v for (k, v) in vars(meta).items() if not k.startswith('_') - ) + } for option in self._build_default_options(): assert not hasattr(self, option.name), "Can't override field %s." % option.name diff --git a/factory/declarations.py b/factory/declarations.py index 18c1fd51..5afebc75 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -422,7 +422,7 @@ class List(SubFactory): FORCE_SEQUENCE = True def __init__(self, params, list_factory='factory.ListFactory'): - params = dict((str(i), v) for i, v in enumerate(params)) + params = {str(i): v for i, v in enumerate(params)} super(List, self).__init__(list_factory, **params) @@ -457,7 +457,7 @@ def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): 'yes_declaration': enums.get_builder_phase(yes_declaration), 'no_declaration': enums.get_builder_phase(no_declaration), } - used_phases = set(phase for phase in phases.values() if phase is not None) + used_phases = {phase for phase in phases.values() if phase is not None} if len(used_phases) > 1: raise TypeError("Inconsistent phases for %r: %r" % (self, phases)) diff --git a/tests/test_base.py b/tests/test_base.py index 6752d256..99f2f70d 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -279,7 +279,7 @@ class Meta: sub = TestSubFactory.build() alt_parent = TestObjectFactory.build() alt_sub = TestSubFactory.build() - ones = set([x.one for x in (parent, alt_parent, sub, alt_sub)]) + ones = {x.one for x in (parent, alt_parent, sub, alt_sub)} self.assertEqual(4, len(ones)) From e118f300d8f2885fee356b10fc5a98ca13b5a02e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 04:12:32 -0800 Subject: [PATCH 462/714] Remove "Generated by Django ..." comment from example migration The project no longer supports Django 1.10. As the project evolves, so too should the migration. Overtime, it may no longer resemble the original generated form. --- .../django_demo/generic_foreignkey/migrations/0001_initial.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py index 93af4287..7a1d6957 100644 --- a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py +++ b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10 on 2017-04-06 14:33 from __future__ import unicode_literals import django.db.models.deletion From ea37bfeabe24113cc7ddb68b2886395f66541037 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 11 Feb 2020 03:35:26 -0800 Subject: [PATCH 463/714] Remove support for end-of-life Python 2.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python 2.7 went end-of-life 2020-01-01 🎉. It is no longer receiving updates, including for security issues. For additional details on support Python versions, see: Supported: https://devguide.python.org/#status-of-python-branches End-of-life: https://devguide.python.org/devcycle/#end-of-life-branches Removing support for Python 2.7 will reduce testing and maintenance resources while allowing the library to move towards modern Python conventions. Allows the following cleanups: - Remove compat.py modules. The shims are now unnecessary. - Remove mock test dependency. Use unittest.mock instead. - Remove '-*- coding: utf-8 -*-' encoding cookie from source files. Source files default to utf-8 in Python 3. - Remove unnecessary __future__ imports. - Remove '(object)' from class definitions. Python 3 has no old-style classes. - Use Python 3 simplified super() syntax. - Use Python 3 metaclass syntax. Using pypinfo, we can show the PyPI download statistics for the last month. It shows total Python 2 usage below 20% and shrinking. | python_version | percent | download_count | | -------------- | ------: | -------------: | | 3.7 | 38.36% | 369,649 | | 3.6 | 33.88% | 326,559 | | 2.7 | 16.70% | 160,905 | | 3.8 | 6.84% | 65,965 | | 3.5 | 3.84% | 37,011 | | 3.4 | 0.36% | 3,497 | | 3.9 | 0.01% | 102 | | 2.6 | 0.00% | 23 | | 3.3 | 0.00% | 17 | | Total | | 963,728 | --- .travis.yml | 2 - Makefile | 2 +- README.rst | 4 +- docs/changelog.rst | 4 +- docs/conf.py | 16 ++-- docs/examples.rst | 14 +-- docs/reference.rst | 2 +- .../django_demo/generic_foreignkey/apps.py | 2 - .../migrations/0001_initial.py | 3 - .../django_demo/generic_foreignkey/models.py | 4 +- examples/flask_alchemy/demoapp.py | 1 - factory/__init__.py | 1 - factory/alchemy.py | 4 +- factory/base.py | 27 +++--- factory/builder.py | 10 +- factory/compat.py | 54 ----------- factory/declarations.py | 69 +++++++------- factory/django.py | 17 ++-- factory/enums.py | 1 - factory/errors.py | 1 - factory/faker.py | 5 +- factory/fuzzy.py | 26 +++-- factory/helpers.py | 1 - factory/mogo.py | 3 - factory/mongoengine.py | 3 - factory/random.py | 2 - factory/utils.py | 39 ++------ requirements_dev.txt | 1 - setup.cfg | 8 +- setup.py | 1 - tests/alchemyapp/models.py | 1 - tests/alter_time.py | 16 +--- tests/compat.py | 15 --- tests/cyclic/bar.py | 3 +- tests/cyclic/foo.py | 3 +- tests/cyclic/self_ref.py | 3 +- tests/djapp/models.py | 3 +- tests/djapp/settings.py | 1 - tests/test_alchemy.py | 9 +- tests/test_base.py | 9 +- tests/test_declarations.py | 12 +-- tests/test_django.py | 17 ++-- tests/test_docs_internals.py | 16 ++-- tests/test_faker.py | 9 +- tests/test_fuzzy.py | 25 +++-- tests/test_helpers.py | 4 +- tests/test_mongoengine.py | 1 - tests/test_using.py | 95 ++++++++----------- tests/test_utils.py | 14 --- tests/testdata/__init__.py | 1 - tests/utils.py | 9 +- tox.ini | 4 +- 52 files changed, 196 insertions(+), 401 deletions(-) delete mode 100644 factory/compat.py delete mode 100644 tests/compat.py diff --git a/.travis.yml b/.travis.yml index 3050490c..463dc10b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,13 +9,11 @@ install: matrix: include: - - python: "2.7" - python: "3.5" - python: "3.6" - python: "3.7" - python: "3.8" - - python: "pypy" - python: "pypy3" # Documentation diff --git a/Makefile b/Makefile index ef54a982..4de39994 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ test: -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ - -m unittest discover + -m unittest # DOC: Test the examples example-test: diff --git a/README.rst b/README.rst index 93bcba3b..9601bedf 100644 --- a/README.rst +++ b/README.rst @@ -318,8 +318,10 @@ The associated object's strategy will be used: Support Policy -------------- -``factory_boy`` supports Python 2.7, 3.5 to 3.8, as well as PyPy 2.7 and 5.8. +``factory_boy`` supports Python 3.5 to 3.8 as well as PyPy3. +- **Python**'s `supported versions + `__. - **Django**'s `supported versions `__. - **SQLAlchemy**: `latest version on PyPI `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 4bd7f6e3..9ce091c4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,8 +18,8 @@ ChangeLog - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. *Removed:* - - Drop support for Python 3.4. This version `is not maintained anymore `_. - - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `_. + - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. + - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. 2.12.0 (2019-05-11) diff --git a/docs/conf.py b/docs/conf.py index 23aee946..546b6a46 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # Factory Boy documentation build configuration file, created by # sphinx-quickstart on Thu Sep 15 23:51:15 2011. # @@ -54,8 +52,8 @@ master_doc = 'index' # General information about the project. -project = u'Factory Boy' -copyright = u'2011-2015, Raphaël Barrois, Mark Sandstrom' +project = 'Factory Boy' +copyright = '2011-2015, Raphaël Barrois, Mark Sandstrom' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -120,7 +118,7 @@ #html_title = None if 'READTHEDOCS_VERSION' in os.environ: # Use the readthedocs version string in preference to our known version. - html_title = u"{} {} documentation".format( + html_title = "{} {} documentation".format( project, os.environ['READTHEDOCS_VERSION']) # A shorter title for the navigation bar. Default is the same as html_title. @@ -196,8 +194,8 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'FactoryBoy.tex', u'Factory Boy Documentation', - u'Raphaël Barrois, Mark Sandstrom', 'manual'), + ('index', 'FactoryBoy.tex', 'Factory Boy Documentation', + 'Raphaël Barrois, Mark Sandstrom', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -229,8 +227,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'factoryboy', u'Factory Boy Documentation', - [u'Raphaël Barrois, Mark Sandstrom'], 1) + ('index', 'factoryboy', 'Factory Boy Documentation', + ['Raphaël Barrois, Mark Sandstrom'], 1) ] diff --git a/docs/examples.rst b/docs/examples.rst index f9182687..09728a41 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -12,7 +12,7 @@ First, let's define a couple of objects: .. code-block:: python - class Account(object): + class Account: def __init__(self, username, email, date_joined): self.username = username self.email = email @@ -22,7 +22,7 @@ First, let's define a couple of objects: return '%s (%s)' % (self.username, self.email) - class Profile(object): + class Profile: GENDER_MALE = 'm' GENDER_FEMALE = 'f' @@ -35,11 +35,11 @@ First, let's define a couple of objects: self.lastname = lastname self.planet = planet - def __unicode__(self): - return u'%s %s (%s)' % ( - unicode(self.firstname), - unicode(self.lastname), - unicode(self.account.username), + def __str__(self): + return '%s %s (%s)' % ( + self.firstname, + self.lastname, + self.account.username, ) Factories diff --git a/docs/reference.rst b/docs/reference.rst index c9c35ae6..04df196c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -611,7 +611,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. set according to the declarations. -.. class:: StubObject(object) +.. class:: StubObject A plain, stupid object. No method, no helpers, simply a bunch of attributes. diff --git a/examples/django_demo/generic_foreignkey/apps.py b/examples/django_demo/generic_foreignkey/apps.py index 09c30b26..4ca07241 100644 --- a/examples/django_demo/generic_foreignkey/apps.py +++ b/examples/django_demo/generic_foreignkey/apps.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py index 7a1d6957..ce5cf761 100644 --- a/examples/django_demo/generic_foreignkey/migrations/0001_initial.py +++ b/examples/django_demo/generic_foreignkey/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import django.db.models.deletion from django.db import migrations, models diff --git a/examples/django_demo/generic_foreignkey/models.py b/examples/django_demo/generic_foreignkey/models.py index 49e87a7c..912a8d1f 100644 --- a/examples/django_demo/generic_foreignkey/models.py +++ b/examples/django_demo/generic_foreignkey/models.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models @@ -12,5 +10,5 @@ class TaggedItem(models.Model): object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') - def __str__(self): # __unicode__ on Python 2 + def __str__(self): return self.tag diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py index b49bfe84..db57167d 100644 --- a/examples/flask_alchemy/demoapp.py +++ b/examples/flask_alchemy/demoapp.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. from flask import Flask diff --git a/factory/__init__.py b/factory/__init__.py index 20929fac..afcb7cce 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. # Backward compatibility; this should be removed soon. diff --git a/factory/alchemy.py b/factory/alchemy.py index e12c4cd4..26956ef4 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -from __future__ import unicode_literals import warnings @@ -38,7 +36,7 @@ def _check_force_flush(self, meta, value): ) def _build_default_options(self): - return super(SQLAlchemyOptions, self)._build_default_options() + [ + return super()._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), base.OptionDefault( 'sqlalchemy_session_persistence', diff --git a/factory/base.py b/factory/base.py index 95904d98..ddc9ebf9 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -from __future__ import unicode_literals import collections import logging @@ -76,7 +74,7 @@ def __new__(mcs, class_name, bases, attrs): meta = options_class() attrs['_meta'] = meta - new_class = super(FactoryMetaClass, mcs).__new__( + new_class = super().__new__( mcs, class_name, bases, attrs) meta.contribute_to_class( @@ -101,7 +99,7 @@ class BaseMeta: strategy = enums.CREATE_STRATEGY -class OptionDefault(object): +class OptionDefault: """The default for an option. Attributes: @@ -136,7 +134,7 @@ def __str__(self): self.name, self.value, self.inherit) -class FactoryOptions(object): +class FactoryOptions: def __init__(self): self.factory = None self.base_factory = None @@ -387,7 +385,7 @@ def __repr__(self): # Factory base classes -class _Counter(object): +class _Counter: """Simple, naive counter. Attributes: @@ -407,7 +405,7 @@ def reset(self, next_value=0): self.seq = next_value -class BaseFactory(object): +class BaseFactory: """Factory base support for sequences, attributes and stubs.""" # Backwards compatibility @@ -667,23 +665,22 @@ def simple_generate_batch(cls, create, size, **kwargs): return cls.generate_batch(strategy, size, **kwargs) -# Note: we're calling str() on the class name to avoid issues with Py2's type() expecting bytes -# instead of unicode. -Factory = FactoryMetaClass(str('Factory'), (BaseFactory,), { - 'Meta': BaseMeta, - '__doc__': """Factory base with build and create support. +class Factory(BaseFactory, metaclass=FactoryMetaClass): + """Factory base with build and create support. This class has the ability to support multiple ORMs by using custom creation functions. - """, -}) + """ + + class Meta(BaseMeta): + pass # Backwards compatibility Factory.AssociatedClassError = errors.AssociatedClassError -class StubObject(object): +class StubObject: """A generic container.""" def __init__(self, **kwargs): for field, value in kwargs.items(): diff --git a/factory/builder.py b/factory/builder.py index a4798bd7..b494cc30 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -16,7 +16,7 @@ ) -class DeclarationSet(object): +class DeclarationSet: """A set of declarations, including the recursive parameters. Attributes: @@ -198,7 +198,7 @@ def parse_declarations(decls, base_pre=None, base_post=None): return pre_declarations, post_declarations -class BuildStep(object): +class BuildStep: def __init__(self, builder, sequence, parent_step=None): self.builder = builder self.sequence = sequence @@ -229,7 +229,7 @@ def recurse(self, factory, declarations, force_sequence=None): return builder.build(parent_step=self, force_sequence=force_sequence) -class StepBuilder(object): +class StepBuilder: """A factory instantiation step. Attributes: @@ -306,7 +306,7 @@ def recurse(self, factory_meta, extras): return self.__class__(factory_meta, extras, strategy=self.strategy) -class Resolver(object): +class Resolver: """Resolve a set of declarations. Attributes are set at instantiation time, values are computed lazily. @@ -384,6 +384,6 @@ def __getattr__(self, name): def __setattr__(self, name, value): """Prevent setting attributes once __init__ is done.""" if not self.__initialized: - return super(Resolver, self).__setattr__(name, value) + return super().__setattr__(name, value) else: raise AttributeError('Setting of object attributes is not allowed') diff --git a/factory/compat.py b/factory/compat.py deleted file mode 100644 index bce21636..00000000 --- a/factory/compat.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: See the LICENSE file. - - -"""Compatibility tools""" - -import datetime -import sys - -PY2 = (sys.version_info[0] == 2) - -if PY2: # pragma: no cover - def is_string(obj): - return isinstance(obj, (str, unicode)) # noqa - - def force_text(str_or_unicode): - if isinstance(str_or_unicode, unicode): # noqa - return str_or_unicode - return str_or_unicode.decode('utf-8') - -else: # pragma: no cover - def is_string(obj): - return isinstance(obj, str) - - def force_text(text): - return text - - -try: # pragma: no cover - # Python >= 3.2 - UTC = datetime.timezone.utc -except AttributeError: # pragma: no cover - 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/declarations.py b/factory/declarations.py index 5afebc75..32d4b620 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,12 +1,10 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -from __future__ import unicode_literals import itertools import logging -from . import compat, enums, errors, utils +from . import enums, errors, utils logger = logging.getLogger('factory.generate') @@ -64,11 +62,11 @@ class LazyFunction(BaseDeclaration): """ def __init__(self, function, *args, **kwargs): - super(LazyFunction, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.function = function def evaluate(self, instance, step, extra): - logger.debug("LazyFunction: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(step)) + logger.debug("LazyFunction: Evaluating %r on %r", self.function, step) return self.function() @@ -81,15 +79,15 @@ class LazyAttribute(BaseDeclaration): """ def __init__(self, function, *args, **kwargs): - super(LazyAttribute, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.function = function def evaluate(self, instance, step, extra): - logger.debug("LazyAttribute: Evaluating %s on %s", utils.log_repr(self.function), utils.log_repr(instance)) + logger.debug("LazyAttribute: Evaluating %r on %r", self.function, instance) return self.function(instance) -class _UNSPECIFIED(object): +class _UNSPECIFIED: pass @@ -136,7 +134,7 @@ class SelfAttribute(BaseDeclaration): """ def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs): - super(SelfAttribute, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) depth = len(attribute_name) - len(attribute_name.lstrip('.')) attribute_name = attribute_name[depth:] @@ -151,7 +149,7 @@ def evaluate(self, instance, step, extra): else: target = instance - logger.debug("SelfAttribute: Picking attribute %r on %s", self.attribute_name, utils.log_repr(target)) + logger.debug("SelfAttribute: Picking attribute %r on %r", self.attribute_name, target) return deepgetattr(target, self.attribute_name, self.default) def __repr__(self): @@ -173,7 +171,7 @@ class Iterator(BaseDeclaration): """ def __init__(self, iterator, cycle=True, getter=None): - super(Iterator, self).__init__() + super().__init__() self.getter = getter self.iterator = None @@ -188,7 +186,7 @@ def evaluate(self, instance, step, extra): if self.iterator is None: self.iterator = self.iterator_builder() - logger.debug("Iterator: Fetching next value from %s", utils.log_repr(self.iterator)) + logger.debug("Iterator: Fetching next value from %r", self.iterator) value = next(iter(self.iterator)) if self.getter is None: return value @@ -210,7 +208,7 @@ class Sequence(BaseDeclaration): and returning the computed value. """ def __init__(self, function): - super(Sequence, self).__init__() + super().__init__() self.function = function def evaluate(self, instance, step, extra): @@ -229,8 +227,8 @@ class LazyAttributeSequence(Sequence): """ def evaluate(self, instance, step, extra): logger.debug( - "LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%s", - self.function, step.sequence, utils.log_repr(instance)) + "LazyAttributeSequence: Computing next value of %r for seq=%s, obj=%r", + self.function, step.sequence, instance) return self.function(instance, int(step.sequence)) @@ -244,7 +242,7 @@ class ContainerAttribute(BaseDeclaration): not passed in (i.e used outside a SubFactory). """ def __init__(self, function, strict=True, *args, **kwargs): - super(ContainerAttribute, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.function = function self.strict = strict @@ -287,7 +285,7 @@ class ParameteredAttribute(BaseDeclaration): EXTEND_CONTAINERS = False def __init__(self, **kwargs): - super(ParameteredAttribute, self).__init__() + super().__init__() self.defaults = kwargs def _prepare_containers(self, obj, containers=()): @@ -333,7 +331,7 @@ def generate(self, step, params): raise NotImplementedError() -class _FactoryWrapper(object): +class _FactoryWrapper: """Handle a 'factory' arg. Such args can be either a Factory subclass, or a fully qualified import @@ -345,7 +343,7 @@ def __init__(self, factory_or_path): if isinstance(factory_or_path, type): self.factory = factory_or_path else: - if not (compat.is_string(factory_or_path) and '.' in factory_or_path): + if not (isinstance(factory_or_path, str) and '.' in factory_or_path): raise ValueError( "A factory= argument must receive either a class " "or the fully qualified path to a Factory subclass; got " @@ -381,7 +379,7 @@ class SubFactory(ParameteredAttribute): UNROLL_CONTEXT_BEFORE_EVALUATION = False def __init__(self, factory, **kwargs): - super(SubFactory, self).__init__(**kwargs) + super().__init__(**kwargs) self.factory_wrapper = _FactoryWrapper(factory) def get_factory(self): @@ -413,7 +411,7 @@ class Dict(SubFactory): FORCE_SEQUENCE = True def __init__(self, params, dict_factory='factory.DictFactory'): - super(Dict, self).__init__(dict_factory, **dict(params)) + super().__init__(dict_factory, **dict(params)) class List(SubFactory): @@ -423,27 +421,24 @@ class List(SubFactory): def __init__(self, params, list_factory='factory.ListFactory'): params = {str(i): v for i, v in enumerate(params)} - super(List, self).__init__(list_factory, **params) + super().__init__(list_factory, **params) # Parameters # ========== -class Skip(object): +class Skip: def __bool__(self): return False - # Py2 compatibility - __nonzero__ = __bool__ - SKIP = Skip() class Maybe(BaseDeclaration): def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): - super(Maybe, self).__init__() + super().__init__() if enums.get_builder_phase(decider) is None: # No builder phase => flat value @@ -530,7 +525,7 @@ def get_revdeps(self, parameters): class SimpleParameter(Parameter): def __init__(self, value): - super(SimpleParameter, self).__init__() + super().__init__() self.value = value def as_declarations(self, field_name, declarations): @@ -549,7 +544,7 @@ def wrap(cls, value): class Trait(Parameter): """The simplest complex parameter, it enables a bunch of new declarations based on a boolean flag.""" def __init__(self, **overrides): - super(Trait, self).__init__() + super().__init__() self.overrides = overrides def as_declarations(self, field_name, declarations): @@ -603,7 +598,7 @@ def call(self, instance, step, context): # pragma: no cover class PostGeneration(PostGenerationDeclaration): """Calls a given function once the object has been generated.""" def __init__(self, function): - super(PostGeneration, self).__init__() + super().__init__() self.function = function def call(self, instance, step, context): @@ -634,7 +629,7 @@ class RelatedFactory(PostGenerationDeclaration): UNROLL_CONTEXT_BEFORE_EVALUATION = False def __init__(self, factory, factory_related_name='', **defaults): - super(RelatedFactory, self).__init__() + super().__init__() self.name = factory_related_name self.defaults = defaults @@ -650,8 +645,8 @@ def call(self, instance, step, context): if context.value_provided: # The user passed in a custom value logger.debug( - "RelatedFactory: Using provided %s instead of generating %s.%s.", - utils.log_repr(context.value), + "RelatedFactory: Using provided %r instead of generating %s.%s.", + context.value, factory.__module__, factory.__name__, ) return context.value @@ -684,7 +679,7 @@ class RelatedFactoryList(RelatedFactory): def __init__(self, factory, factory_related_name='', size=2, **defaults): self.size = size - super(RelatedFactoryList, self).__init__(factory, factory_related_name, **defaults) + super().__init__(factory, factory_related_name, **defaults) def call(self, instance, step, context): return [super(RelatedFactoryList, self).call(instance, step, context) @@ -710,7 +705,7 @@ class UserFactory(factory.Factory): password = factory.PostGenerationMethodCall('set_pass', password='') """ def __init__(self, method_name, *args, **kwargs): - super(PostGenerationMethodCall, self).__init__() + super().__init__() if len(args) > 1: raise errors.InvalidDeclarationError( "A PostGenerationMethodCall can only handle 1 positional argument; " @@ -733,8 +728,8 @@ def call(self, instance, step, context): kwargs.update(context.extra) method = getattr(instance, self.method_name) logger.debug( - "PostGenerationMethodCall: Calling %s.%s(%s)", - utils.log_repr(instance), + "PostGenerationMethodCall: Calling %r.%s(%s)", + instance, self.method_name, utils.log_pprint(args, kwargs), ) diff --git a/factory/django.py b/factory/django.py index de0c3ba1..e9385eab 100644 --- a/factory/django.py +++ b/factory/django.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """factory_boy extensions for use with the Django framework.""" -from __future__ import absolute_import, unicode_literals import functools import io @@ -12,7 +10,6 @@ import os from . import base, declarations, errors -from .compat import is_string try: import django @@ -65,13 +62,13 @@ def _get_model(app, model): class DjangoOptions(base.FactoryOptions): def _build_default_options(self): - return super(DjangoOptions, self)._build_default_options() + [ + return super()._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True), ] def _get_counter_reference(self): - counter_reference = super(DjangoOptions, self)._get_counter_reference() + counter_reference = super()._get_counter_reference() if (counter_reference == self.base_factory and self.base_factory._meta.model is not None and self.base_factory._meta.model._meta.abstract @@ -83,7 +80,7 @@ def _get_counter_reference(self): return counter_reference def get_model_class(self): - if is_string(self.model) and '.' in self.model: + if isinstance(self.model, str) and '.' in self.model: app, model_name = self.model.split('.', 1) self.model = get_model(app, model_name) @@ -107,7 +104,7 @@ class Meta: @classmethod def _load_model_class(cls, definition): - if is_string(definition) and '.' in definition: + if isinstance(definition, str) and '.' in definition: app, model = definition.split('.', 1) return get_model(app, model) @@ -135,7 +132,7 @@ def _generate(cls, strategy, params): # Original params are used in _get_or_create if it cannot build an # object initially due to an IntegrityError being raised cls._original_params = params - return super(DjangoModelFactory, cls)._generate(strategy, params) + return super()._generate(strategy, params) @classmethod def _get_or_create(cls, model_class, *args, **kwargs): @@ -201,7 +198,7 @@ class FileField(declarations.ParameteredAttribute): def __init__(self, **defaults): require_django() - super(FileField, self).__init__(**defaults) + super().__init__(**defaults) def _make_data(self, params): """Create data for the field.""" @@ -273,7 +270,7 @@ def _make_data(self, params): return thumb_io.getvalue() -class mute_signals(object): +class mute_signals: """Temporarily disables and then restores any django signals. Args: diff --git a/factory/enums.py b/factory/enums.py index 116e3152..02f686e3 100644 --- a/factory/enums.py +++ b/factory/enums.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. # Strategies diff --git a/factory/errors.py b/factory/errors.py index 63d75487..06e8aab2 100644 --- a/factory/errors.py +++ b/factory/errors.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. diff --git a/factory/faker.py b/factory/faker.py index 76ee6a18..6f10b785 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. @@ -14,8 +13,6 @@ class Meta: """ -from __future__ import absolute_import, unicode_literals - import contextlib import faker @@ -39,7 +36,7 @@ class Faker(declarations.BaseDeclaration): >>> foo = factory.Faker('name') """ def __init__(self, provider, **kwargs): - super(Faker, self).__init__() + super().__init__() self.provider = provider self.provider_kwargs = kwargs self.locale = kwargs.pop('locale', None) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index a979c3f3..d96f85a4 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -1,17 +1,15 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Additional declarations for "fuzzy" attribute definitions.""" -from __future__ import unicode_literals import datetime import decimal import string import warnings -from . import compat, declarations, random +from . import declarations, random random_seed_warning = ( "Setting a specific random seed for {} can still have varying results " @@ -72,7 +70,7 @@ class FuzzyAttribute(BaseFuzzyAttribute): """ def __init__(self, fuzzer, **kwargs): - super(FuzzyAttribute, self).__init__(**kwargs) + super().__init__(**kwargs) self.fuzzer = fuzzer def fuzz(self): @@ -97,7 +95,7 @@ class FuzzyText(BaseFuzzyAttribute): """ def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters, **kwargs): - super(FuzzyText, self).__init__(**kwargs) + super().__init__(**kwargs) self.prefix = prefix self.suffix = suffix self.length = length @@ -121,7 +119,7 @@ def __init__(self, choices, getter=None, **kwargs): self.choices = None self.choices_generator = choices self.getter = getter - super(FuzzyChoice, self).__init__(**kwargs) + super().__init__(**kwargs) def fuzz(self): if self.choices is None: @@ -144,7 +142,7 @@ def __init__(self, low, high=None, step=1, **kwargs): self.high = high self.step = step - super(FuzzyInteger, self).__init__(**kwargs) + super().__init__(**kwargs) def fuzz(self): return random.randgen.randrange(self.low, self.high + 1, self.step) @@ -162,7 +160,7 @@ def __init__(self, low, high=None, precision=2, **kwargs): self.high = high self.precision = precision - super(FuzzyDecimal, self).__init__(**kwargs) + super().__init__(**kwargs) def fuzz(self): base = decimal.Decimal(str(random.randgen.uniform(self.low, self.high))) @@ -181,7 +179,7 @@ def __init__(self, low, high=None, precision=15, **kwargs): self.high = high self.precision = precision - super(FuzzyFloat, self).__init__(**kwargs) + super().__init__(**kwargs) def fuzz(self): base = random.randgen.uniform(self.low, self.high) @@ -192,7 +190,7 @@ class FuzzyDate(BaseFuzzyAttribute): """Random date within a given date range.""" def __init__(self, start_date, end_date=None, **kwargs): - super(FuzzyDate, self).__init__(**kwargs) + super().__init__(**kwargs) if end_date is None: if random.randgen.state_set: cls_name = self.__class__.__name__ @@ -230,7 +228,7 @@ 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) + super().__init__(**kwargs) if end_dt is None: if random.randgen.state_set: @@ -293,7 +291,7 @@ def _check_bounds(self, start_dt, end_dt): raise ValueError( "FuzzyNaiveDateTime only handles naive datetimes, got end=%r" % end_dt) - super(FuzzyNaiveDateTime, self)._check_bounds(start_dt, end_dt) + super()._check_bounds(start_dt, end_dt) class FuzzyDateTime(BaseFuzzyDateTime): @@ -304,7 +302,7 @@ class FuzzyDateTime(BaseFuzzyDateTime): """ def _now(self): - return datetime.datetime.now(tz=compat.UTC) + return datetime.datetime.now(tz=datetime.timezone.utc) def _check_bounds(self, start_dt, end_dt): if start_dt.tzinfo is None: @@ -315,4 +313,4 @@ def _check_bounds(self, start_dt, end_dt): raise ValueError( "FuzzyDateTime requires timezone-aware datetimes, got end=%r" % end_dt) - super(FuzzyDateTime, self)._check_bounds(start_dt, end_dt) + super()._check_bounds(start_dt, end_dt) diff --git a/factory/helpers.py b/factory/helpers.py index 5433b319..496de6e3 100644 --- a/factory/helpers.py +++ b/factory/helpers.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. diff --git a/factory/mogo.py b/factory/mogo.py index 9c167a79..f886ae14 100644 --- a/factory/mogo.py +++ b/factory/mogo.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """factory_boy extensions for use with the mogo library (pymongo wrapper).""" -from __future__ import unicode_literals - from . import base diff --git a/factory/mongoengine.py b/factory/mongoengine.py index 10cc1deb..eb4a8dc5 100644 --- a/factory/mongoengine.py +++ b/factory/mongoengine.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """factory_boy extensions for use with the mongoengine library (pymongo wrapper).""" -from __future__ import unicode_literals - from . import base diff --git a/factory/random.py b/factory/random.py index 75a546a3..73b34f09 100644 --- a/factory/random.py +++ b/factory/random.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import random import faker.generator diff --git a/factory/utils.py b/factory/utils.py index f2b7aa54..a74e0b35 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -from __future__ import unicode_literals import collections - -from . import compat +import importlib def import_object(module_name, attribute_name): @@ -15,19 +12,11 @@ def import_object(module_name, attribute_name): >>> import_object('datetime', 'datetime') """ - # Py2 compatibility: force str (i.e bytes) when importing. - module = __import__(str(module_name), {}, {}, [str(attribute_name)], 0) - return getattr(module, str(attribute_name)) - + module = importlib.import_module(module_name) + return getattr(module, attribute_name) -def _safe_repr(obj): - try: - return log_repr(obj) - except Exception: - return '' % id(obj) - -class log_pprint(object): +class log_pprint: """Helper for properly printing args / kwargs passed to an object. Since it is only used with factory.debug(), the computation is @@ -45,26 +34,18 @@ def __repr__(self): def __str__(self): return ', '.join( [ - _safe_repr(arg) for arg in self.args + repr(arg) for arg in self.args ] + [ - '%s=%s' % (key, _safe_repr(value)) + '%s=%s' % (key, repr(value)) for key, value in self.kwargs.items() ] ) -def log_repr(obj): - """Generate a text-compatible repr of an object. - - Some projects have a tendency to generate bytes-style repr in Python2. - """ - return compat.force_text(repr(obj)) - - -class ResetableIterator(object): +class ResetableIterator: """An iterator wrapper that can be 'reset()' to its start.""" def __init__(self, iterator, **kwargs): - super(ResetableIterator, self).__init__(**kwargs) + super().__init__(**kwargs) self.iterator = iter(iterator) self.past_elements = collections.deque() self.next_elements = collections.deque() @@ -87,7 +68,7 @@ def reset(self): self.next_elements.extend(self.past_elements) -class OrderedBase(object): +class OrderedBase: """Marks a class as being ordered. Each instance (even from subclasses) will share a global creation counter. @@ -96,7 +77,7 @@ class OrderedBase(object): CREATION_COUNTER_FIELD = '_creation_counter' def __init__(self, **kwargs): - super(OrderedBase, self).__init__(**kwargs) + super().__init__(**kwargs) if type(self) is not OrderedBase: self.touch_creation_counter() diff --git a/requirements_dev.txt b/requirements_dev.txt index 313d25f7..50b25220 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -5,7 +5,6 @@ coverage Django flake8 isort -mock;python_version<"3" Pillow SQLAlchemy mongoengine diff --git a/setup.cfg b/setup.cfg index 88dd92f1..8136e1ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,3 @@ -[bdist_wheel] -universal = 1 - [metadata] name = factory_boy version = 2.12.1.dev0 @@ -23,9 +20,8 @@ classifiers = License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 @@ -38,7 +34,7 @@ classifiers = [options] zip_safe = false packages = factory -python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* +python_requires = >=3.5 install_requires = Faker>=0.7.0 [options.extras_require] diff --git a/setup.py b/setup.py index 8b95793a..beda28e8 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- from setuptools import setup diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index c6e1838a..e31e9034 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. diff --git a/tests/alter_time.py b/tests/alter_time.py index 9cf391b1..3da968d5 100644 --- a/tests/alter_time.py +++ b/tests/alter_time.py @@ -1,14 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # This code is in the public domain # Author: Raphaël Barrois -from __future__ import print_function - import datetime - -from .compat import mock +from unittest import mock real_datetime_class = datetime.datetime @@ -35,7 +31,7 @@ class DatetimeSubclassMeta(type): def __instancecheck__(mcs, obj): return isinstance(obj, real_datetime_class) - class BaseMockedDatetime(real_datetime_class): + class MockedDatetime(real_datetime_class, metaclass=DatetimeSubclassMeta): @classmethod def now(cls, tz=None): return target.replace(tzinfo=tz) @@ -44,9 +40,6 @@ def now(cls, tz=None): def utcnow(cls): return target - # Python2 & Python3-compatible metaclass - MockedDatetime = DatetimeSubclassMeta('datetime', (BaseMockedDatetime,), {}) - return mock.patch.object(datetime_module, 'datetime', MockedDatetime) @@ -74,14 +67,11 @@ class DateSubclassMeta(type): def __instancecheck__(mcs, obj): return isinstance(obj, real_date_class) - class BaseMockedDate(real_date_class): + class MockedDate(real_date_class, metaclass=DateSubclassMeta): @classmethod def today(cls): return target - # Python2 & Python3-compatible metaclass - MockedDate = DateSubclassMeta('date', (BaseMockedDate,), {}) - return mock.patch.object(datetime_module, 'date', MockedDate) diff --git a/tests/compat.py b/tests/compat.py deleted file mode 100644 index 08fdf06e..00000000 --- a/tests/compat.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright: See the LICENSE file. - -"""Compatibility tools for tests""" - -import sys - -is_python2 = (sys.version_info[0] == 2) - -if is_python2: - import StringIO as io - import mock -else: - import io # noqa: F401 - from unittest import mock # noqa: F401 diff --git a/tests/cyclic/bar.py b/tests/cyclic/bar.py index bbc420ed..6985a11f 100644 --- a/tests/cyclic/bar.py +++ b/tests/cyclic/bar.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Helper to test circular factory dependencies.""" @@ -6,7 +5,7 @@ import factory -class Bar(object): +class Bar: def __init__(self, foo, y): self.foo = foo self.y = y diff --git a/tests/cyclic/foo.py b/tests/cyclic/foo.py index 8c0a92a9..31ec02cc 100644 --- a/tests/cyclic/foo.py +++ b/tests/cyclic/foo.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Helper to test circular factory dependencies.""" @@ -8,7 +7,7 @@ from . import bar as bar_mod -class Foo(object): +class Foo: def __init__(self, bar, x): self.bar = bar self.x = x diff --git a/tests/cyclic/self_ref.py b/tests/cyclic/self_ref.py index bf757eb2..b0111b07 100644 --- a/tests/cyclic/self_ref.py +++ b/tests/cyclic/self_ref.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Helper to test circular factory dependencies.""" @@ -6,7 +5,7 @@ import factory -class TreeElement(object): +class TreeElement: def __init__(self, name, parent): self.parent = parent self.name = name diff --git a/tests/djapp/models.py b/tests/djapp/models.py index bba53d57..1373c771 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Helpers for testing django apps.""" @@ -102,7 +101,7 @@ class WithSignals(models.Model): class CustomManager(models.Manager): def create(self, arg=None, **kwargs): - return super(CustomManager, self).create(**kwargs) + return super().create(**kwargs) class WithCustomManager(models.Model): diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index fb91d2cf..d14e3e5c 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Settings for factory_boy/Django tests.""" diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 72544e84..a08c4815 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -1,16 +1,15 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Tests for factory_boy/SQLAlchemy interactions.""" import unittest import warnings +from unittest import mock import factory from factory.alchemy import SQLAlchemyModelFactory from .alchemyapp import models -from .compat import mock class StandardFactory(SQLAlchemyModelFactory): @@ -41,7 +40,7 @@ class Meta: class SQLAlchemyPkSequenceTestCase(unittest.TestCase): def setUp(self): - super(SQLAlchemyPkSequenceTestCase, self).setUp() + super().setUp() StandardFactory.reset_sequence(1) NonIntegerPkFactory._meta.sqlalchemy_session.rollback() @@ -78,7 +77,7 @@ def test_pk_force_value(self): class SQLAlchemySessionPersistenceTestCase(unittest.TestCase): def setUp(self): - super(SQLAlchemySessionPersistenceTestCase, self).setUp() + super().setUp() self.mock_session = mock.NonCallableMagicMock(spec=models.session) def test_flushing(self): @@ -158,7 +157,7 @@ class Meta: class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): - super(SQLAlchemyNonIntegerPkTestCase, self).setUp() + super().setUp() NonIntegerPkFactory.reset_sequence() NonIntegerPkFactory._meta.sqlalchemy_session.rollback() diff --git a/tests/test_base.py b/tests/test_base.py index 99f2f70d..4759bd1e 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. import unittest @@ -6,7 +5,7 @@ from factory import base, declarations, enums, errors -class TestObject(object): +class TestObject: def __init__(self, one=None, two=None, three=None, four=None): self.one = one self.two = two @@ -14,7 +13,7 @@ def __init__(self, one=None, two=None, three=None, four=None): self.four = four -class FakeDjangoModel(object): +class FakeDjangoModel: @classmethod def create(cls, **kwargs): instance = cls(**kwargs) @@ -285,7 +284,7 @@ class Meta: class FactorySequenceTestCase(unittest.TestCase): def setUp(self): - super(FactorySequenceTestCase, self).setUp() + super().setUp() class TestObjectFactory(base.Factory): class Meta: @@ -507,7 +506,7 @@ class Meta: @classmethod def _generate(cls, create, attrs): attrs['four'] = 4 - return super(TestModelFactory, cls)._generate(create, attrs) + return super()._generate(create, attrs) b = TestModelFactory.build(one=1) self.assertEqual(1, b.one) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 2e6474d9..3b9cfd1f 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -1,13 +1,12 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. import datetime import unittest +from unittest import mock from factory import base, declarations, errors, helpers from . import utils -from .compat import mock class OrderedDeclarationTestCase(unittest.TestCase): @@ -17,7 +16,7 @@ def test_errors(self): class DigTestCase(unittest.TestCase): - class MyObj(object): + class MyObj: def __init__(self, n): self.n = n @@ -44,13 +43,6 @@ def test_chaining(self): class MaybeTestCase(unittest.TestCase): - @classmethod - def setUpClass(cls): - # remove after dropping python 2 - import sys - if int(sys.version[0]) == 2: - cls.assertRaisesRegex = cls.assertRaisesRegexp - def test_init(self): declarations.Maybe('foo', 1, 2) diff --git a/tests/test_django.py b/tests/test_django.py index 14b2e9d7..8c966534 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Tests for factory_boy/Django interactions.""" @@ -6,6 +5,7 @@ import io import os import unittest +from unittest import mock import django from django import test as django_test @@ -17,7 +17,6 @@ import factory from . import testdata -from .compat import mock try: from PIL import Image @@ -163,7 +162,7 @@ class Meta: class DjangoPkSequenceTestCase(django_test.TestCase): def setUp(self): - super(DjangoPkSequenceTestCase, self).setUp() + super().setUp() StandardFactory.reset_sequence() def test_pk_first(self): @@ -253,7 +252,7 @@ def test_unique_field_not_in_get_or_create(self): class DjangoPkForceTestCase(django_test.TestCase): def setUp(self): - super(DjangoPkForceTestCase, self).setUp() + super().setUp() StandardFactoryWithPKField.reset_sequence() def test_no_pk(self): @@ -337,7 +336,7 @@ class Meta: class DjangoNonIntegerPkTestCase(django_test.TestCase): def setUp(self): - super(DjangoNonIntegerPkTestCase, self).setUp() + super().setUp() NonIntegerPkFactory.reset_sequence() def test_first(self): @@ -403,7 +402,7 @@ class DjangoRelatedFieldTestCase(django_test.TestCase): @classmethod def setUpClass(cls): - super(DjangoRelatedFieldTestCase, cls).setUpClass() + super().setUpClass() class PointedFactory(factory.django.DjangoModelFactory): class Meta: @@ -496,7 +495,7 @@ def test_create_pointed_related_with_trait(self): class DjangoFileFieldTestCase(django_test.TestCase): def tearDown(self): - super(DjangoFileFieldTestCase, self).tearDown() + super().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)) @@ -673,7 +672,7 @@ class Meta: class DjangoImageFieldTestCase(django_test.TestCase): def tearDown(self): - super(DjangoImageFieldTestCase, self).tearDown() + super().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)) @@ -954,7 +953,7 @@ def foo(): self.assertSignalsReactivated() def test_classmethod_decorator(self): - class Foo(object): + class Foo: @classmethod @factory.django.mute_signals(signals.pre_save, signals.post_save) def generate(cls): diff --git a/tests/test_docs_internals.py b/tests/test_docs_internals.py index 82a3e4e6..a869a4d9 100644 --- a/tests/test_docs_internals.py +++ b/tests/test_docs_internals.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2011-2015 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -25,10 +24,9 @@ import factory import factory.fuzzy -from factory.compat import UTC -class User(object): +class User: def __init__( self, username, @@ -52,7 +50,7 @@ def log(self, action, timestamp): UserLog(user=self, action=action, timestamp=timestamp) -class UserLog(object): +class UserLog: ACTIONS = ['create', 'update', 'disable'] @@ -70,7 +68,7 @@ class Meta: user = factory.SubFactory('test_docs_internals.UserFactory') timestamp = factory.fuzzy.FuzzyDateTime( - datetime.datetime(2000, 1, 1, tzinfo=UTC), + datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc), ) action = factory.Iterator(UserLog.ACTIONS) @@ -92,8 +90,8 @@ class Params: username = factory.Faker('user_name') full_name = factory.Faker('name') creation_date = factory.fuzzy.FuzzyDateTime( - datetime.datetime(2000, 1, 1, tzinfo=UTC), - datetime.datetime(2015, 12, 31, 20, tzinfo=UTC) + datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc), + datetime.datetime(2015, 12, 31, 20, tzinfo=datetime.timezone.utc) ) # Conditional flags @@ -102,8 +100,8 @@ class Params: 'enabled', None, factory.fuzzy.FuzzyDateTime( - datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=10), - datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=1), + datetime.datetime.now().replace(tzinfo=datetime.timezone.utc) - datetime.timedelta(days=10), + datetime.datetime.now().replace(tzinfo=datetime.timezone.utc) - datetime.timedelta(days=1), ), ) diff --git a/tests/test_faker.py b/tests/test_faker.py index 202b8c9a..30cd5a59 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. import random @@ -9,7 +8,7 @@ import factory -class MockFaker(object): +class MockFaker: def __init__(self, expected): self.expected = expected self.random = random.Random() @@ -37,7 +36,7 @@ def test_simple_biased(self): self.assertEqual("John Doe", faker_field.generate()) def test_full_factory(self): - class Profile(object): + class Profile: def __init__(self, first_name, last_name, email): self.first_name = first_name self.last_name = last_name @@ -59,7 +58,7 @@ class Meta: self.assertEqual('john.doe@example.org', profile.email) def test_override_locale(self): - class Profile(object): + class Profile: def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name @@ -89,7 +88,7 @@ class Meta: self.assertEqual("Valjean", profile.last_name) def test_add_provider(self): - class Face(object): + class Face: def __init__(self, smiley, french_smiley): self.smiley = smiley self.french_smiley = french_smiley diff --git a/tests/test_fuzzy.py b/tests/test_fuzzy.py index d6344a46..67cf7030 100644 --- a/tests/test_fuzzy.py +++ b/tests/test_fuzzy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. @@ -6,11 +5,11 @@ import decimal import unittest import warnings +from unittest import mock -from factory import compat, fuzzy, random +from factory import fuzzy, random from . import utils -from .compat import mock class FuzzyAttributeTestCase(unittest.TestCase): @@ -44,8 +43,7 @@ def test_mock(self): def test_generator(self): def options(): - for i in range(3): - yield i + yield from range(3) d = fuzzy.FuzzyChoice(options()) @@ -57,7 +55,7 @@ def options(): self.assertIn(res, [0, 1, 2]) def test_lazy_generator(self): - class Gen(object): + class Gen: def __init__(self, options): self.options = options self.unrolled = False @@ -183,7 +181,6 @@ def test_precision(self): self.assertEqual(decimal.Decimal('8.001').quantize(decimal.Decimal(10) ** -3), res) - @unittest.skipIf(compat.PY2, "decimal.FloatOperation was added in Py3") def test_no_approximation(self): """We should not go through floats in our fuzzy calls unless actually needed.""" fuzz = fuzzy.FuzzyDecimal(0, 10) @@ -346,12 +343,12 @@ def test_partial_definition(self): def test_aware_start(self): """Tests that a timezone-aware start datetime is rejected.""" with self.assertRaises(ValueError): - fuzzy.FuzzyNaiveDateTime(self.jan1.replace(tzinfo=compat.UTC), self.jan31) + fuzzy.FuzzyNaiveDateTime(self.jan1.replace(tzinfo=datetime.timezone.utc), self.jan31) def test_aware_end(self): """Tests that a timezone-aware end datetime is rejected.""" with self.assertRaises(ValueError): - fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31.replace(tzinfo=compat.UTC)) + fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31.replace(tzinfo=datetime.timezone.utc)) def test_force_year(self): fuzz = fuzzy.FuzzyNaiveDateTime(self.jan1, self.jan31, force_year=4) @@ -438,9 +435,9 @@ 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) + cls.jan1 = datetime.datetime(2013, 1, 1, tzinfo=datetime.timezone.utc) + cls.jan3 = datetime.datetime(2013, 1, 3, tzinfo=datetime.timezone.utc) + cls.jan31 = datetime.datetime(2013, 1, 31, tzinfo=datetime.timezone.utc) def test_accurate_definition(self): """Tests explicit definition of a FuzzyDateTime.""" @@ -538,7 +535,7 @@ def test_biased(self): with mock.patch('factory.random.randgen.randint', fake_randint): res = utils.evaluate_declaration(fuzz) - self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=compat.UTC), res) + self.assertEqual(datetime.datetime(2013, 1, 16, tzinfo=datetime.timezone.utc), res) def test_biased_partial(self): """Tests a FuzzyDate with a biased random and implicit upper bound.""" @@ -549,7 +546,7 @@ def test_biased_partial(self): with mock.patch('factory.random.randgen.randint', fake_randint): res = utils.evaluate_declaration(fuzz) - self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=compat.UTC), res) + self.assertEqual(datetime.datetime(2013, 1, 2, tzinfo=datetime.timezone.utc), res) class FuzzyTextTestCase(unittest.TestCase): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 57b45edd..2c187448 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. +import io import logging import unittest from factory import helpers -from .compat import io - class DebugTest(unittest.TestCase): """Tests for the 'factory.debug()' helper.""" diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 2b4cf2c7..bc1915b0 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Tests for factory_boy/MongoEngine interactions.""" diff --git a/tests/test_using.py b/tests/test_using.py index 2899e1cc..a1351c8d 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. """Tests using factory.""" @@ -14,10 +13,9 @@ from factory import errors from . import utils -from .compat import is_python2 -class TestObject(object): +class TestObject: def __init__(self, one=None, two=None, three=None, four=None, five=None): self.one = one self.two = two @@ -35,7 +33,7 @@ def as_dict(self): ) -class Dummy(object): +class Dummy: def __init__(self, **kwargs): self._fields = set(kwargs) for k, v in kwargs.items(): @@ -52,14 +50,14 @@ def __repr__(self): ) -class FakeModel(object): +class FakeModel: @classmethod def create(cls, **kwargs): instance = cls(**kwargs) instance.id = 1 return instance - class FakeModelManager(object): + class FakeModelManager: def get_or_create(self, **kwargs): defaults = kwargs.pop('defaults', {}) kwargs.update(defaults) @@ -448,7 +446,7 @@ def _create(cls, model_class, *args, **kwargs): self.assertTrue(obj.properly_created) def test_non_django_create(self): - class NonDjango(object): + class NonDjango: def __init__(self, x, y=2): self.x = x self.y = y @@ -520,7 +518,7 @@ def one(a): self.assertEqual(test_object.one, 'one') def test_self_attribute(self): - class TmpObj(object): + class TmpObj: n = 3 class TestObjectFactory(factory.Factory): @@ -903,7 +901,7 @@ def test_inheritance_sequence_unrelated_objects(self): until the "slave" factory has been called. """ - class TestObject2(object): + class TestObject2: def __init__(self, one): self.one = one @@ -989,7 +987,7 @@ def alt_create(**kwargs): self.assertEqual(TestObjectFactory.alt_create(foo=1), {"foo": 1}) def test_inline_args(self): - class TestObject(object): + class TestObject: def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs @@ -1009,7 +1007,7 @@ class Meta: self.assertEqual({'z': 5, 't': 4}, obj.kwargs) def test_exclude(self): - class TestObject(object): + class TestObject: def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs @@ -1029,7 +1027,7 @@ class Meta: self.assertEqual({'y': 2, 't': 4}, obj.kwargs) def test_exclude_and_inline_args(self): - class TestObject(object): + class TestObject: def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs @@ -1052,7 +1050,7 @@ class Meta: class NonKwargParametersTestCase(unittest.TestCase): def test_build(self): - class TestObject(object): + class TestObject: def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs @@ -1071,7 +1069,7 @@ class Meta: self.assertEqual({'three': 3}, obj.kwargs) def test_create(self): - class TestObject(object): + class TestObject: def __init__(self, *args, **kwargs): self.args = None self.kwargs = None @@ -1105,7 +1103,7 @@ class KwargAdjustTestCase(unittest.TestCase): """Tests for the _adjust_kwargs method.""" def test_build(self): - class TestObject(object): + class TestObject: def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs @@ -1124,7 +1122,7 @@ def _adjust_kwargs(cls, **kwargs): self.assertEqual((), obj.args) def test_rename(self): - class TestObject(object): + class TestObject: def __init__(self, attributes=None): self.attributes = attributes @@ -1140,7 +1138,7 @@ class Meta: def test_rename_non_existent_kwarg(self): # see https://github.com/FactoryBoy/factory_boy/issues/504 - class TestObject(object): + class TestObject: def __init__(self, attributes=None): self.attributes = attributes @@ -1538,7 +1536,7 @@ class Meta: self.assertEqual('parent child', test_model.child.one) def test_sub_factory_and_sequence(self): - class TestObject(object): + class TestObject: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -1561,7 +1559,7 @@ class Meta: self.assertEqual(1, wrapping.wrapped.one) def test_sub_factory_overriding(self): - class TestObject(object): + class TestObject: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -1570,7 +1568,7 @@ class TestObjectFactory(factory.Factory): class Meta: model = TestObject - class OtherTestObject(object): + class OtherTestObject: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -1618,7 +1616,7 @@ class Meta: def test_nested_sub_factory(self): """Test nested sub-factories.""" - class TestObject(object): + class TestObject: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -1647,7 +1645,7 @@ class Meta: def test_nested_sub_factory_with_overridden_sub_factories(self): """Test nested sub-factories, with attributes overridden with subfactories.""" - class TestObject(object): + class TestObject: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -1681,7 +1679,7 @@ def test_nested_subfactory_with_override(self): """Tests replacing a SubFactory field with an actual value.""" # The test class - class TestObject(object): + class TestObject: def __init__(self, two='one', wrapped=None): self.two = two self.wrapped = wrapped @@ -1708,7 +1706,7 @@ class Meta: def test_deep_nested_subfactory(self): counter = iter(range(100)) - class Node(object): + class Node: def __init__(self, label, child=None): self.id = next(counter) self.label = label @@ -1755,7 +1753,7 @@ class Meta: def test_sub_factory_and_inheritance(self): """Test inheriting from a factory with subfactories, overriding.""" - class TestObject(object): + class TestObject: def __init__(self, **kwargs): for k, v in kwargs.items(): setattr(self, k, v) @@ -1781,20 +1779,20 @@ class ExtendedWrappingTestObjectFactory(WrappingTestObjectFactory): def test_diamond_sub_factory(self): """Tests the case where an object has two fields with a common field.""" - class InnerMost(object): + class InnerMost: def __init__(self, a, b): self.a = a self.b = b - class SideA(object): + class SideA: def __init__(self, inner_from_a): self.inner_from_a = inner_from_a - class SideB(object): + class SideB: def __init__(self, inner_from_b): self.inner_from_b = inner_from_b - class OuterMost(object): + class OuterMost: def __init__(self, foo, side_a, side_b): self.foo = foo self.side_a = side_a @@ -1940,20 +1938,6 @@ class Meta: for i, obj in enumerate(objs): self.assertEqual(i + 10, obj.one) - @unittest.skipUnless(is_python2, "Scope bleeding fixed in Python3+") - @utils.disable_warnings - def test_iterator_list_comprehension_scope_bleeding(self): - class TestObjectFactory(factory.Factory): - class Meta: - model = TestObject - - one = factory.Iterator([j * 3 for j in range(5)]) - - # Scope bleeding: j will end up in TestObjectFactory's scope. - - with self.assertRaises(TypeError): - TestObjectFactory.build() - @utils.disable_warnings def test_iterator_list_comprehension_protected(self): class TestObjectFactory(factory.Factory): @@ -1976,8 +1960,7 @@ class Meta: @factory.iterator def one(): - for i in range(10, 50): # pragma: no cover - yield i + yield from range(10, 50) objs = TestObjectFactory.build_batch(20) @@ -1990,7 +1973,7 @@ def test_iterator_late_loading(self): This allows, for Django objects, to call: foo = factory.Iterator(models.MyThingy.objects.all()) """ - class DBRequest(object): + class DBRequest: def __init__(self): self.ready = False @@ -2033,7 +2016,7 @@ def one(): self.assertEqual(obj1.one, obj3.one) -class BetterFakeModelManager(object): +class BetterFakeModelManager: def __init__(self, keys, instance): self.keys = keys self.instance = instance @@ -2051,7 +2034,7 @@ def using(self, db): return self -class BetterFakeModel(object): +class BetterFakeModel: @classmethod def create(cls, **kwargs): instance = cls(**kwargs) @@ -2342,7 +2325,7 @@ class Params: self.assertEqual(1 + 1 * 4, obj.one) def test_post_generation_method_call(self): - class TestObject(object): + class TestObject: def __init__(self, one=None, two=None): self.one = one self.two = two @@ -2392,7 +2375,7 @@ def test_related_factory_list_of_varying_size(self): related_list_sizes = [5, 5, 4, 4, 3, 3, 2, 2, 1, 1] RELATED_LIST_SIZE = lambda: related_list_sizes.pop() - class TestRelatedObject(object): + class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): # Mock out the 'List of Related Objects' generated by RelatedFactoryList if hasattr(obj, 'related_list'): @@ -2457,7 +2440,7 @@ class Meta: def test_related_factory_list_of_static_size(self): RELATED_LIST_SIZE = 4 - class TestRelatedObject(object): + class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): # Mock out the 'List of Related Objects' generated by RelatedFactoryList if hasattr(obj, 'related_list'): @@ -2517,7 +2500,7 @@ class Meta: self.assertEqual(obj, related_obj.three) def test_related_factory(self): - class TestRelatedObject(object): + class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): obj.related = self self.one = one @@ -2565,7 +2548,7 @@ class Meta: def test_related_factory_no_name(self): relateds = [] - class TestRelatedObject(object): + class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): relateds.append(self) self.one = one @@ -2610,7 +2593,7 @@ class Meta: self.assertEqual(4, related.two) def test_related_factory_selfattribute(self): - class TestRelatedObject(object): + class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): obj.related = self self.one = one @@ -2641,7 +2624,7 @@ class Meta: self.assertEqual(4, obj.related.two) def test_parameterized_related_factory(self): - class TestRelatedObject(object): + class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): obj.related = self self.one = one @@ -2680,7 +2663,7 @@ class RelatedFactoryExtractionTestCase(unittest.TestCase): def setUp(self): self.relateds = [] - class TestRelatedObject(object): + class TestRelatedObject: def __init__(subself, obj): self.relateds.append(subself) subself.obj = obj diff --git a/tests/test_utils.py b/tests/test_utils.py index f10ec0a7..1d54eefe 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,15 +1,11 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. -from __future__ import unicode_literals import itertools import unittest from factory import utils -from .compat import is_python2 - class ImportObjectTestCase(unittest.TestCase): def test_datetime(self): @@ -43,33 +39,23 @@ def test_only_kwargs(self): def test_bytes_args(self): txt = str(utils.log_pprint((b'\xe1\xe2',))) expected = "b'\\xe1\\xe2'" - if is_python2: - expected = expected.lstrip('b') self.assertEqual(expected, txt) def test_text_args(self): txt = str(utils.log_pprint(('ŧêßŧ',))) expected = "'ŧêßŧ'" - if is_python2: - expected = "u'\\u0167\\xea\\xdf\\u0167'" self.assertEqual(expected, txt) def test_bytes_kwargs(self): txt = str(utils.log_pprint(kwargs={'x': b'\xe1\xe2', 'y': b'\xe2\xe1'})) expected1 = "x=b'\\xe1\\xe2', y=b'\\xe2\\xe1'" expected2 = "y=b'\\xe2\\xe1', x=b'\\xe1\\xe2'" - if is_python2: - expected1 = expected1.replace('b', '') - expected2 = expected2.replace('b', '') self.assertIn(txt, (expected1, expected2)) def test_text_kwargs(self): txt = str(utils.log_pprint(kwargs={'x': 'ŧêßŧ', 'y': 'ŧßêŧ'})) expected1 = "x='ŧêßŧ', y='ŧßêŧ'" expected2 = "y='ŧßêŧ', x='ŧêßŧ'" - if is_python2: - expected1 = "x=u'\\u0167\\xea\\xdf\\u0167', y=u'\\u0167\\xdf\\xea\\u0167'" - expected2 = "y=u'\\u0167\\xdf\\xea\\u0167', x=u'\\u0167\\xea\\xdf\\u0167'" self.assertIn(txt, (expected1, expected2)) diff --git a/tests/testdata/__init__.py b/tests/testdata/__init__.py index 9a9e6845..b0de791a 100644 --- a/tests/testdata/__init__.py +++ b/tests/testdata/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. diff --git a/tests/utils.py b/tests/utils.py index 31ae519f..8c62a018 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright: See the LICENSE file. import functools @@ -18,11 +17,11 @@ def decorated(*args, **kwargs): return decorated -class MultiModulePatcher(object): +class MultiModulePatcher: """An abstract context processor for patching multiple modules.""" def __init__(self, *target_modules, **kwargs): - super(MultiModulePatcher, self).__init__(**kwargs) + super().__init__(**kwargs) self.patchers = [self._build_patcher(mod) for mod in target_modules] def _build_patcher(self, target_module): # pragma: no cover @@ -43,7 +42,7 @@ class mocked_date_today(MultiModulePatcher): def __init__(self, target_date, *target_modules, **kwargs): self.target_date = target_date - super(mocked_date_today, self).__init__(*target_modules, **kwargs) + super().__init__(*target_modules, **kwargs) def _build_patcher(self, target_module): module_datetime = getattr(target_module, 'datetime') @@ -53,7 +52,7 @@ def _build_patcher(self, target_module): class mocked_datetime_now(MultiModulePatcher): def __init__(self, target_dt, *target_modules, **kwargs): self.target_dt = target_dt - super(mocked_datetime_now, self).__init__(*target_modules, **kwargs) + super().__init__(*target_modules, **kwargs) def _build_patcher(self, target_module): module_datetime = getattr(target_module, 'datetime') diff --git a/tox.ini b/tox.ini index 77888d43..d7027a22 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,9 @@ minversion = 1.9 envlist = lint - py{27,35,36,37,38}-django111-alchemy-mongoengine, + py{35,36,37,38}-django111-alchemy-mongoengine, py{35,36,37,38}-django22-alchemy-mongoengine, py{36,37,38}-django30-alchemy-mongoengine, - pypy-django{111}-alchemy-mongoengine, pypy3-django{111,22}-alchemy-mongoengine, docs examples @@ -15,7 +14,6 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = - mock;python_version<"3" django111: Django>=1.11,<1.12 django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 From 9445c0952a85c48fd922cdc0ac1c198e7089c8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 3 Mar 2020 11:31:59 +0100 Subject: [PATCH 464/714] Recommend using testall locally Increase the likelihood of catching issues locally. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 9601bedf..abcb675d 100644 --- a/README.rst +++ b/README.rst @@ -372,7 +372,7 @@ All pull requests should pass the test suite, which can be launched simply with: .. code-block:: sh - $ make test + $ make testall From 8b413302f4fb6bc78213be5c2e8b430568a37be4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 3 Mar 2020 09:42:44 +0100 Subject: [PATCH 465/714] Install factory_boy in readthedocs build environment 3eb79173eee147ec5cf827d20b1a1caf5aec915e restored the `__version__` attribute in the `factory` module, and transitioned the documentation to read the version for it. However, `factory_boy` is missing from readthedocs build environment. Attempts to read the `__version__` resulted in: ``` ModuleNotFoundError: No module named 'factory' ``` Refs #676 Fixes #704 --- MANIFEST.in | 1 + readthedocs.yml | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 readthedocs.yml diff --git a/MANIFEST.in b/MANIFEST.in index d6b2c4f8..d94e0082 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,6 +8,7 @@ graft docs graft examples graft tests +exclude readthedocs.yml global-exclude *.py[cod] __pycache__ .*.sw[po] prune .github prune docs/_build diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 00000000..7bfb9fd6 --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,8 @@ +--- +version: 2 +python: + install: + - method: pip + package: . + extra_requirements: + - doc From 66a24aeb0ef760c5ca253cc0b7e7bd2b75ddb12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 3 Mar 2020 11:19:08 +0100 Subject: [PATCH 466/714] Offer development dependencies as an extras_require --- Makefile | 2 +- README.rst | 7 +++++++ requirements_dev.txt | 13 ------------- setup.cfg | 11 +++++++++++ tox.ini | 4 ++-- 5 files changed, 21 insertions(+), 16 deletions(-) delete mode 100644 requirements_dev.txt diff --git a/Makefile b/Makefile index 4de39994..10588e0a 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ clean: # DOC: Install and/or upgrade dependencies update: pip install --upgrade pip setuptools - pip install --upgrade -r requirements_dev.txt + pip install --upgrade --editable .[dev] pip freeze diff --git a/README.rst b/README.rst index abcb675d..534ca7ec 100644 --- a/README.rst +++ b/README.rst @@ -368,6 +368,13 @@ factory_boy is distributed under the MIT License. Issues should be opened through `GitHub Issues `_; whenever possible, a pull request should be included. Questions and suggestions are welcome on the `mailing-list `_. +Development dependencies can be installed in a `virtualenv +`_ with: + +.. code-block:: sh + + $ pip install --editable '.[dev]' + All pull requests should pass the test suite, which can be launched simply with: .. code-block:: sh diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 50b25220..00000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,13 +0,0 @@ --e .[doc] --r examples/requirements.txt - -coverage -Django -flake8 -isort -Pillow -SQLAlchemy -mongoengine -wheel>=0.32.0 -tox -zest.releaser[recommended] diff --git a/setup.cfg b/setup.cfg index 8136e1ce..57e8ef7c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,17 @@ python_requires = >=3.5 install_requires = Faker>=0.7.0 [options.extras_require] +dev = + coverage + Django + flake8 + isort + Pillow + SQLAlchemy + mongoengine + wheel>=0.32.0 + tox + zest.releaser[recommended] doc = Sphinx sphinx_rtd_theme diff --git a/tox.ini b/tox.ini index d7027a22..cefd27f3 100644 --- a/tox.ini +++ b/tox.ini @@ -47,9 +47,9 @@ commands = make linkcheck [testenv:lint] deps = - -rrequirements_dev.txt + -rexamples/requirements.txt check_manifest -skip_install = true +extras = dev whitelist_externals = make commands = make lint From 7620bd068082adc0126e383954bc451f2de547fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 10 Mar 2020 19:20:39 +0100 Subject: [PATCH 467/714] [DO NOT REVIEW] Add Python 2.x support removal to changelog --- docs/changelog.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9ce091c4..c56c03a2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,13 @@ ChangeLog ========= +3.0.0 (unreleased) +------------------ + +*New:* + + - Drop support for Python 2.x + 2.12.1 (unreleased) ------------------- From 265becd5b9caa71c2f58f38e4e0518fe20884c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 10 Mar 2020 19:35:56 +0100 Subject: [PATCH 468/714] Drop deprecated force_flush option from SQLAlchemyModelFactory Refs #716 --- docs/changelog.rst | 2 ++ docs/orms.rst | 9 --------- factory/alchemy.py | 26 -------------------------- tests/test_alchemy.py | 29 ----------------------------- 4 files changed, 2 insertions(+), 64 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c56c03a2..b5282fd7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ ChangeLog *New:* - Drop support for Python 2.x + - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use + ``sqlalchemy_session_persistence = "flush"`` instead. 2.12.1 (unreleased) ------------------- diff --git a/docs/orms.rst b/docs/orms.rst index cee76924..0883c6cb 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -325,15 +325,6 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: The default value is ``None``. - If ``force_flush`` is set to ``True``, it overrides this option. - - .. attribute:: force_flush - - Force a session ``flush()`` at the end of :func:`~factory.alchemy.SQLAlchemyModelFactory._create()`. - - .. note:: - - This option is deprecated. Use ``sqlalchemy_session_persistence`` instead. A (very) simple example: diff --git a/factory/alchemy.py b/factory/alchemy.py index 26956ef4..862f89ed 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -1,8 +1,5 @@ # Copyright: See the LICENSE file. - -import warnings - from . import base SESSION_PERSISTENCE_COMMIT = 'commit' @@ -22,19 +19,6 @@ def _check_sqlalchemy_session_persistence(self, meta, value): (meta, VALID_SESSION_PERSISTENCE_TYPES, value) ) - def _check_force_flush(self, meta, value): - if value: - warnings.warn( - "%(meta)s.force_flush has been deprecated as of 2.8.0 and will be removed in 3.0.0. " - "Please set ``%(meta)s.sqlalchemy_session_persistence = 'flush'`` instead." - % dict(meta=meta), - DeprecationWarning, - # Stacklevel: - # declaration -> FactoryMetaClass.__new__ -> meta.contribute_to_class - # -> meta._fill_from_meta -> option.apply -> option.checker - stacklevel=6, - ) - def _build_default_options(self): return super()._build_default_options() + [ base.OptionDefault('sqlalchemy_session', None, inherit=True), @@ -44,14 +28,6 @@ def _build_default_options(self): inherit=True, checker=self._check_sqlalchemy_session_persistence, ), - - # DEPRECATED as of 2.8.0, remove in 3.0.0 - base.OptionDefault( - 'force_flush', - False, - inherit=True, - checker=self._check_force_flush, - ), ] @@ -68,8 +44,6 @@ def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" session = cls._meta.sqlalchemy_session session_persistence = cls._meta.sqlalchemy_session_persistence - if cls._meta.force_flush: - session_persistence = SESSION_PERSISTENCE_FLUSH obj = model_class(*args, **kwargs) if session is None: diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index a08c4815..1f05c313 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -3,7 +3,6 @@ """Tests for factory_boy/SQLAlchemy interactions.""" import unittest -import warnings from unittest import mock import factory @@ -126,34 +125,6 @@ class Meta: sqlalchemy_session_persistence = 'invalid_persistence_option' model = models.StandardModel - def test_force_flush_deprecation(self): - with warnings.catch_warnings(record=True) as warning_list: - # Do not turn expected warning into an error. - warnings.filterwarnings("default", category=DeprecationWarning, module=r"tests\.test_alchemy") - - class OutdatedPersistenceFactory(StandardFactory): - class Meta: - force_flush = True - sqlalchemy_session = self.mock_session - - # There should be *1* DeprecationWarning - self.assertEqual(len(warning_list), 1) - warning = warning_list[0] - self.assertTrue(issubclass(warning.category, DeprecationWarning)) - - # The warning text should point to the class declaration. - text = warnings.formatwarning(warning.message, warning.category, warning.filename, warning.lineno) - self.assertIn('test_alchemy.py', text) - self.assertIn('class OutdatedPersistenceFactory', text) - - # However, we shall keep the old-style behavior. - self.mock_session.commit.assert_not_called() - self.mock_session.flush.assert_not_called() - - OutdatedPersistenceFactory.create() - self.mock_session.commit.assert_not_called() - self.mock_session.flush.assert_called_once_with() - class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): def setUp(self): From ad5b71d7fdcf7fc71c45efd47a23f69bb7f3740b Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 10 Mar 2020 17:35:45 -0700 Subject: [PATCH 469/714] Combine the two unreleased sections in changelog.rst --- docs/changelog.rst | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index b5282fd7..dddf2b3a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,15 +4,6 @@ ChangeLog 3.0.0 (unreleased) ------------------ -*New:* - - - Drop support for Python 2.x - - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use - ``sqlalchemy_session_persistence = "flush"`` instead. - -2.12.1 (unreleased) -------------------- - *New:* - Add support for Python 3.8 @@ -29,6 +20,8 @@ ChangeLog *Removed:* - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. + - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use + ``sqlalchemy_session_persistence = "flush"`` instead. 2.12.0 (2019-05-11) From d7f4870fc618acbfb528f7f0e46cba390415ca9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 10 Mar 2020 19:13:15 +0100 Subject: [PATCH 470/714] Remove deprecated attributes() and declarations() on BaseFactory Refs #716 --- docs/changelog.rst | 1 + factory/base.py | 39 --------------------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index dddf2b3a..9e781ced 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,7 @@ ChangeLog - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use ``sqlalchemy_session_persistence = "flush"`` instead. + - Drop deprecated ``attributes()`` and ``declarations()`` methods from ``factory.BaseFactory``. 2.12.0 (2019-05-11) diff --git a/factory/base.py b/factory/base.py index ddc9ebf9..2f1d1228 100644 --- a/factory/base.py +++ b/factory/base.py @@ -3,7 +3,6 @@ import collections import logging -import warnings from . import builder, declarations, enums, errors, utils @@ -442,44 +441,6 @@ def _setup_next_sequence(cls): """ return 0 - @classmethod - def attributes(cls, create=False, extra=None): - """Build a dict of attribute values, respecting declaration order. - - The process is: - - Handle 'orderless' attributes, overriding defaults with provided - kwargs when applicable - - Handle ordered attributes, overriding them with provided kwargs when - applicable; the current list of computed attributes is available - to the currently processed object. - """ - warnings.warn( - "Usage of Factory.attributes() is deprecated.", - DeprecationWarning, - stacklevel=2, - ) - declarations = cls._meta.pre_declarations.as_dict() - declarations.update(extra or {}) - from . import helpers - return helpers.make_factory(dict, **declarations) - - @classmethod - def declarations(cls, extra_defs=None): - """Retrieve a copy of the declared attributes. - - Args: - extra_defs (dict): additional definitions to insert into the - retrieved DeclarationDict. - """ - warnings.warn( - "Factory.declarations is deprecated; use Factory._meta.pre_declarations instead.", - DeprecationWarning, - stacklevel=2, - ) - decls = cls._meta.pre_declarations.as_dict() - decls.update(extra_defs or {}) - return decls - @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" From 49870b7aa4a4db9c7cb2e943f03bee00d5f0fdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 10 Mar 2020 18:53:05 +0100 Subject: [PATCH 471/714] Remove deprecated aliases from factory.fuzzy Refs #716 --- docs/changelog.rst | 2 ++ factory/fuzzy.py | 30 ------------------------------ 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e781ced..2c6648b1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -23,6 +23,8 @@ ChangeLog - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use ``sqlalchemy_session_persistence = "flush"`` instead. - Drop deprecated ``attributes()`` and ``declarations()`` methods from ``factory.BaseFactory``. + - Drop deprecated aliases ``factory.fuzzy`` aliases for ``get_random_state()``, ``set_random_state()`` and + ``reseed_random()`` .fuzzy``. Use methods from the ``factory.random`` module instead. 2.12.0 (2019-05-11) diff --git a/factory/fuzzy.py b/factory/fuzzy.py index d96f85a4..61f6eba8 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -18,36 +18,6 @@ ) -def get_random_state(): - warnings.warn( - "`factory.fuzzy.get_random_state` is deprecated. " - "You should use `factory.random.get_random_state` instead", - DeprecationWarning, - stacklevel=2 - ) - return random.get_random_state() - - -def set_random_state(state): - warnings.warn( - "`factory.fuzzy.set_random_state` is deprecated. " - "You should use `factory.random.set_random_state` instead", - DeprecationWarning, - stacklevel=2 - ) - return random.set_random_state(state) - - -def reseed_random(seed): - warnings.warn( - "`factory.fuzzy.set_random_state` is deprecated. " - "You should use `factory.random.reseed_random` instead", - DeprecationWarning, - stacklevel=2 - ) - random.reseed_random(seed) - - class BaseFuzzyAttribute(declarations.BaseDeclaration): """Base class for fuzzy attributes. From c58d08259616a14fc7537d7d9f00ec825d15f9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 11 Mar 2020 09:43:21 +0100 Subject: [PATCH 472/714] Bump version to 3.0.0.dev0 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 57e8ef7c..f32cc6b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 2.12.1.dev0 +version = 3.0.0.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From a8da29e21b42544fe208b8f641836ca2f2b222c2 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Tue, 10 Mar 2020 17:28:37 -0700 Subject: [PATCH 473/714] Remove backwards compatibility shims in factory/__init__.py Refs #716 --- docs/changelog.rst | 7 +++++++ examples/django_demo/generic_foreignkey/factories.py | 2 +- examples/flask_alchemy/demoapp_factories.py | 2 +- factory/__init__.py | 6 ------ tests/test_django.py | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2c6648b1..a87c3f01 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,13 @@ ChangeLog - Drop deprecated ``attributes()`` and ``declarations()`` methods from ``factory.BaseFactory``. - Drop deprecated aliases ``factory.fuzzy`` aliases for ``get_random_state()``, ``set_random_state()`` and ``reseed_random()`` .fuzzy``. Use methods from the ``factory.random`` module instead. + - Remove references ``django``, ``alchemy``, ``mogo`` and ``mongoengine`` + in module ``factory``. Import the submodules ``factory.django``, + ``factory.alchemy``, ``factory.mogo`` and ``factory.mongoengine`` + instead. + - Remove aliases ``factory.DjangoFactory`` and ``factory.MogoFactory``. Use + ``factory.django.DjangoFactory`` and ``factory.mogo.MogoFactory`` + instead. 2.12.0 (2019-05-11) diff --git a/examples/django_demo/generic_foreignkey/factories.py b/examples/django_demo/generic_foreignkey/factories.py index 688dd557..c7f5117e 100644 --- a/examples/django_demo/generic_foreignkey/factories.py +++ b/examples/django_demo/generic_foreignkey/factories.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import Group, User from django.contrib.contenttypes.models import ContentType -import factory +import factory.django from .models import TaggedItem diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py index 9f56d6d0..61bbb774 100644 --- a/examples/flask_alchemy/demoapp_factories.py +++ b/examples/flask_alchemy/demoapp_factories.py @@ -1,5 +1,5 @@ import demoapp -import factory +import factory.alchemy import factory.fuzzy diff --git a/factory/__init__.py b/factory/__init__.py index afcb7cce..93995214 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,7 +1,5 @@ # Copyright: See the LICENSE file. -# Backward compatibility; this should be removed soon. -from . import alchemy, django, mogo, mongoengine from .base import ( BaseDictFactory, BaseListFactory, @@ -63,7 +61,3 @@ import pkg_resources __version__ = pkg_resources.get_distribution("factory_boy").version - - -MogoFactory = mogo.MogoFactory -DjangoModelFactory = django.DjangoModelFactory diff --git a/tests/test_django.py b/tests/test_django.py index 8c966534..ee4c08be 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -14,7 +14,7 @@ from django.test import utils as django_test_utils from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner -import factory +import factory.django from . import testdata From 91131ebecf79535d36c67f39fd7522380020dc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarek=20G=C5=82owacki?= Date: Fri, 24 Apr 2020 01:37:23 +1000 Subject: [PATCH 474/714] Quick, before anyone notices! --- docs/recipes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 2fd4e01e..2a1fdee5 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -344,7 +344,7 @@ This time, we want the company owner to live in a country neighboring the countr Custom manager methods ---------------------- -Sometimes you need a factory to call a specific manager method other then the +Sometimes you need a factory to call a specific manager method other than the default :meth:`Model.objects.create() ` method: .. code-block:: python From 53926acc470dea7b1dbae472d659ef94074d3391 Mon Sep 17 00:00:00 2001 From: paulonteri Date: Sat, 25 Apr 2020 13:42:16 +0300 Subject: [PATCH 475/714] Fix grammatical error in docs --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 04df196c..c4f0e0ff 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -752,7 +752,7 @@ LazyFunction The :class:`LazyFunction` is the simplest case where the value of an attribute does not depend on the object being built. -It takes as argument a function to call; that should not take any arguments and +It takes as an argument a function to call; that should not take any arguments and return a value. .. code-block:: python From 97160037dca9b6b495f197155783b80c7c2ea1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 24 May 2020 14:47:50 +0200 Subject: [PATCH 476/714] Move Related{,List}Factory tests to their own test case Improve tests organization by grouping tests for a given feature together. --- tests/test_using.py | 260 ++++++++++++++++++++++---------------------- 1 file changed, 132 insertions(+), 128 deletions(-) diff --git a/tests/test_using.py b/tests/test_using.py index a1351c8d..98dcf19c 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2370,135 +2370,8 @@ def register(self, create, extracted, reference=0, **kwargs): book = BookFactory.build() self.assertEqual({0: book}, LIBRARY) - def test_related_factory_list_of_varying_size(self): - # Create our list of expected "related object counts" - related_list_sizes = [5, 5, 4, 4, 3, 3, 2, 2, 1, 1] - RELATED_LIST_SIZE = lambda: related_list_sizes.pop() - - class TestRelatedObject: - def __init__(self, obj=None, one=None, two=None): - # Mock out the 'List of Related Objects' generated by RelatedFactoryList - if hasattr(obj, 'related_list'): - obj.related_list.append(self) - else: - obj.related_list = [self] - self.one = one - self.two = two - self.three = obj - - class TestRelatedObjectFactoryList(factory.Factory): - class Meta: - model = TestRelatedObject - one = 1 - two = factory.LazyAttribute(lambda o: o.one + 1) - - class TestObjectFactory(factory.Factory): - class Meta: - model = TestObject - one = 3 - two = 2 - # RELATED_LIST_SIZE is a lambda, this allows flexibility, as opposed - # to creating "n" related objects for every parent object... - three = factory.RelatedFactoryList(TestRelatedObjectFactoryList, - 'obj', - size=RELATED_LIST_SIZE) - # Create 5 TestObjectFactories: Each with 1, 2, ... 5 related objs - for related_list_size in reversed(related_list_sizes[1::2]): - obj = TestObjectFactory.build() - # Normal fields - self.assertEqual(3, obj.one) - self.assertEqual(2, obj.two) - # RelatedFactory was built - self.assertIsNone(obj.three) - self.assertIsNotNone(obj.related_list) - - for related_obj in obj.related_list: - self.assertEqual(1, related_obj.one) - self.assertEqual(2, related_obj.two) - # Each RelatedFactory in the RelatedFactoryList was passed the "parent" object - self.assertEqual(related_list_size, len(obj.related_list)) - # obj.related is the list of TestRelatedObject(s) - for related_obj in obj.related_list: - self.assertEqual(obj, related_obj.three) - - obj = TestObjectFactory.build(three__one=3) - # Normal fields - self.assertEqual(3, obj.one) - self.assertEqual(2, obj.two) - # RelatedFactory was build - self.assertIsNone(obj.three) - self.assertIsNotNone(obj.related_list) - # three__one was correctly parse - for related_obj in obj.related_list: - self.assertEqual(3, related_obj.one) - self.assertEqual(4, related_obj.two) - # Each RelatedFactory in RelatedFactoryList received "parent" object - self.assertEqual(related_list_size, len(obj.related_list)) - for related_obj in obj.related_list: - self.assertEqual(obj, related_obj.three) - - def test_related_factory_list_of_static_size(self): - RELATED_LIST_SIZE = 4 - - class TestRelatedObject: - def __init__(self, obj=None, one=None, two=None): - # Mock out the 'List of Related Objects' generated by RelatedFactoryList - if hasattr(obj, 'related_list'): - obj.related_list.append(self) - else: - obj.related_list = [self] - - self.one = one - self.two = two - self.three = obj - - class TestRelatedObjectFactoryList(factory.Factory): - class Meta: - model = TestRelatedObject - one = 1 - two = factory.LazyAttribute(lambda o: o.one + 1) - - class TestObjectFactory(factory.Factory): - class Meta: - model = TestObject - one = 3 - two = 2 - three = factory.RelatedFactoryList(TestRelatedObjectFactoryList, 'obj', - size=RELATED_LIST_SIZE) - - obj = TestObjectFactory.build() - # Normal fields - self.assertEqual(3, obj.one) - self.assertEqual(2, obj.two) - # RelatedFactory was built - self.assertIsNone(obj.three) - self.assertIsNotNone(obj.related_list) - - for related_obj in obj.related_list: - self.assertEqual(1, related_obj.one) - self.assertEqual(2, related_obj.two) - # Each RelatedFactory in the RelatedFactoryList was passed the "parent" object - self.assertEqual(RELATED_LIST_SIZE, len(obj.related_list)) - # obj.related is the list of TestRelatedObject(s) - for related_obj in obj.related_list: - self.assertEqual(obj, related_obj.three) - - obj = TestObjectFactory.build(three__one=3) - # Normal fields - self.assertEqual(3, obj.one) - self.assertEqual(2, obj.two) - # RelatedFactory was build - self.assertIsNone(obj.three) - self.assertIsNotNone(obj.related_list) - # three__one was correctly parse - for related_obj in obj.related_list: - self.assertEqual(3, related_obj.one) - self.assertEqual(4, related_obj.two) - # Each RelatedFactory in RelatedFactoryList received "parent" object - self.assertEqual(RELATED_LIST_SIZE, len(obj.related_list)) - for related_obj in obj.related_list: - self.assertEqual(obj, related_obj.three) +class RelatedFactoryTestCase(unittest.TestCase): def test_related_factory(self): class TestRelatedObject: def __init__(self, obj=None, one=None, two=None): @@ -2659,6 +2532,137 @@ class Params: self.assertEqual('blah', obj2.related.two) +class RelatedListFactoryTestCase(unittest.TestCase): + def test_related_factory_list_of_varying_size(self): + # Create our list of expected "related object counts" + related_list_sizes = [5, 5, 4, 4, 3, 3, 2, 2, 1, 1] + RELATED_LIST_SIZE = lambda: related_list_sizes.pop() + + class TestRelatedObject: + def __init__(self, obj=None, one=None, two=None): + # Mock out the 'List of Related Objects' generated by RelatedFactoryList + if hasattr(obj, 'related_list'): + obj.related_list.append(self) + else: + obj.related_list = [self] + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactoryList(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + # RELATED_LIST_SIZE is a lambda, this allows flexibility, as opposed + # to creating "n" related objects for every parent object... + three = factory.RelatedFactoryList(TestRelatedObjectFactoryList, + 'obj', + size=RELATED_LIST_SIZE) + # Create 5 TestObjectFactories: Each with 1, 2, ... 5 related objs + for related_list_size in reversed(related_list_sizes[1::2]): + obj = TestObjectFactory.build() + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was built + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + + for related_obj in obj.related_list: + self.assertEqual(1, related_obj.one) + self.assertEqual(2, related_obj.two) + # Each RelatedFactory in the RelatedFactoryList was passed the "parent" object + self.assertEqual(related_list_size, len(obj.related_list)) + # obj.related is the list of TestRelatedObject(s) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + obj = TestObjectFactory.build(three__one=3) + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was build + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + # three__one was correctly parse + for related_obj in obj.related_list: + self.assertEqual(3, related_obj.one) + self.assertEqual(4, related_obj.two) + # Each RelatedFactory in RelatedFactoryList received "parent" object + self.assertEqual(related_list_size, len(obj.related_list)) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + def test_related_factory_list_of_static_size(self): + RELATED_LIST_SIZE = 4 + + class TestRelatedObject: + def __init__(self, obj=None, one=None, two=None): + # Mock out the 'List of Related Objects' generated by RelatedFactoryList + if hasattr(obj, 'related_list'): + obj.related_list.append(self) + else: + obj.related_list = [self] + + self.one = one + self.two = two + self.three = obj + + class TestRelatedObjectFactoryList(factory.Factory): + class Meta: + model = TestRelatedObject + one = 1 + two = factory.LazyAttribute(lambda o: o.one + 1) + + class TestObjectFactory(factory.Factory): + class Meta: + model = TestObject + one = 3 + two = 2 + three = factory.RelatedFactoryList(TestRelatedObjectFactoryList, 'obj', + size=RELATED_LIST_SIZE) + + obj = TestObjectFactory.build() + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was built + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + + for related_obj in obj.related_list: + self.assertEqual(1, related_obj.one) + self.assertEqual(2, related_obj.two) + # Each RelatedFactory in the RelatedFactoryList was passed the "parent" object + self.assertEqual(RELATED_LIST_SIZE, len(obj.related_list)) + # obj.related is the list of TestRelatedObject(s) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + obj = TestObjectFactory.build(three__one=3) + # Normal fields + self.assertEqual(3, obj.one) + self.assertEqual(2, obj.two) + # RelatedFactory was build + self.assertIsNone(obj.three) + self.assertIsNotNone(obj.related_list) + # three__one was correctly parse + for related_obj in obj.related_list: + self.assertEqual(3, related_obj.one) + self.assertEqual(4, related_obj.two) + # Each RelatedFactory in RelatedFactoryList received "parent" object + self.assertEqual(RELATED_LIST_SIZE, len(obj.related_list)) + for related_obj in obj.related_list: + self.assertEqual(obj, related_obj.three) + + class RelatedFactoryExtractionTestCase(unittest.TestCase): def setUp(self): self.relateds = [] From 60b7cbb7d3d6755f5b3a176cacd0e383a0fd0b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 18 Jun 2020 14:13:08 +0200 Subject: [PATCH 477/714] Fix readthedocs build The correct key to specify the package to pip install is `path`, not `package`. https://docs.readthedocs.io/en/stable/config-file/v2.html#packages --- readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readthedocs.yml b/readthedocs.yml index 7bfb9fd6..57cad8d1 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -3,6 +3,6 @@ version: 2 python: install: - method: pip - package: . + path: . extra_requirements: - doc From 28a06d1af802de2b357db6e73c271219a75ba578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 22 Jun 2020 21:11:42 +0200 Subject: [PATCH 478/714] Fix formatting issue in get_random_state() doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-by: Raphaël Barrois --- docs/reference.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index c4f0e0ff..889c3fe6 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -2104,8 +2104,7 @@ of :class:`random.Random`, which can be managed through the :mod:`factory.random .. method:: get_random_state() Call :meth:`get_random_state` to retrieve the random generator's current - state. This method synchronizes both Faker’s and `factory_boy`’s random - state. + state. This method synchronizes both Faker’s and factory_boy’s random state. The returned object is implementation-specific. .. method:: set_random_state(state) From f319276d78dba27e7accc2ae08d976ed1119b3c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 22 Jun 2020 21:09:11 +0200 Subject: [PATCH 479/714] =?UTF-8?q?Fix=20=E2=80=9Cremoved=E2=80=9D=20secti?= =?UTF-8?q?on=20formatting=20in=20changelog=20for=203.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-by: Raphaël Barrois --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index a87c3f01..b0505825 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,7 @@ ChangeLog - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. *Removed:* + - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use From 8bddf17316a95424ce4e4b12a5a5960ece5dcf71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 23 Jun 2020 10:57:32 +0200 Subject: [PATCH 480/714] Remove requirements*.txt from MANIFEST.in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The requirements file duplicate the `setup.py` instruction since 6f37f9be2d2e1bc75340068911db18b2bbcbe722. - `requirements_dev.txt` has been removed in 66a24aeb0ef760c5ca253cc0b7e7bd2b75ddb12a. - `requirements.txt` has been removed in 7a23a28eb89455b940f3c262179d7c7e54b45a92. - `requirements_docs.txt` has been removed in a0d2e24225c85f48e01972a7411c875d4be8d701. The requirements file remaining are `graft`-ed with the examples. Reviewed-by: Raphaël Barrois --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index d94e0082..a8c0ba33 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ include ChangeLog CODE_OF_CONDUCT.md CONTRIBUTING.rst CREDITS LICENSE README.rst -include requirements*.txt include Makefile tox.ini .flake8 graft factory From 6d38278aa5b4b90f6e5797e6c1e21c6f13273cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 23 Jun 2020 10:52:28 +0200 Subject: [PATCH 481/714] Move flake8 configuration to setup.cfg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize configuration in one place for easier maintenance. Reviewed-by: Raphaël Barrois --- .flake8 | 7 ------- MANIFEST.in | 2 +- Makefile | 4 ++-- setup.cfg | 8 ++++++++ 4 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index d2583294..00000000 --- a/.flake8 +++ /dev/null @@ -1,7 +0,0 @@ -[flake8] -ignore = - # Ignore "and" at start of line. - W503 - # Ignore "do not assign a lambda expression, use a def". - E731 -max-line-length = 120 diff --git a/MANIFEST.in b/MANIFEST.in index a8c0ba33..c56cd9c7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include ChangeLog CODE_OF_CONDUCT.md CONTRIBUTING.rst CREDITS LICENSE README.rst -include Makefile tox.ini .flake8 +include Makefile tox.ini graft factory diff --git a/Makefile b/Makefile index 10588e0a..0f5ff00c 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,8 @@ example-test: # Note: we run the linter in two runs, because our __init__.py files has specific warnings we want to exclude # DOC: Perform code quality tasks lint: - $(FLAKE8) --config .flake8 --exclude $(PACKAGE)/__init__.py $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) - $(FLAKE8) --config .flake8 --ignore F401 $(PACKAGE)/__init__.py + $(FLAKE8) --exclude $(PACKAGE)/__init__.py $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) + $(FLAKE8) --ignore F401 $(PACKAGE)/__init__.py $(ISORT) --recursive --check-only --diff $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) check-manifest diff --git a/setup.cfg b/setup.cfg index f32cc6b3..7ff81fd5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,6 +60,14 @@ version-levels = 3 [distutils] index-servers = pypi +[flake8] +ignore = + # Ignore "and" at start of line. + W503 + # Ignore "do not assign a lambda expression, use a def". + E731 +max-line-length = 120 + [isort] multi_line_output = 3 include_trailing_comma = True From b8e4270894c6cfe0a33682c9b205b7d58bde0837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 22 Jun 2020 21:07:00 +0200 Subject: [PATCH 482/714] Add missing entries to the changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-by: Raphaël Barrois --- docs/changelog.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index b0505825..3a22d92e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,9 @@ ChangeLog - Add support for Python 3.8 - Add support for Django 2.2 and 3.0 + - Report misconfiguration when a :py:class:`~factory.Factory` is used as the :py:attr:`~factory.Factory.model` for another :py:class:`~factory.Factory`. + - Allow configuring the color palette of :py:class:`~factory.django.ImageField`. + - :py:meth:`get_random_state()` now represents the state of Faker and ``factory_boy`` fuzzy attributes. *Bugfix:* From c169404cf2d81a439d9fdc0754be64ea63701cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 18 Jan 2020 12:16:57 +0100 Subject: [PATCH 483/714] Clarify RelatedFactory.factory_related_name documentation Fixes #685 --- docs/reference.rst | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 889c3fe6..45002d38 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1631,15 +1631,10 @@ RelatedFactory - Or the fully qualified path to a :class:`Factory` subclass (see :ref:`subfactory-circular` for details) - .. attribute:: name - - The generated object (where the :class:`RelatedFactory` attribute will - 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 - keyword: + .. attribute:: factory_related_name + If set, the object generated by the factory declaring the + ``RelatedFactory`` is passed as keyword argument to the related factory. .. code-block:: python @@ -1657,7 +1652,7 @@ RelatedFactory lang = 'fr' capital_city = factory.RelatedFactory( CityFactory, # Not CityFactory() - 'capital_of', + factory_related_name='capital_of', name="Paris", ) @@ -1729,14 +1724,10 @@ RelatedFactoryList - Or the fully qualified path to a :class:`Factory` subclass (see :ref:`subfactory-circular` for details) - .. attribute:: name - - The generated object (where the :class:`RelatedFactory` attribute will - set) may be passed to the related factories if the :attr:`factory_related_name` parameter - is set. + .. attribute:: factory_related_name - It will be passed as a keyword argument, using the :attr:`name` value as - keyword: + If set, the object generated by the factory declaring the + ``RelatedFactory`` is passed as keyword argument to the related factory. .. attribute:: size From 28b4912413493a7154ec62e47dd6812f0ac437fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 22 Jan 2020 10:44:44 +0100 Subject: [PATCH 484/714] Prefer kwarg for RelatedFactory.factory_related_name --- docs/recipes.rst | 25 ++++++++++++++++++++----- docs/reference.rst | 4 +++- tests/test_django.py | 11 +++++++++-- tests/test_docs_internals.py | 6 ++++-- tests/test_using.py | 17 +++++++++++++---- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 2a1fdee5..33de8fce 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -80,7 +80,11 @@ use a :class:`~factory.RelatedFactory` declaration: class Meta: model = models.User - log = factory.RelatedFactory(UserLogFactory, 'user', action=models.UserLog.ACTION_CREATE) + log = factory.RelatedFactory( + UserLogFactory, + factory_related_name='user', + action=models.UserLog.ACTION_CREATE, + ) When a :class:`UserFactory` is instantiated, factory_boy will call @@ -121,7 +125,7 @@ Since version 2.9, the :meth:`~factory.django.mute_signals` decorator should be # 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') + profile = factory.RelatedFactory(ProfileFactory, factory_related_name='user') .. OHAI_VIM:* @@ -248,11 +252,22 @@ If more links are needed, simply add more :class:`RelatedFactory` declarations: rank = 1 class UserWithGroupFactory(UserFactory): - membership = factory.RelatedFactory(GroupLevelFactory, 'user') + membership = factory.RelatedFactory( + GroupLevelFactory, + factory_related_name='user', + ) class UserWith2GroupsFactory(UserFactory): - membership1 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group1') - membership2 = factory.RelatedFactory(GroupLevelFactory, 'user', group__name='Group2') + membership1 = factory.RelatedFactory( + GroupLevelFactory, + factory_related_name='user', + group__name='Group1', + ) + membership2 = factory.RelatedFactory( + GroupLevelFactory, + factory_related_name='user', + group__name='Group2', + ) Whenever the ``UserWithGroupFactory`` is called, it will, as a post-generation hook, diff --git a/docs/reference.rst b/docs/reference.rst index 45002d38..6f5b610a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1699,7 +1699,9 @@ If a value is passed for the :class:`RelatedFactory` attribute, this disables model = Country lang = 'fr' - capital_city = factory.RelatedFactory(CityFactory, 'capital_of', + capital_city = factory.RelatedFactory( + CityFactory, + factory_related_name='capital_of', # Would also work with SelfAttribute('capital_of.lang') main_lang=factory.SelfAttribute('..lang'), ) diff --git a/tests/test_django.py b/tests/test_django.py index ee4c08be..694b79ab 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -416,7 +416,10 @@ class Meta: pointed = factory.SubFactory(PointedFactory, foo='new_foo') class PointedRelatedFactory(PointedFactory): - pointer = factory.RelatedFactory(PointerFactory, 'pointed') + pointer = factory.RelatedFactory( + PointerFactory, + factory_related_name='pointed', + ) class PointerExtraFactory(PointerFactory): pointed__foo = 'extra_new_foo' @@ -427,7 +430,11 @@ class PointedRelatedExtraFactory(PointedRelatedFactory): class PointedRelatedWithTraitFactory(PointedFactory): class Params: with_pointer = factory.Trait( - pointer=factory.RelatedFactory(PointerFactory, 'pointed', bar='with_trait') + pointer=factory.RelatedFactory( + PointerFactory, + factory_related_name='pointed', + bar='with_trait', + ) ) cls.PointedFactory = PointedFactory diff --git a/tests/test_docs_internals.py b/tests/test_docs_internals.py index a869a4d9..cfc76377 100644 --- a/tests/test_docs_internals.py +++ b/tests/test_docs_internals.py @@ -107,8 +107,10 @@ class Params: # Related logs creation_log = factory.RelatedFactory( - UserLogFactory, 'user', - action='create', timestamp=factory.SelfAttribute('user.creation_date'), + UserLogFactory, + factory_related_name='user', + action='create', + timestamp=factory.SelfAttribute('user.creation_date'), ) diff --git a/tests/test_using.py b/tests/test_using.py index 98dcf19c..fe4a3ed5 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -2391,7 +2391,10 @@ class Meta: model = TestObject one = 3 two = 2 - three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') + three = factory.RelatedFactory( + TestRelatedObjectFactory, + factory_related_name='obj', + ) obj = TestObjectFactory.build() # Normal fields @@ -2486,7 +2489,7 @@ class Meta: two = 2 three = factory.RelatedFactory( TestRelatedObjectFactory, - 'obj', + factory_related_name='obj', two=factory.SelfAttribute('obj.two'), ) @@ -2519,7 +2522,10 @@ class Params: one = 3 two = 2 - three = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') + three = factory.RelatedFactory( + TestRelatedObjectFactory, + factory_related_name='obj', + ) three__two = factory.SelfAttribute('..blah') obj = TestObjectFactory.build() @@ -2680,7 +2686,10 @@ class Meta: class TestObjectFactory(factory.Factory): class Meta: model = TestObject - one = factory.RelatedFactory(TestRelatedObjectFactory, 'obj') + one = factory.RelatedFactory( + TestRelatedObjectFactory, + factory_related_name='obj', + ) self.TestRelatedObject = TestRelatedObject self.TestRelatedObjectFactory = TestRelatedObjectFactory From 9606377441ffd886006a807a452de0ccda6ad26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 22 Jun 2020 21:23:32 +0200 Subject: [PATCH 485/714] Cosmetic edits to recipes for SQLAlchemy Many-to-Many --- docs/recipes.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 33de8fce..c6671b20 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -203,9 +203,8 @@ But when ``UserFactory.create(groups=(group1, group2, group3))`` is called, the ``groups`` declaration will add passed in groups to the set of groups for the user. -For SQLAlchemy, ``list`` is for many-to-many relationship. -Modify ``self.groups.add(group)`` to ``self.groups.append(group)`` from the above example, -then create ``UserFactory.create(groups=[group1, group2, group3])`` +For SQLAlchemy, change ``self.groups.add(group)`` in the above example to +``self.groups.append(group)``. Many-to-many relation with a 'through' -------------------------------------- From 4bb5017023046f377514bd1e068b3b830f85b646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 16 Jun 2020 10:17:18 +0200 Subject: [PATCH 486/714] Simplify RelatedFactoryList documented example The LIST_SIZE constant is not useful, randint already returns an integer in the given range. Additionally, there was an off-by one index as randint bounds are inclusive, causing an IndexError when randint returned 5. Thanks @loren-jiang for the report! Closes #741. --- docs/reference.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 6f5b610a..b5f2bbe3 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1748,14 +1748,11 @@ RelatedFactoryList .. code-block:: python - LIST_SIZES = [1, 2, 3, 4, 5] - class FooFactory(factory.Factory): class Meta: model = Foo # Generate a list of `factory` objects of random size, ranging from 1 -> 5 - bar = factory.RelatedFactoryList(BarFactory, - size=lambda: LIST_SIZES[random.randint(0,5)]) + bar = factory.RelatedFactoryList(BarFactory, size=lambda: random.randint(1, 5)) # Each Foo object will have exactly 3 Bar objects generated for its foobar attribute. foobar = factory.RelatedFactoryList(BarFactory, size=3) From 59ee6584d8788af065a86aa168e8f42a4e9f3801 Mon Sep 17 00:00:00 2001 From: gonz Date: Thu, 31 Oct 2019 17:27:08 -0600 Subject: [PATCH 487/714] Add SQLAlchemy get_or_create support --- docs/changelog.rst | 1 + docs/orms.rst | 37 +++++++++++++++ factory/alchemy.py | 61 +++++++++++++++++++++++-- tests/alchemyapp/models.py | 17 +++++++ tests/test_alchemy.py | 92 +++++++++++++++++++++++++++++++++++++- 5 files changed, 204 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3a22d92e..9bccbb5e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Report misconfiguration when a :py:class:`~factory.Factory` is used as the :py:attr:`~factory.Factory.model` for another :py:class:`~factory.Factory`. - Allow configuring the color palette of :py:class:`~factory.django.ImageField`. - :py:meth:`get_random_state()` now represents the state of Faker and ``factory_boy`` fuzzy attributes. + - Add SQLAlchemy ``get_or_create`` support *Bugfix:* diff --git a/docs/orms.rst b/docs/orms.rst index 0883c6cb..e67eb13b 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -325,6 +325,43 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: The default value is ``None``. + .. attribute:: sqlalchemy_get_or_create + + .. versionadded:: 3.0.0 + + Fields whose name are passed in this list will be used to perform a + :meth:`Model.query.one_or_none() ` + or the usual :meth:`Session.add() `: + + .. code-block:: python + + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = User + sqlalchemy_session = session + sqlalchemy_get_or_create = ('username',) + + username = 'john' + + .. code-block:: pycon + + >>> User.query.all() + [] + >>> UserFactory() # Creates a new user + + >>> User.query.all() + [] + + >>> UserFactory() # Fetches the existing user + + >>> User.query.all() # No new user! + [] + + >>> UserFactory(username='jack') # Creates another user + + >>> User.query.all() + [, ] + A (very) simple example: diff --git a/factory/alchemy.py b/factory/alchemy.py index 862f89ed..beccdccf 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -1,6 +1,9 @@ # Copyright: See the LICENSE file. -from . import base +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm.exc import NoResultFound + +from . import base, errors SESSION_PERSISTENCE_COMMIT = 'commit' SESSION_PERSISTENCE_FLUSH = 'flush' @@ -21,6 +24,7 @@ def _check_sqlalchemy_session_persistence(self, meta, value): def _build_default_options(self): return super()._build_default_options() + [ + base.OptionDefault('sqlalchemy_get_or_create', (), inherit=True), base.OptionDefault('sqlalchemy_session', None, inherit=True), base.OptionDefault( 'sqlalchemy_session_persistence', @@ -39,15 +43,66 @@ class SQLAlchemyModelFactory(base.Factory): class Meta: abstract = True + @classmethod + def _generate(cls, strategy, params): + # Original params are used in _get_or_create if it cannot build an + # object initially due to an IntegrityError being raised + cls._original_params = params + return super(SQLAlchemyModelFactory, cls)._generate(strategy, params) + + @classmethod + def _get_or_create(cls, model_class, session, *args, **kwargs): + key_fields = {} + for field in cls._meta.sqlalchemy_get_or_create: + if field not in kwargs: + raise errors.FactoryError( + "sqlalchemy_get_or_create - " + "Unable to find initialization value for '%s' in factory %s" % + (field, cls.__name__)) + key_fields[field] = kwargs.pop(field) + + obj = session.query(model_class).filter_by( + *args, **key_fields).one_or_none() + + if not obj: + try: + obj = cls._save(model_class, session, *args, **key_fields, **kwargs) + except IntegrityError as e: + session.rollback() + get_or_create_params = { + lookup: value + for lookup, value in cls._original_params.items() + if lookup in cls._meta.sqlalchemy_get_or_create + } + if get_or_create_params: + try: + obj = session.query(model_class).filter_by( + **get_or_create_params).one() + except NoResultFound: + # Original params are not a valid lookup and triggered a create(), + # that resulted in an IntegrityError. + raise e + else: + raise e + + return obj + @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" session = cls._meta.sqlalchemy_session - session_persistence = cls._meta.sqlalchemy_session_persistence - obj = model_class(*args, **kwargs) if session is None: raise RuntimeError("No session provided.") + if cls._meta.sqlalchemy_get_or_create: + return cls._get_or_create(model_class, session, *args, **kwargs) + return cls._save(model_class, session, *args, **kwargs) + + @classmethod + def _save(cls, model_class, session, *args, **kwargs): + session_persistence = cls._meta.sqlalchemy_session_persistence + + obj = model_class(*args, **kwargs) session.add(obj) if session_persistence == SESSION_PERSISTENCE_FLUSH: session.flush() diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index e31e9034..013a56ae 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -20,6 +20,23 @@ class StandardModel(Base): foo = Column(Unicode(20)) +class MultiFieldModel(Base): + __tablename__ = 'MultiFieldModelTable' + + id = Column(Integer(), primary_key=True) + foo = Column(Unicode(20)) + slug = Column(Unicode(20), unique=True) + + +class MultifieldUniqueModel(Base): + __tablename__ = 'MultiFieldUniqueModelTable' + + id = Column(Integer(), primary_key=True) + slug = Column(Unicode(20), unique=True) + text = Column(Unicode(20), unique=True) + title = Column(Unicode(20), unique=True) + + class NonIntegerPk(Base): __tablename__ = 'NonIntegerPk' diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 1f05c313..2205ca1e 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -5,6 +5,8 @@ import unittest from unittest import mock +import sqlalchemy + import factory from factory.alchemy import SQLAlchemyModelFactory @@ -36,8 +38,41 @@ class Meta: id = factory.Sequence(lambda n: n) -class SQLAlchemyPkSequenceTestCase(unittest.TestCase): +class MultifieldModelFactory(SQLAlchemyModelFactory): + class Meta: + model = models.MultiFieldModel + sqlalchemy_get_or_create = ('slug',) + sqlalchemy_session = models.session + sqlalchemy_session_persistence = 'commit' + + id = factory.Sequence(lambda n: n) + foo = factory.Sequence(lambda n: 'foo%d' % n) + +class WithGetOrCreateFieldFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_get_or_create = ('foo',) + sqlalchemy_session = models.session + sqlalchemy_session_persistence = 'commit' + + id = factory.Sequence(lambda n: n) + foo = factory.Sequence(lambda n: 'foo%d' % n) + + +class WithMultipleGetOrCreateFieldsFactory(SQLAlchemyModelFactory): + class Meta: + model = models.MultifieldUniqueModel + sqlalchemy_get_or_create = ("slug", "text",) + sqlalchemy_session = models.session + sqlalchemy_session_persistence = 'commit' + + id = factory.Sequence(lambda n: n) + slug = factory.Sequence(lambda n: "slug%s" % n) + text = factory.Sequence(lambda n: "text%s" % n) + + +class SQLAlchemyPkSequenceTestCase(unittest.TestCase): def setUp(self): super().setUp() StandardFactory.reset_sequence(1) @@ -74,6 +109,61 @@ def test_pk_force_value(self): self.assertEqual(0, std2.id) +class SQLAlchemyGetOrCreateTests(unittest.TestCase): + def setUp(self): + models.session.rollback() + + def test_simple_call(self): + obj1 = WithGetOrCreateFieldFactory(foo='foo1') + obj2 = WithGetOrCreateFieldFactory(foo='foo1') + self.assertEqual(obj1, obj2) + + def test_missing_arg(self): + with self.assertRaises(factory.FactoryError): + MultifieldModelFactory() + + def test_raises_exception_when_existing_objs(self): + StandardFactory.create_batch(2, foo='foo') + with self.assertRaises(sqlalchemy.orm.exc.MultipleResultsFound): + WithGetOrCreateFieldFactory(foo='foo') + + def test_multicall(self): + objs = MultifieldModelFactory.create_batch( + 6, + slug=factory.Iterator(['main', 'alt']), + ) + self.assertEqual(6, len(objs)) + self.assertEqual(2, len(set(objs))) + self.assertEqual( + list( + obj.slug for obj in models.session.query( + models.MultiFieldModel.slug + ) + ), + ["alt", "main"], + ) + + +class MultipleGetOrCreateFieldsTest(unittest.TestCase): + def setUp(self): + models.session.rollback() + + def test_one_defined(self): + obj1 = WithMultipleGetOrCreateFieldsFactory() + obj2 = WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug) + self.assertEqual(obj1, obj2) + + def test_both_defined(self): + obj1 = WithMultipleGetOrCreateFieldsFactory() + with self.assertRaises(sqlalchemy.exc.IntegrityError): + WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug, text="alt") + + def test_unique_field_not_in_get_or_create(self): + WithMultipleGetOrCreateFieldsFactory(title='Title') + with self.assertRaises(sqlalchemy.exc.IntegrityError): + WithMultipleGetOrCreateFieldsFactory(title='Title') + + class SQLAlchemySessionPersistenceTestCase(unittest.TestCase): def setUp(self): super().setUp() From 0a564cd89fd2cfe0e7cfb44dcb721efc7313a8d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 31 Jul 2020 12:51:17 +0200 Subject: [PATCH 488/714] Upgrade to isort 5 https://timothycrosley.github.io/isort/docs/major_releases/introducing_isort_5/ The `--recursive` option is now the default behavior. Linting failed with error: ``` ERROR: /home/travis/build/FactoryBoy/factory_boy/examples/flask_alchemy/demoapp_factories.py Imports are incorrectly sorted and/or formatted. --- /home/travis/build/FactoryBoy/factory_boy/examples/flask_alchemy/demoapp_factories.py:before 2020-07-31 07:14:11.953212 +++ /home/travis/build/FactoryBoy/factory_boy/examples/flask_alchemy/demoapp_factories.py:after 2020-07-31 07:14:42.688270 @@ -1,4 +1,5 @@ import demoapp + import factory.alchemy import factory.fuzzy ``` --- Makefile | 2 +- examples/flask_alchemy/demoapp_factories.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0f5ff00c..a892821b 100644 --- a/Makefile +++ b/Makefile @@ -76,7 +76,7 @@ example-test: lint: $(FLAKE8) --exclude $(PACKAGE)/__init__.py $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) $(FLAKE8) --ignore F401 $(PACKAGE)/__init__.py - $(ISORT) --recursive --check-only --diff $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) + $(ISORT) --check-only --diff $(EXAMPLES_DIR) $(PACKAGE) $(SETUP_PY) $(TESTS_DIR) check-manifest coverage: diff --git a/examples/flask_alchemy/demoapp_factories.py b/examples/flask_alchemy/demoapp_factories.py index 61bbb774..35472425 100644 --- a/examples/flask_alchemy/demoapp_factories.py +++ b/examples/flask_alchemy/demoapp_factories.py @@ -1,4 +1,5 @@ import demoapp + import factory.alchemy import factory.fuzzy From 7a2d57778f1d8167cbcfb2cf6d61558d7ee39abf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 31 Jul 2020 14:19:48 +0200 Subject: [PATCH 489/714] Restore submodule imports in package __init__ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do not break import paths for users doing: ```python import factory class MyFactory(factory.django.DjangoModelFactory): # ... ``` Although it would be cleaner not to import the package at all, users are relying on these import paths. Without a concrete issue to solve, avoid breaking the import paths. Reviewed-by: Raphaël Barrois --- factory/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/factory/__init__.py b/factory/__init__.py index 93995214..3eb59017 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -51,6 +51,23 @@ stub_batch, ) +try: + import alchemy +except ImportError: + pass +try: + import django +except ImportError: + pass +try: + import mogo +except ImportError: + pass +try: + import mongoengine +except ImportError: + pass + __author__ = 'Raphaël Barrois ' try: # Python 3.8+ From 288b7fdd25c5373427d98c900542a38d130e1192 Mon Sep 17 00:00:00 2001 From: Matthew Duck Date: Sat, 27 Jun 2020 07:42:41 +0100 Subject: [PATCH 490/714] docs(orm): #483 clarify get_or_create behaviour Some users have expected the `django_get_or_create` feature to update a model after retrieving it from the database, but this is not the intended behaviour. Add docs to make this explicit. See https://github.com/FactoryBoy/factory_boy/issues/483 for discussion on the feature. --- docs/orms.rst | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/docs/orms.rst b/docs/orms.rst index e67eb13b..4508d235 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -89,6 +89,25 @@ All factories for a Django :class:`~django.db.models.Model` should use the >>> User.objects.all() [, ] + .. warning:: When ``django_get_or_create`` is used, be aware that any new + values passed to the Factory are **not** used to update an existing model. + + .. code-block:: pycon + + >>> john = UserFactory(username="john") # Fetches the existing user + + + >>> john.email + "john@example.com" + + >>> john = UserFactory( # Fetches the existing user + >>> username="john", # and provides a new email value + >>> email="a_new_email@example.com" + >>> ) + + + >>> john.email # The email value was not updated + "john@example.com" Extra fields @@ -362,6 +381,26 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: >>> User.query.all() [, ] + .. warning:: When ``sqlalchemy_get_or_create`` is used, be aware that any new + values passed to the Factory are **not** used to update an existing model. + + .. code-block:: pycon + + >>> john = UserFactory(username="john") # Fetches the existing user + + + >>> john.email + "john@example.com" + + >>> john = UserFactory( # Fetches the existing user + >>> username="john", # and provides a new email value + >>> email="a_new_email@example.com" + >>> ) + + + >>> john.email # The email value was not updated + "john@example.com" + A (very) simple example: From 5b9ad19cb84552d6621ff016d4801d157a0444ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 26 Jun 2020 13:21:49 +0200 Subject: [PATCH 491/714] Fix incorrectly highlighted code section ChangeLog is ReStructuredText, not Markdown. --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9bccbb5e..af27943d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,7 +18,7 @@ ChangeLog - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. - :issue:`598`: Limit ``get_or_create`` behavior to fields specified in ``django_get_or_create``. - - :issue:`606`: Re-raise :class:`~django.db.IntegrityError` when `django_get_or_create` with multiple fields fails to lookup model using user provided keyword arguments. + - :issue:`606`: Re-raise :class:`~django.db.IntegrityError` when ``django_get_or_create`` with multiple fields fails to lookup model using user provided keyword arguments. - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. *Removed:* From 43f557d312b3115bc56048e160e36b8a2a521842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 17 Jul 2020 14:44:27 +0200 Subject: [PATCH 492/714] Simplify Django imports The `django` module is no longer imported by `factory.__init__`. Importing `factory.django` makes it obvious Django is required. Django not being installed can raise an `ImportError`. --- factory/django.py | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/factory/django.py b/factory/django.py index e9385eab..8261a73d 100644 --- a/factory/django.py +++ b/factory/django.py @@ -9,17 +9,10 @@ import logging import os -from . import base, declarations, errors - -try: - import django - from django.core import files as django_files - from django.db import IntegrityError -except ImportError as e: # pragma: no cover - django = None - django_files = None - import_failure = e +from django.core import files as django_files +from django.db import IntegrityError +from . import base, declarations, errors logger = logging.getLogger('factory.generate') @@ -27,12 +20,6 @@ DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS -def require_django(): - """Simple helper to ensure Django is available.""" - if django_files is None: # pragma: no cover - raise import_failure - - _LAZY_LOADS = {} @@ -51,13 +38,8 @@ def _lazy_load_get_model(): get_model loads django.conf.settings, which may fail if the settings haven't been configured yet. """ - if django is None: - def _get_model(app, model): - raise import_failure - else: - from django import apps as django_apps - _get_model = django_apps.apps.get_model - _LAZY_LOADS['get_model'] = _get_model + from django import apps as django_apps + _LAZY_LOADS['get_model'] = django_apps.apps.get_model class DjangoOptions(base.FactoryOptions): @@ -196,10 +178,6 @@ class FileField(declarations.ParameteredAttribute): DEFAULT_FILENAME = 'example.dat' - def __init__(self, **defaults): - require_django() - super().__init__(**defaults) - def _make_data(self, params): """Create data for the field.""" return params.get('data', b'') From 85543a502eed7a3d5de17669a98e761e92a78add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 26 Jun 2020 12:58:19 +0200 Subject: [PATCH 493/714] Make a breaking changes summary in change log Helps users identify breaking changes and the recommended import. Also, fix a couple formatting issues with the changelog. --- docs/changelog.rst | 47 +++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index af27943d..0c6043ab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,36 @@ ChangeLog 3.0.0 (unreleased) ------------------ +Breaking changes +"""""""""""""""" + +The following aliases were removed: + ++------------------------------------------------+---------------------------------------------------+ +| Broken alias | New import | ++================================================+===================================================+ +| ``from factory import DjangoModelFactory`` | ``from factory.django import DjangoModelFactory`` | ++------------------------------------------------+---------------------------------------------------+ +| ``from factory import MogoFactory`` | ``from factory.mogo import MogoFactory`` | ++------------------------------------------------+---------------------------------------------------+ +| ``from factory.fuzzy import get_random_state`` | ``from factory.random import get_random_state`` | ++------------------------------------------------+---------------------------------------------------+ +| ``from factory.fuzzy import set_random_state`` | ``from factory.random import set_random_state`` | ++------------------------------------------------+---------------------------------------------------+ +| ``from factory.fuzzy import reseed_random`` | ``from factory.random import reseed_random`` | ++------------------------------------------------+---------------------------------------------------+ + +*Removed:* + + - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. + - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. + - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use + ``sqlalchemy_session_persistence = "flush"`` instead. + - Drop deprecated ``attributes()`` from :class:`~factory.Factory` subclasses; use + ``factory.make_factory(dict, FactoryClass._meta.pre_declarations)`` instead. + - Drop deprecated ``declarations()`` from :class:`~factory.Factory` subclasses; use ``FactoryClass._meta.pre_declarations`` instead. + - Drop ``factory.compat`` module. + *New:* - Add support for Python 3.8 @@ -21,23 +51,6 @@ ChangeLog - :issue:`606`: Re-raise :class:`~django.db.IntegrityError` when ``django_get_or_create`` with multiple fields fails to lookup model using user provided keyword arguments. - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. -*Removed:* - - - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. - - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. - - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use - ``sqlalchemy_session_persistence = "flush"`` instead. - - Drop deprecated ``attributes()`` and ``declarations()`` methods from ``factory.BaseFactory``. - - Drop deprecated aliases ``factory.fuzzy`` aliases for ``get_random_state()``, ``set_random_state()`` and - ``reseed_random()`` .fuzzy``. Use methods from the ``factory.random`` module instead. - - Remove references ``django``, ``alchemy``, ``mogo`` and ``mongoengine`` - in module ``factory``. Import the submodules ``factory.django``, - ``factory.alchemy``, ``factory.mogo`` and ``factory.mongoengine`` - instead. - - Remove aliases ``factory.DjangoFactory`` and ``factory.MogoFactory``. Use - ``factory.django.DjangoFactory`` and ``factory.mogo.MogoFactory`` - instead. - 2.12.0 (2019-05-11) ------------------- From a56a8520bbdb1b7dfd5bdb8ad0391a71d635a719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 26 Jun 2020 09:54:13 +0200 Subject: [PATCH 494/714] Better error when feeding SubFactory a non-factory Before this change, calling `factory.SubFactory(UserModel)` would raise an unexpected error, `Options object has no attribute 'pre_declarations'`. That message isn't very helpful; instead, a `AssociatedClassError` will be raised, pointing at both the calling factory and called model. This change also introduces a module for collecting tests related to the developer experience: error messages, debugging, etc. Closes #561. --- docs/changelog.rst | 4 +++ factory/base.py | 2 +- factory/builder.py | 11 +++++++ tests/test_dev_experience.py | 58 ++++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/test_dev_experience.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 0c6043ab..5e919bc7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -43,6 +43,10 @@ The following aliases were removed: - :py:meth:`get_random_state()` now represents the state of Faker and ``factory_boy`` fuzzy attributes. - Add SQLAlchemy ``get_or_create`` support +*Improvements:* + + - :issue:`561`: Display a developer-friendly error message when providing a model instead of a factory in a :class:`~factory.declarations.SubFactory` class. + *Bugfix:* - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. diff --git a/factory/base.py b/factory/base.py index 2f1d1228..bd00bf67 100644 --- a/factory/base.py +++ b/factory/base.py @@ -375,7 +375,7 @@ def get_model_class(self): return self.model def __str__(self): - return "<%s for %s>" % (self.__class__.__name__, self.factory.__class__.__name__) + return "<%s for %s>" % (self.__class__.__name__, self.factory.__name__) def __repr__(self): return str(self) diff --git a/factory/builder.py b/factory/builder.py index b494cc30..09153f76 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -225,9 +225,17 @@ def chain(self): return (self.stub,) + parent_chain def recurse(self, factory, declarations, force_sequence=None): + from . import base + if not issubclass(factory, base.BaseFactory): + raise errors.AssociatedClassError( + "%r: Attempting to recursing into a non-factory object %r" + % (self, factory)) builder = self.builder.recurse(factory._meta, declarations) return builder.build(parent_step=self, force_sequence=force_sequence) + def __repr__(self): + return "".format(self.builder) + class StepBuilder: """A factory instantiation step. @@ -305,6 +313,9 @@ def recurse(self, factory_meta, extras): """Recurse into a sub-factory call.""" return self.__class__(factory_meta, extras, strategy=self.strategy) + def __repr__(self): + return "" % (self.factory_meta, self.strategy) + class Resolver: """Resolve a set of declarations. diff --git a/tests/test_dev_experience.py b/tests/test_dev_experience.py new file mode 100644 index 00000000..6677b08c --- /dev/null +++ b/tests/test_dev_experience.py @@ -0,0 +1,58 @@ +# Copyright: See the LICENSE file. + +"""Tests about developer experience: help messages, errors, etc.""" + +import collections +import unittest + +import factory +import factory.errors + +Country = collections.namedtuple('Country', ['name', 'continent', 'capital_city']) +City = collections.namedtuple('City', ['name', 'population']) + + +class DeclarationTests(unittest.TestCase): + def test_subfactory_to_model(self): + """A helpful error message occurs when pointing a subfactory to a model.""" + class CountryFactory(factory.Factory): + class Meta: + model = Country + + name = factory.Faker('country') + continent = "Antarctica" + + # Error here: pointing the SubFactory to a model, not a factory. + capital_city = factory.SubFactory(City) + + with self.assertRaises(factory.errors.AssociatedClassError) as raised: + CountryFactory() + + self.assertIn('City', str(raised.exception)) + self.assertIn('Country', str(raised.exception)) + + def test_subfactory_to_factorylike_model(self): + """A helpful error message occurs when pointing a subfactory to a model. + + This time with a model that looks more like a factory (ie has a `._meta`).""" + + class CityModel: + _meta = None + name = "Coruscant" + population = 0 + + class CountryFactory(factory.Factory): + class Meta: + model = Country + + name = factory.Faker('country') + continent = "Antarctica" + + # Error here: pointing the SubFactory to a model, not a factory. + capital_city = factory.SubFactory(CityModel) + + with self.assertRaises(factory.errors.AssociatedClassError) as raised: + CountryFactory() + + self.assertIn('CityModel', str(raised.exception)) + self.assertIn('Country', str(raised.exception)) From 05d9d443a8285623d07146d5c49d3925b8b7c7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 12 Aug 2020 22:44:30 +0200 Subject: [PATCH 495/714] Include documentation tools in dev installs. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a892821b..43b83e07 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ clean: # DOC: Install and/or upgrade dependencies update: pip install --upgrade pip setuptools - pip install --upgrade --editable .[dev] + pip install --upgrade --editable .[dev,doc] pip freeze From decd1f3f6e8dba98ef153e3c6718e7ba65795b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 12 Aug 2020 22:56:52 +0200 Subject: [PATCH 496/714] Fix README's RST syntax. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 534ca7ec..8a60e31d 100644 --- a/README.rst +++ b/README.rst @@ -146,7 +146,7 @@ through specific ``factory.Factory`` subclasses: * MongoEngine, with ``factory.mongoengine.MongoEngineFactory`` * SQLAlchemy, with ``factory.alchemy.SQLAlchemyModelFactory`` -More details can be found in the :ref:`ORM section `. +More details can be found in the ORM section. Using factories From 3b0b867548003dcfe00c6e93a8f20e96ba065112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 12 Aug 2020 23:02:23 +0200 Subject: [PATCH 497/714] Preparing release 3.0.0 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5e919bc7..26168735 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -3.0.0 (unreleased) +3.0.0 (2020-08-12) ------------------ Breaking changes diff --git a/setup.cfg b/setup.cfg index 7ff81fd5..b2c82ccf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.0.0.dev0 +version = 3.0.0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From 80854c99e9c9ba3648b6b5ee8aceec003ee68692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 12 Aug 2020 23:02:39 +0200 Subject: [PATCH 498/714] Back to development: 3.0.1 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26168735..6f6ebb2d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +3.0.1 (unreleased) +------------------ + +- Nothing changed yet. + + 3.0.0 (2020-08-12) ------------------ diff --git a/setup.cfg b/setup.cfg index b2c82ccf..46d2e933 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.0.0 +version = 3.0.1.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From efb5019e424bc74dc030422e8900a3f92df45275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 13 Aug 2020 09:56:04 +0200 Subject: [PATCH 499/714] Hotfix: Fix `factory.django` aliases. --- docs/changelog.rst | 4 +++- factory/__init__.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6f6ebb2d..bf17d330 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,9 @@ ChangeLog 3.0.1 (unreleased) ------------------ -- Nothing changed yet. +*Bugfix:* + + - :issue:`769`: Fix ``import factory; factory.django.DjangoModelFactory`` and similar calls. 3.0.0 (2020-08-12) diff --git a/factory/__init__.py b/factory/__init__.py index 3eb59017..2f627000 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -52,19 +52,19 @@ ) try: - import alchemy + from . import alchemy except ImportError: pass try: - import django + from . import django except ImportError: pass try: - import mogo + from . import mogo except ImportError: pass try: - import mongoengine + from . import mongoengine except ImportError: pass From cea134a4d831abf3efb9af350700aa7ab0c64058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 13 Aug 2020 09:56:44 +0200 Subject: [PATCH 500/714] Preparing release 3.0.1 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bf17d330..4d381857 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -3.0.1 (unreleased) +3.0.1 (2020-08-13) ------------------ *Bugfix:* diff --git a/setup.cfg b/setup.cfg index 46d2e933..abecc3f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.0.1.dev0 +version = 3.0.1 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From 901a7309f0e60abd4ba5d18b83d2e0ac5cba3ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 13 Aug 2020 09:56:56 +0200 Subject: [PATCH 501/714] Back to development: 3.0.2 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4d381857..4d4a6f77 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +3.0.2 (unreleased) +------------------ + +- Nothing changed yet. + + 3.0.1 (2020-08-13) ------------------ diff --git a/setup.cfg b/setup.cfg index abecc3f3..c85b3d1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.0.1 +version = 3.0.2.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From f0a4ef008f07f8d42221565d8c33b88083f0be6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 14 Aug 2020 10:34:35 +0200 Subject: [PATCH 502/714] Allow nesting declarations in Faker params. A `factory.Faker()` call may accept extra keywords, as required by its provider. This change allows using any factory-supported declaration (`SelfAttribute`, fuzzy, other faker calls). This is built through a new class, `ParameteredDeclaration`, which holds all the specific code for lazily evaluating parameters to a declaration. That class replaces `ParameteredAttribute` in other fields with a similar behaviour, namely `django.FileField` and `django.ImageField`. The `ParameteredAttribute` internal class, while similar, is kept, as it provides a dedicated support for `SubFactory` and might be used for other declarations relying on a custom factory. Moreover, although part of the private API, it is already relied upon by other projects [1]. Once `ParameteredDeclaration`' API is stabilised, it could be documented as a formal extension point for custom declarations. [1] https://github.com/mvantellingen/wagtail-factories/blob/master/src/wagtail_factories/blocks.py#L20 --- docs/changelog.rst | 6 +++-- docs/reference.rst | 39 ++++++++++++++++++++++++++++ factory/declarations.py | 35 ++++++++++++++++++++++++++ factory/django.py | 7 ++---- factory/faker.py | 24 ++++++++---------- tests/test_faker.py | 56 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 145 insertions(+), 22 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4d4a6f77..73299887 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,12 @@ ChangeLog ========= -3.0.2 (unreleased) +3.1.0 (unreleased) ------------------ -- Nothing changed yet. +*New:* + + - Allow all types of declarations in :class:`factory.Faker` calls - enables references to other faker-defined attributes. 3.0.1 (2020-08-13) diff --git a/docs/reference.rst b/docs/reference.rst index b5f2bbe3..d255f6e0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -695,6 +695,43 @@ Faker >>> user.name 'Lucy Cechtelar' + Some providers accept parameters; they should be passed after the provider name: + + .. code-block:: python + + class UserFactory(fatory.Factory): + class Meta: + model = User + + arrival = factory.Faker( + 'date_between_dates', + date_start=datetime.date(2020, 1, 1), + date_end=datetime.date(2020, 5, 31), + ) + + As with :class:`~factory.SubFactory`, the parameters can be any valid declaration. + This does not apply to the provider name or the locale. + + .. code-block:: python + + class TripFactory(fatory.Factory): + class Meta: + model = Trip + + departure = factory.Faker( + 'date', + end_datetime=datetime.date.today(), + ) + arrival = factory.Faker( + 'date_between_dates', + date_start=factory.SelfAttribute('..departure'), + ) + + .. note:: When using :class:`~factory.SelfAttribute` or :class:`~factory.LazyAttribute` + in a :class:`factory.Faker` parameter, the current object is the declarations + provided to the :class:`~factory.Faker` declaration; go :ref:`up a level ` + to reach fields of the surrounding :class:`~factory.Factory`, as shown + in the ``SelfAttribute('..xxx')`` example above. .. attribute:: locale @@ -1246,6 +1283,8 @@ That declaration takes a single argument, a dot-delimited path to the attribute 3 +.. _factory-parent: + Parents ~~~~~~~ diff --git a/factory/declarations.py b/factory/declarations.py index 32d4b620..9ea91bb9 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -331,6 +331,39 @@ def generate(self, step, params): raise NotImplementedError() +class ParameteredDeclaration(BaseDeclaration): + """A declaration with parameters. + + The parameters can be any factory-enabled declaration, and will be resolved + before the call to the user-defined code in `self.generate()`. + + Attributes: + defaults (dict): Default values for the parameters; can be overridden + by call-time parameters. Accepts BaseDeclaration subclasses. + """ + + def __init__(self, **defaults): + self.defaults = defaults + super().__init__() + + def unroll_context(self, instance, step, context): + merged_context = {} + merged_context.update(self.defaults) + merged_context.update(context) + return super().unroll_context(instance, step, merged_context) + + def evaluate(self, instance, step, extra): + return self.generate(extra) + + def generate(self, params): + """Generate a value for this declaration. + + Args: + params (dict): the parameters, after a factory evaluation. + """ + raise NotImplementedError() + + class _FactoryWrapper: """Handle a 'factory' arg. @@ -375,6 +408,8 @@ class SubFactory(ParameteredAttribute): """ EXTEND_CONTAINERS = True + # Whether to align the attribute's sequence counter to the holding + # factory's sequence counter FORCE_SEQUENCE = False UNROLL_CONTEXT_BEFORE_EVALUATION = False diff --git a/factory/django.py b/factory/django.py index 8261a73d..5405be71 100644 --- a/factory/django.py +++ b/factory/django.py @@ -173,7 +173,7 @@ def _after_postgeneration(cls, instance, create, results=None): instance.save() -class FileField(declarations.ParameteredAttribute): +class FileField(declarations.ParameteredDeclaration): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' @@ -219,11 +219,8 @@ def _make_content(self, params): filename = params.get('filename', default_filename) return filename, content - def generate(self, step, params): + def generate(self, params): """Fill in the field.""" - # Recurse into a DictFactory: allows users to have some params depending - # on others. - params = step.recurse(base.DictFactory, params, force_sequence=step.sequence) filename, content = self._make_content(params) return django_files.File(content.file, filename) diff --git a/factory/faker.py b/factory/faker.py index 6f10b785..1c9e28aa 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -21,7 +21,7 @@ class Meta: from . import declarations -class Faker(declarations.BaseDeclaration): +class Faker(declarations.ParameteredDeclaration): """Wrapper for 'faker' values. Args: @@ -36,20 +36,16 @@ class Faker(declarations.BaseDeclaration): >>> foo = factory.Faker('name') """ def __init__(self, provider, **kwargs): - super().__init__() + locale = kwargs.pop('locale', None) self.provider = provider - self.provider_kwargs = kwargs - self.locale = kwargs.pop('locale', None) - - def generate(self, extra_kwargs=None): - kwargs = {} - kwargs.update(self.provider_kwargs) - kwargs.update(extra_kwargs or {}) - subfaker = self._get_faker(self.locale) - return subfaker.format(self.provider, **kwargs) - - def evaluate(self, instance, step, extra): - return self.generate(extra) + super().__init__( + locale=locale, + **kwargs) + + def generate(self, params): + locale = params.pop('locale') + subfaker = self._get_faker(locale) + return subfaker.format(self.provider, **params) _FAKER_REGISTRY = {} _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE diff --git a/tests/test_faker.py b/tests/test_faker.py index 30cd5a59..fb9b7218 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -1,5 +1,7 @@ # Copyright: See the LICENSE file. +import collections +import datetime import random import unittest @@ -17,6 +19,16 @@ def format(self, provider, **kwargs): return self.expected[provider] +class AdvancedMockFaker: + def __init__(self, handlers): + self.handlers = handlers + self.random = random.Random() + + def format(self, provider, **kwargs): + handler = self.handlers[provider] + return handler(**kwargs) + + class FakerTests(unittest.TestCase): def setUp(self): self._real_fakers = factory.Faker._FAKER_REGISTRY @@ -30,10 +42,15 @@ def _setup_mock_faker(self, locale=None, **definitions): locale = factory.Faker._DEFAULT_LOCALE factory.Faker._FAKER_REGISTRY[locale] = MockFaker(definitions) + def _setup_advanced_mock_faker(self, locale=None, **handlers): + if locale is None: + locale = factory.Faker._DEFAULT_LOCALE + factory.Faker._FAKER_REGISTRY[locale] = AdvancedMockFaker(handlers) + def test_simple_biased(self): self._setup_mock_faker(name="John Doe") faker_field = factory.Faker('name') - self.assertEqual("John Doe", faker_field.generate()) + self.assertEqual("John Doe", faker_field.generate({'locale': None})) def test_full_factory(self): class Profile: @@ -114,3 +131,40 @@ def smiley(self): face = FaceFactory() self.assertEqual(":)", face.smiley) self.assertEqual("(:", face.french_smiley) + + def test_faker_customization(self): + """Factory declarations in Faker parameters should be accepted.""" + Trip = collections.namedtuple('Trip', ['departure', 'transfer', 'arrival']) + + may_4th = datetime.date(1977, 5, 4) + may_25th = datetime.date(1977, 5, 25) + october_19th = datetime.date(1977, 10, 19) + + class TripFactory(factory.Factory): + class Meta: + model = Trip + + departure = may_4th + arrival = may_25th + transfer = factory.Faker( + 'date_between_dates', + start_date=factory.SelfAttribute('..departure'), + end_date=factory.SelfAttribute('..arrival'), + ) + + def fake_select_date(start_date, end_date): + """Fake date_between_dates.""" + # Ensure that dates have been transfered from the factory + # to Faker parameters. + self.assertEqual(start_date, may_4th) + self.assertEqual(end_date, may_25th) + return october_19th + + self._setup_advanced_mock_faker( + date_between_dates=fake_select_date, + ) + + trip = TripFactory() + self.assertEqual(may_4th, trip.departure) + self.assertEqual(october_19th, trip.transfer) + self.assertEqual(may_25th, trip.arrival) From 55460a5611fd7f31745291ca68ed388787980279 Mon Sep 17 00:00:00 2001 From: Daniel Leal Date: Thu, 17 Sep 2020 13:18:14 -0300 Subject: [PATCH 503/714] Fix small README class inconsistency --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 8a60e31d..56568ce6 100644 --- a/README.rst +++ b/README.rst @@ -213,7 +213,7 @@ For this, factory_boy relies on the excellent `faker >> UserFactory() + >>> RandomUserFactory() From eefd05917f5f129bcc8e5f00deb621f09a6151a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 2 Oct 2020 22:02:57 +0200 Subject: [PATCH 504/714] Restore wheel building. --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index c85b3d1a..1f56fcf8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,9 @@ doc = Sphinx sphinx_rtd_theme +[bdist_wheel] +universal = 1 + [zest.releaser] ; semver-style versions version-levels = 3 From 688820f549de522f279ac0c72446a6b3f9bf1a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 2 Oct 2020 22:03:44 +0200 Subject: [PATCH 505/714] Preparing release 3.1.0 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 73299887..005da8f2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,7 @@ ChangeLog ========= -3.1.0 (unreleased) +3.1.0 (2020-10-02) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index 1f56fcf8..a9568812 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.0.2.dev0 +version = 3.1.0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From a9c9aa82e62ef60bba01aa5366b1814ab8e12266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 2 Oct 2020 22:03:59 +0200 Subject: [PATCH 506/714] Back to development: 3.1.1 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 005da8f2..7e84f9f6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ ChangeLog ========= +3.1.1 (unreleased) +------------------ + +- Nothing changed yet. + + 3.1.0 (2020-10-02) ------------------ diff --git a/setup.cfg b/setup.cfg index a9568812..1ce4b3d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.1.0 +version = 3.1.1.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From 2263d462f0af94efe0962851f1636d4989fac98a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 11 Oct 2020 19:22:40 +0200 Subject: [PATCH 507/714] Drop support for Django 1.11 Django 1.11 is no longer supported upstream. https://www.djangoproject.com/download/#supported-versions --- docs/changelog.rst | 4 +++- setup.cfg | 1 - tox.ini | 6 ++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7e84f9f6..2d9d225f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,7 +4,9 @@ ChangeLog 3.1.1 (unreleased) ------------------ -- Nothing changed yet. +*Removed:* + + - Drop support for Django 1.11. This version `is not maintained anymore `__. 3.1.0 (2020-10-02) diff --git a/setup.cfg b/setup.cfg index 1ce4b3d4..a504cfe6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,7 +13,6 @@ license = MIT classifiers = Development Status :: 5 - Production/Stable Framework :: Django - Framework :: Django :: 1.11 Framework :: Django :: 2.2 Framework :: Django :: 3.0 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index cefd27f3..7690b94c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,9 @@ minversion = 1.9 envlist = lint - py{35,36,37,38}-django111-alchemy-mongoengine, py{35,36,37,38}-django22-alchemy-mongoengine, py{36,37,38}-django30-alchemy-mongoengine, - pypy3-django{111,22}-alchemy-mongoengine, + pypy3-django22-alchemy-mongoengine, docs examples linkcheck @@ -14,10 +13,9 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = - django111: Django>=1.11,<1.12 django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 - django{111,22,30}: Pillow + django{22,30}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 64f10a18525572a5158b753d8f20195553fa5c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 11 Oct 2020 11:02:09 +0200 Subject: [PATCH 508/714] Drop support for Python 3.5 This version has reached its end of life. --- .travis.yml | 1 - Makefile | 3 --- README.rst | 2 +- docs/changelog.rst | 1 + factory/base.py | 7 +++---- setup.cfg | 3 +-- tox.ini | 2 +- 7 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 463dc10b..ecca3e35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ install: matrix: include: - - python: "3.5" - python: "3.6" - python: "3.7" - python: "3.8" diff --git a/Makefile b/Makefile index 43b83e07..99352205 100644 --- a/Makefile +++ b/Makefile @@ -52,13 +52,10 @@ testall: # DOC: Run tests for the currently installed version test: - # imp warning is a PendingDeprecationWarning for Python 3.5 and a - # DeprecationWarning for later versions. python \ -b \ -Werror \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ - -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":PendingDeprecationWarning:distutils: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ diff --git a/README.rst b/README.rst index 56568ce6..70607e01 100644 --- a/README.rst +++ b/README.rst @@ -318,7 +318,7 @@ The associated object's strategy will be used: Support Policy -------------- -``factory_boy`` supports Python 3.5 to 3.8 as well as PyPy3. +``factory_boy`` supports Python 3.6 to 3.8 as well as PyPy3. - **Python**'s `supported versions `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 2d9d225f..bb2b2d1a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ ChangeLog *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. + - Drop support for Python 3.5. This version `is not maintained anymore `__. 3.1.0 (2020-10-02) diff --git a/factory/base.py b/factory/base.py index bd00bf67..7965cf49 100644 --- a/factory/base.py +++ b/factory/base.py @@ -696,10 +696,9 @@ def _build(cls, model_class, *args, **kwargs): raise ValueError( "ListFactory %r does not support Meta.inline_args." % cls) - # When support for Python <3.6 is dropped sorting will no longer be required - # because dictionaries will already be ordered, this can then be changed to: - # values = kwargs.values() - values = [v for k, v in sorted(kwargs.items(), key=lambda item: int(item[0]))] + # kwargs are constructed from a list, their insertion order matches the list + # order, no additional sorting is required. + values = kwargs.values() return model_class(values) @classmethod diff --git a/setup.cfg b/setup.cfg index a504cfe6..f69ac27f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 @@ -33,7 +32,7 @@ classifiers = [options] zip_safe = false packages = factory -python_requires = >=3.5 +python_requires = >=3.6 install_requires = Faker>=0.7.0 [options.extras_require] diff --git a/tox.ini b/tox.ini index 7690b94c..05783a75 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ minversion = 1.9 envlist = lint - py{35,36,37,38}-django22-alchemy-mongoengine, + py{36,37,38}-django22-alchemy-mongoengine, py{36,37,38}-django30-alchemy-mongoengine, pypy3-django22-alchemy-mongoengine, docs From c918335d488845740ab17ae8e445d1749d3d4f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 11 Oct 2020 11:14:06 +0200 Subject: [PATCH 509/714] Add Django 3.1 to the test matrix Django 3.1 was released August 4th, 2020. --- docs/changelog.rst | 4 ++++ setup.cfg | 1 + tox.ini | 6 ++++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bb2b2d1a..692479ed 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ ChangeLog 3.1.1 (unreleased) ------------------ +*New:* + + - Add support for Django 3.1 + *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. diff --git a/setup.cfg b/setup.cfg index f69ac27f..0a8bedc0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,7 @@ classifiers = Framework :: Django Framework :: Django :: 2.2 Framework :: Django :: 3.0 + Framework :: Django :: 3.1 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index 05783a75..0c57a675 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,8 @@ envlist = lint py{36,37,38}-django22-alchemy-mongoengine, py{36,37,38}-django30-alchemy-mongoengine, - pypy3-django22-alchemy-mongoengine, + py{36,37,38}-django31-alchemy-mongoengine, + pypy3-django{22,30,31}-alchemy-mongoengine, docs examples linkcheck @@ -15,7 +16,8 @@ toxworkdir = {env:TOX_WORKDIR:.tox} deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 - django{22,30}: Pillow + django31: Django>=3.1,<3.2 + django{22,30,31}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 89e7553f748041f41614b43a4d1ea02860042536 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 17 Oct 2020 08:17:41 -0700 Subject: [PATCH 510/714] Regenerate Sphinx files using latest modern Sphinx version The Sphinx configurations can be simplified using the most recent upstream template. All existing configuration options have been carried over. The Makefile is also simplified to use a single catch all rule. The simplified Makefile allows for arbitrary targets without new edits. For example, could use `make spelling` after install the sphinxcontrib-spelling package. --- docs/Makefile | 136 ++++----------------------------- docs/conf.py | 205 ++++++-------------------------------------------- docs/make.bat | 205 +++++++++----------------------------------------- 3 files changed, 71 insertions(+), 475 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 7a77848e..d4bb2cbb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,130 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/FactoryBoy.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/FactoryBoy.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/FactoryBoy" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/FactoryBoy" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." +.PHONY: help Makefile -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index 546b6a46..1929a5e1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,13 +1,10 @@ -# Factory Boy documentation build configuration file, created by -# sphinx-quickstart on Thu Sep 15 23:51:15 2011. +# Configuration file for the Sphinx documentation builder. # -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- import os import sys @@ -17,16 +14,25 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.dirname(os.path.abspath('.'))) -# -- General configuration ----------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# -- Project information ----------------------------------------------------- + +project = 'Factory Boy' +copyright = '2011-2015, Raphaël Barrois, Mark Sandstrom' +author = 'adfasf' + +# The full version, including alpha/beta/rc tags +release = factory.__version__ +# The short X.Y version. +version = '.'.join(release.split('.')[:2]) + +# -- General configuration --------------------------------------------------- -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# 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.extlinks', @@ -42,197 +48,32 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' -# General information about the project. -project = 'Factory Boy' -copyright = '2011-2015, Raphaël Barrois, Mark Sandstrom' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The full version, including alpha/beta/rc tags. -release = factory.__version__ -# The short X.Y version. -version = '.'.join(release.split('.')[:2]) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - - -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None if 'READTHEDOCS_VERSION' in os.environ: # Use the readthedocs version string in preference to our known version. html_title = "{} {} documentation".format( project, os.environ['READTHEDOCS_VERSION']) -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'FactoryBoydoc' - - -# -- Options for LaTeX output -------------------------------------------------- - -# The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'FactoryBoy.tex', 'Factory Boy Documentation', - 'Raphaël Barrois, Mark Sandstrom', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -#latex_preamble = '' - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'factoryboy', 'Factory Boy Documentation', - ['Raphaël Barrois, Mark Sandstrom'], 1) -] - -# Example configuration for intersphinx: refer to the Python standard library. +# -- intersphinx ------------------------------------------------------------- intersphinx_mapping = { 'https://docs.python.org/': None, 'django': ( diff --git a/docs/make.bat b/docs/make.bat index e4ecc122..2119f510 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,170 +1,35 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\FactoryBoy.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\FactoryBoy.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd From 9b256e48dd58ef7c97e3ddf09d972d1bb1d2df8e Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 17 Oct 2020 08:32:01 -0700 Subject: [PATCH 511/714] Apply sphinxcontrib-spelling to the docs for spell checking Project URL: https://github.com/sphinx-contrib/spelling sphinxcontrib-spelling is a mature Sphinx extension to do spell checking across docs. It is used by other big name projects such as Django. This tool will now run as part of CI. All spelling mistakes caught by the tool have been fixed. The language is set to en_US so the British spelling of behaviour was converted to the American spelling. The credits.rst file is excluded from checking as it is mostly a list of people's names which often don't appear in the dictionary. --- .travis.yml | 2 ++ Makefile | 3 ++ README.rst | 6 ++-- docs/changelog.rst | 74 +++++++++++++++++++------------------- docs/conf.py | 6 ++++ docs/internals.rst | 8 ++--- docs/introduction.rst | 4 +-- docs/orms.rst | 2 +- docs/recipes.rst | 14 ++++---- docs/reference.rst | 16 ++++----- docs/spelling_wordlist.txt | 36 +++++++++++++++++++ setup.cfg | 1 + tox.ini | 2 +- 13 files changed, 110 insertions(+), 64 deletions(-) create mode 100644 docs/spelling_wordlist.txt diff --git a/.travis.yml b/.travis.yml index ecca3e35..1ecdd12a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ matrix: # Documentation - python: "3.8" env: TOXENV=docs + before_install: + - sudo apt-get -y install libenchant1c2a - python: "3.8" env: TOXENV=linkcheck diff --git a/Makefile b/Makefile index 99352205..7be6b92c 100644 --- a/Makefile +++ b/Makefile @@ -97,6 +97,9 @@ doc: linkcheck: $(MAKE) -C $(DOC_DIR) linkcheck +spelling: + $(MAKE) -C $(DOC_DIR) SPHINXOPTS=-W spelling + # DOC: Show this help message help: @grep -A1 '^# DOC:' Makefile \ diff --git a/README.rst b/README.rst index 70607e01..518f75bb 100644 --- a/README.rst +++ b/README.rst @@ -61,7 +61,7 @@ while only declaring the test-specific fields: ) # etc. -factory_boy is designed to work well with various ORMs (Django, Mongo, SQLAlchemy), +factory_boy is designed to work well with various ORMs (Django, MongoDB, SQLAlchemy), and can easily be extended for other libraries. Its main features include: @@ -325,7 +325,7 @@ Support Policy - **Django**'s `supported versions `__. - **SQLAlchemy**: `latest version on PyPI `__. -- **mongoengine**: `latest version on PyPI `__. +- **MongoEngine**: `latest version on PyPI `__. Debugging factory_boy --------------------- @@ -407,7 +407,7 @@ Valid options are: * ``ALCHEMY`` for ``SQLAlchemy`` -To avoid running ``mongoengine`` tests (e.g no mongo server installed), run: +To avoid running ``mongoengine`` tests (e.g no MongoDB server installed), run: .. code-block:: sh diff --git a/docs/changelog.rst b/docs/changelog.rst index 692479ed..4ebfc0c8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,7 +25,7 @@ ChangeLog 3.0.1 (2020-08-13) ------------------ -*Bugfix:* +*Bug fix:* - :issue:`769`: Fix ``import factory; factory.django.DjangoModelFactory`` and similar calls. @@ -76,10 +76,10 @@ The following aliases were removed: - :issue:`561`: Display a developer-friendly error message when providing a model instead of a factory in a :class:`~factory.declarations.SubFactory` class. -*Bugfix:* +*Bug fix:* - - Fix issue with SubFactory not preserving signal muting behaviour of the used factory, thanks `Patrick Stein `_. - - Fix issue with overriding params in a Trait, thanks `Grégoire Rocher `_. + - Fix issue with SubFactory not preserving signal muting behavior of the used factory, thanks `Patrick Stein `_. + - Fix issue with overriding parameters in a Trait, thanks `Grégoire Rocher `_. - :issue:`598`: Limit ``get_or_create`` behavior to fields specified in ``django_get_or_create``. - :issue:`606`: Re-raise :class:`~django.db.IntegrityError` when ``django_get_or_create`` with multiple fields fails to lookup model using user provided keyword arguments. - :issue:`630`: TypeError masked by __repr__ AttributeError when initializing ``Maybe`` with inconsistent phases. @@ -98,7 +98,7 @@ The following aliases were removed: - Add :class:`~factory.RelatedFactoryList` class for one-to-many support, thanks `Sean Harrington `_. - Make the `locale` argument for :class:`~factory.faker.Faker` keyword-only -*Bugfix:* +*Bug fix:* - Allow renamed arguments to be optional, thanks to `Justin Crown `_. - Fix `django_get_or_create` behavior when using multiple fields with `unique=True`, thanks to `@YPCrumble ` @@ -107,7 +107,7 @@ The following aliases were removed: 2.11.1 (2018-05-05) ------------------- -*Bugfix:* +*Bug fix:* - Fix passing deep context to a :class:`~factory.SubFactory` (``Foo(x__y__z=factory.Faker('name')``) @@ -115,7 +115,7 @@ The following aliases were removed: 2.11.0 (2018-05-05) ------------------- -*Bugfix:* +*Bug fix:* - Fix :class:`~factory.fuzzy.FuzzyFloat` to return a 15 decimal digits precision float by default - :issue:`451`: Restore :class:`~factory.django.FileField` to a @@ -127,7 +127,7 @@ The following aliases were removed: 2.10.0 (2018-01-28) ------------------- -*Bugfix:* +*Bug fix:* - :issue:`443`: Don't crash when calling :meth:`factory.Iterator.reset()` on a brand new iterator. @@ -141,7 +141,7 @@ The following aliases were removed: 2.9.2 (2017-08-03) ------------------ -*Bugfix:* +*Bug fix:* - Fix declaration corruption bug when a factory defined `foo__bar__baz=1` and a caller provided a `foo__bar=x` parameter at call time: this got merged into the factory's base @@ -152,7 +152,7 @@ The following aliases were removed: 2.9.1 (2017-08-02) ------------------ -*Bugfix:* +*Bug fix:* - Fix packaging issues (see https://github.com/zestsoftware/zest.releaser/issues/212) - Don't crash when debugging PostGenerationDeclaration @@ -163,7 +163,7 @@ The following aliases were removed: ------------------ This version brings massive changes to the core engine, thus reducing the number of -corner cases and weird behaviours. +corner cases and weird behaviors. *New:* @@ -184,7 +184,7 @@ corner cases and weird behaviours. 2.8.1 (2016-12-17) ------------------ -*Bugfix:* +*Bug fix:* - Fix packaging issues. @@ -200,7 +200,7 @@ corner cases and weird behaviours. thanks to `Oleg Pidsadnyi `_. - :issue:`309`: Provide new options for SQLAlchemy session persistence -*Bugfix:* +*Bug fix:* - :issue:`334`: Adjust for the package change in ``faker`` @@ -251,7 +251,7 @@ corner cases and weird behaviours. - Simplify imports for ORM layers, now available through a simple ``factory`` import, at ``factory.alchemy.SQLAlchemyModelFactory`` / ``factory.django.DjangoModelFactory`` / ``factory.mongoengine.MongoEngineFactory``. -*Bugfix:* +*Bug fix:* - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching @@ -263,7 +263,7 @@ corner cases and weird behaviours. 2.5.2 (2015-04-21) ------------------ -*Bugfix:* +*Bug fix:* - Add support for Django 1.7/1.8 - Add support for mongoengine>=0.9.0 / pymongo>=2.1 @@ -273,7 +273,7 @@ corner cases and weird behaviours. 2.5.1 (2015-03-27) ------------------ -*Bugfix:* +*Bug fix:* - Respect custom managers in :class:`~factory.django.DjangoModelFactory` (see :issue:`192`) - Allow passing declarations (e.g :class:`~factory.Sequence`) as parameters to :class:`~factory.django.FileField` @@ -291,7 +291,7 @@ corner cases and weird behaviours. - Support non-default databases at the factory level (see :issue:`171`) - Make :class:`factory.django.FileField` and :class:`factory.django.ImageField` non-post_generation, i.e normal fields also available in ``save()`` (see :issue:`141`). -*Bugfix:* +*Bug fix:* - Avoid issues when using :meth:`factory.django.mute_signals` on a base factory class (see :issue:`183`). - Fix limitations of :class:`factory.StubFactory`, that can now use :class:`factory.SubFactory` and co (see :issue:`131`). @@ -342,7 +342,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a 2.4.1 (2014-06-23) ------------------ -*Bugfix:* +*Bug fix:* - Fix overriding deeply inherited attributes (set in one factory, overridden in a subclass, used in a sub-sub-class). @@ -384,10 +384,10 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a 2.3.1 (2014-01-22) ------------------ -*Bugfix:* +*Bug fix:* - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:pr:`126`) - - Don't crash when handling objects whose __repr__ is non-pure-ascii bytes on Py2, + - Don't crash when handling objects whose ``__repr__`` is non-pure-ASCII bytes on Python 2, discovered by `mbertheau `_ (:issue:`123`) and `strycore `_ (:pr:`127`) .. _v2.3.0: @@ -406,7 +406,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a 2.2.1 (2013-09-24) ------------------ -*Bugfix:* +*Bug fix:* - Fixed sequence counter for :class:`~factory.django.DjangoModelFactory` when a factory inherits from another factory relating to an abstract model. @@ -416,13 +416,13 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a 2.2.0 (2013-09-24) ------------------ -*Bugfix:* +*Bug fix:* - Removed duplicated :class:`~factory.alchemy.SQLAlchemyModelFactory` lurking in :mod:`factory` (:pr:`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`) + 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:* @@ -449,7 +449,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a 2.1.1 (2013-07-02) ------------------ -*Bugfix:* +*Bug fix:* - Properly retrieve the ``color`` keyword argument passed to :class:`~factory.django.ImageField` @@ -476,9 +476,9 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a - Add :class:`factory.django.FileField` and :class:`factory.django.ImageField` hooks for related Django model fields (:issue:`52`) -*Bugfix* +*Bug fix* - - Properly handle non-integer pks in :class:`~factory.django.DjangoModelFactory` (:issue:`57`). + - Properly handle non-integer primary keys in :class:`~factory.django.DjangoModelFactory` (:issue:`57`). - Disable :class:`~factory.RelatedFactory` generation when a specific value was passed (:issue:`62`, thanks to `Gabe Koscky `_) @@ -603,7 +603,7 @@ All warnings will turn into errors starting from v2.0.0. In order to upgrade client code, apply the following rules: - Add a ``FACTORY_FOR`` attribute pointing to the target class to each - :class:`~factory.Factory`, instead of relying on automagic associated class + :class:`~factory.Factory`, instead of relying on automatic associated class discovery - When using factory_boy for Django models, have each factory inherit from :class:`~factory.django.DjangoModelFactory` @@ -633,7 +633,7 @@ In order to upgrade client code, apply the following rules: 1.1.5 (2012-07-09) ------------------ -*Bugfix:* +*Bug fix:* - Fix :class:`~factory.PostGenerationDeclaration` and derived classes. @@ -657,7 +657,7 @@ In order to upgrade client code, apply the following rules: 1.1.3 (2012-03-09) ------------------ -*Bugfix:* +*Bug fix:* - Fix packaging rules @@ -696,9 +696,9 @@ In order to upgrade client code, apply the following rules: - Provide the :func:`~factory.make_factory` helper: ``MyClassFactory = make_factory(MyClass, x=3, y=4)`` - Add :func:`~factory.build`, :func:`~factory.create`, :func:`~factory.stub` helpers -*Bugfix:* +*Bug fix:* - - Allow classmethod/staticmethod on factories + - Allow ``classmethod``/``staticmethod`` on factories *Deprecation:* @@ -721,11 +721,11 @@ In order to upgrade client code, apply the following rules: - 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:* +*Bug fix:* - Handle nested :class:`~factory.SubFactory` - Share sequence counter between parent and subclasses - - Fix :class:`~factory.SubFactory` / :class:`~factory.Sequence` interferences + - Fix :class:`~factory.SubFactory` / :class:`~factory.Sequence` interference .. _v1.0.2: @@ -748,7 +748,7 @@ In order to upgrade client code, apply the following rules: - Allow :class:`~factory.Factory` inheritance - Improve handling of custom build/create functions -*Bugfix:* +*Bug fix:* - Fix concurrency between :class:`~factory.LazyAttribute` and :class:`~factory.Sequence` @@ -766,8 +766,6 @@ In order to upgrade client code, apply the following rules: Credits ------- -* Initial version by Mark Sandstrom (2010) -* Developed by Raphaël Barrois since 2011 - +See :doc:`credits`. .. vim:et:ts=4:sw=4:tw=119:ft=rst: diff --git a/docs/conf.py b/docs/conf.py index 1929a5e1..b885b99d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -85,3 +85,9 @@ 'https://docs.sqlalchemy.org/en/latest/objects.inv', ), } + + +# -- spelling --------------------------------------------------------------- +spelling_exclude_patterns = [ + 'credits.rst', +] diff --git a/docs/internals.rst b/docs/internals.rst index 6cbf3cec..951ba54d 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -46,12 +46,12 @@ Parsing, Step 2: adapting the class definition for declaration ``foo``. -Instantiating, Step 1: Converging entrypoints ---------------------------------------------- +Instantiating, Step 1: Converging entry points +---------------------------------------------- First, decide the strategy: -- If the entrypoint is specific to a strategy (:meth:`~Factory.build`, +- If the entry point is specific to a strategy (:meth:`~Factory.build`, :meth:`~Factory.create_batch`, ...), use it - If it is generic (:meth:`~Factory.generate`, :meth:`Factory.__call__`), use the strategy defined at the :attr:`class Meta ` level @@ -59,7 +59,7 @@ First, decide the strategy: Then, we'll pass the strategy and passed-in overrides to the :meth:`~Factory._generate` method. -.. note:: According to the project roadmap, a future version will use a :meth:`~Factory._generate_batch`` at its core instead. +.. note:: According to the project road map, a future version will use a :meth:`~Factory._generate_batch`` at its core instead. A factory's :meth:`~Factory._generate` function actually delegates to a ``StepBuilder()`` object. This object will carry the overall "build an object" context (strategy, depth, and possibly other). diff --git a/docs/introduction.rst b/docs/introduction.rst index e1b29625..7fae2a2c 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -266,8 +266,8 @@ This is handled by the :data:`~factory.FactoryOptions.inline_args` attribute: -Altering a factory's behaviour: parameters and traits ------------------------------------------------------ +Altering a factory's behavior: parameters and traits +---------------------------------------------------- Some classes are better described with a few, simple parameters, that aren't fields on the actual model. In that case, use a :attr:`~factory.Factory.Params` declaration: diff --git a/docs/orms.rst b/docs/orms.rst index 4508d235..7d95aaf5 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -334,7 +334,7 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: .. attribute:: sqlalchemy_session_persistence - Control the action taken by sqlalchemy session at the end of a create call. + Control the action taken by ``sqlalchemy_session`` at the end of a create call. Valid values are: diff --git a/docs/recipes.rst b/docs/recipes.rst index c6671b20..f07b81b2 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -136,7 +136,7 @@ Since version 2.9, the :meth:`~factory.django.mute_signals` decorator should be >>> u.get_profile().title u"Lord" -Such behaviour can be extended to other situations where a signal interferes with +Such behavior can be extended to other situations where a signal interferes with factory_boy related factories. Any factories that call these classes with :class:`~factory.SubFactory` will also need to be decorated in the same manner. @@ -311,8 +311,8 @@ When a field of a related class should match one of the container: Here, we want: -- The User to have the lang of its country (``factory.SelfAttribute('country.lang')``) -- The Company owner to live in the country of the company (``factory.SelfAttribute('..country')``) +- The ``User`` to have the ``lang`` of its country (``factory.SelfAttribute('country.lang')``) +- The ``Company`` owner to live in the country of the company (``factory.SelfAttribute('..country')``) .. code-block:: python @@ -386,7 +386,7 @@ A common pattern with factory_boy is to use a :class:`factory.Sequence` declarat to provide varying values to attributes declared as unique. However, it is sometimes useful to force a given value to the counter, for instance -to ensure that tests are properly reproductible. +to ensure that tests are properly reproducible. factory_boy provides a few hooks for this: @@ -448,7 +448,7 @@ Forcing the initial value for all projects The sequence counter of a :class:`~factory.Factory` can also be set automatically upon the first call through the :meth:`~factory.Factory._setup_next_sequence` method; this helps when the - objects's attributes mustn't conflict with pre-existing data. + objects' attributes mustn't conflict with preexisting data. A typical example is to ensure that running a Python script twice will create non-conflicting objects, by setting up the counter to "max used value plus one": @@ -477,7 +477,7 @@ Forcing the initial value for all projects Using reproducible randomness ----------------------------- -Although using random values is great, it can provoke test flakyness. +Although using random values is great, it can provoke test flakiness. factory_boy provides a few helpers for this. .. note:: Those methods will seed the random engine used in both :class:`factory.Faker` and :mod:`factory.fuzzy` objects. @@ -500,7 +500,7 @@ Reproducing unseeded tests For such cases, use a combination of :meth:`factory.random.get_random_state()` and :meth:`factory.random.set_random_state()`. - Since the random state structure is implementation-specific, we recommand passing it around + Since the random state structure is implementation-specific, we recommend passing it around as a base64-encoded pickle dump. .. code-block:: python diff --git a/docs/reference.rst b/docs/reference.rst index d255f6e0..e1d88fe4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -18,7 +18,7 @@ Meta options .. versionadded:: 2.4.0 - A :class:`Factory`'s behaviour can be tuned through a few settings. + A :class:`Factory`'s behavior can be tuned through a few settings. For convenience, they are declared in a single ``class Meta`` attribute: @@ -248,7 +248,7 @@ Attributes and methods **Extension points:** A :class:`Factory` subclass may override a couple of class methods to adapt - its behaviour: + its behavior: .. classmethod:: _adjust_kwargs(cls, **kwargs) @@ -578,7 +578,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. OHAI_VIM* - .. warning:: For backward compatibility reasons, the default behaviour of + .. warning:: For backward compatibility reasons, the default behavior of factory_boy is to call ``MyClass.objects.create(*args, **kwargs)`` when using the ``create`` strategy. @@ -637,7 +637,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. 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`; + sending all output to ``stream``; upon leaving the context, the logging levels are reset. A typical use case is to understand what happens during a single factory call: @@ -1356,7 +1356,7 @@ Iterator .. versionadded:: 1.3.0 The ``cycle`` argument is available as of v1.3.0; previous versions - had a behaviour equivalent to ``cycle=False``. + had a behavior equivalent to ``cycle=False``. .. attribute:: getter @@ -1507,7 +1507,7 @@ with the :class:`Dict` and :class:`List` attributes: containing factory's one. - The :class:`Dict` behaviour can be tuned through the following parameters: + The :class:`Dict` behavior can be tuned through the following parameters: .. attribute:: dict_factory @@ -1541,7 +1541,7 @@ with the :class:`Dict` and :class:`List` attributes: ['user', 'active', 'superadmin'] - The :class:`List` behaviour can be tuned through the following parameters: + The :class:`List` behavior can be tuned through the following parameters: .. attribute:: list_factory @@ -1729,7 +1729,7 @@ If a value is passed for the :class:`RelatedFactory` attribute, this disables .. note:: The target of the :class:`RelatedFactory` is evaluated *after* the initial factory has been instantiated. However, the build context is passed down to that factory; this means that calls to - :class:`factory.SelfAttribute` *can* go back to the calling factorry's context: + :class:`factory.SelfAttribute` *can* go back to the calling factory's context: .. code-block:: python diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 00000000..95d4be7f --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,36 @@ +args +backends +backtrace +boolean +datastore +datetimes +dicts +Django +filename +fuzzer +fuzzers +fuzzying +getter +instantiation +iterable +iterables +kwarg +kwargs +metaclass +misconfiguration +Mogo +mongoengine +pre +prepend +pymongo +queryset +recurse +subclassed +subclasses +subclassing +subfactories +thoughtbot +tox +unexplicit +username +lookup diff --git a/setup.cfg b/setup.cfg index 0a8bedc0..6ccfdcd6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ dev = doc = Sphinx sphinx_rtd_theme + sphinxcontrib-spelling [bdist_wheel] universal = 1 diff --git a/tox.ini b/tox.ini index 0c57a675..a18e143a 100644 --- a/tox.ini +++ b/tox.ini @@ -29,7 +29,7 @@ basepython = python3.8 extras = doc whitelist_externals = make -commands = make doc +commands = make doc spelling [testenv:examples] basepython = python3.8 From b205a19787f2eb9927beae4def01acc5d8f9c45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 30 Oct 2020 22:14:01 +0100 Subject: [PATCH 512/714] =?UTF-8?q?Fix=20typo=20overriden=20=E2=86=92=20ov?= =?UTF-8?q?erridden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thank @nadege for spotting it! Co-authored-by: Nadège Michel --- docs/introduction.rst | 2 +- docs/reference.rst | 2 +- factory/base.py | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/introduction.rst b/docs/introduction.rst index 7fae2a2c..d26e4b9f 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -137,7 +137,7 @@ argument and returning the value for the field: >>> LogFactory() - >>> # The LazyFunction can be overriden + >>> # The LazyFunction can be overridden >>> LogFactory(timestamp=now - timedelta(days=1)) diff --git a/docs/reference.rst b/docs/reference.rst index e1d88fe4..12d7baac 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -805,7 +805,7 @@ return a value. >>> LogFactory() - >>> # The LazyFunction can be overriden + >>> # The LazyFunction can be overridden >>> LogFactory(timestamp=now - timedelta(days=1)) diff --git a/factory/base.py b/factory/base.py index 7965cf49..c338c9aa 100644 --- a/factory/base.py +++ b/factory/base.py @@ -506,12 +506,12 @@ def _create(cls, model_class, *args, **kwargs): @classmethod def build(cls, **kwargs): - """Build an instance of the associated class, with overriden attrs.""" + """Build an instance of the associated class, with overridden attrs.""" return cls._generate(enums.BUILD_STRATEGY, kwargs) @classmethod def build_batch(cls, size, **kwargs): - """Build a batch of instances of the given class, with overriden attrs. + """Build a batch of instances of the given class, with overridden attrs. Args: size (int): the number of instances to build @@ -523,12 +523,12 @@ def build_batch(cls, size, **kwargs): @classmethod def create(cls, **kwargs): - """Create an instance of the associated class, with overriden attrs.""" + """Create an instance of the associated class, with overridden attrs.""" return cls._generate(enums.CREATE_STRATEGY, kwargs) @classmethod def create_batch(cls, size, **kwargs): - """Create a batch of instances of the given class, with overriden attrs. + """Create a batch of instances of the given class, with overridden attrs. Args: size (int): the number of instances to create @@ -540,7 +540,7 @@ def create_batch(cls, size, **kwargs): @classmethod def stub(cls, **kwargs): - """Retrieve a stub of the associated class, with overriden attrs. + """Retrieve a stub of the associated class, with overridden attrs. This will return an object whose attributes are those defined in this factory's declarations or in the extra kwargs. @@ -549,7 +549,7 @@ def stub(cls, **kwargs): @classmethod def stub_batch(cls, size, **kwargs): - """Stub a batch of instances of the given class, with overriden attrs. + """Stub a batch of instances of the given class, with overridden attrs. Args: size (int): the number of instances to stub From 26d1e428018d007a018a4c4205e52a016fa12c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 30 Oct 2020 21:58:20 +0100 Subject: [PATCH 513/714] Upgrade Python 2 super() calls to Python 3 --- factory/alchemy.py | 2 +- factory/declarations.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index beccdccf..2d248872 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -48,7 +48,7 @@ def _generate(cls, strategy, params): # Original params are used in _get_or_create if it cannot build an # object initially due to an IntegrityError being raised cls._original_params = params - return super(SQLAlchemyModelFactory, cls)._generate(strategy, params) + return super()._generate(strategy, params) @classmethod def _get_or_create(cls, model_class, session, *args, **kwargs): diff --git a/factory/declarations.py b/factory/declarations.py index 9ea91bb9..e54f6dd8 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -717,9 +717,11 @@ def __init__(self, factory, factory_related_name='', size=2, **defaults): super().__init__(factory, factory_related_name, **defaults) def call(self, instance, step, context): - return [super(RelatedFactoryList, self).call(instance, step, context) - for i in range(self.size if isinstance(self.size, int) - else self.size())] + parent = super() + return [ + parent.call(instance, step, context) + for i in range(self.size if isinstance(self.size, int) else self.size()) + ] class NotProvided: From 1957bd3e2c711ff96f8301befdb1840592f43bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 28 Oct 2020 15:24:36 +0100 Subject: [PATCH 514/714] Add support for Python 3.9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The readme was updated not to mention the Python version explicitly, it already links to the stable branches on the Python devguide for supported version. That’s reduces the number of elements to upgrade for next Python versions. Use the latest Python version for all auxiliary builds (docs, example test, etc). That allows using the latest features in these builds. --- .travis.yml | 9 +++++---- README.rst | 2 +- docs/changelog.rst | 1 + setup.cfg | 1 + tox.ini | 10 +++++----- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1ecdd12a..0908bebc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,21 +12,22 @@ matrix: - python: "3.6" - python: "3.7" - python: "3.8" + - python: "3.9" - python: "pypy3" # Documentation - - python: "3.8" + - python: "3.9" env: TOXENV=docs before_install: - sudo apt-get -y install libenchant1c2a - - python: "3.8" + - python: "3.9" env: TOXENV=linkcheck # Linting - - python: "3.8" + - python: "3.9" env: TOXENV=examples - - python: "3.8" + - python: "3.9" env: TOXENV=lint services: diff --git a/README.rst b/README.rst index 518f75bb..bb4c9953 100644 --- a/README.rst +++ b/README.rst @@ -318,7 +318,7 @@ The associated object's strategy will be used: Support Policy -------------- -``factory_boy`` supports Python 3.6 to 3.8 as well as PyPy3. +``factory_boy`` supports active Python versions as well as PyPy3. - **Python**'s `supported versions `__. diff --git a/docs/changelog.rst b/docs/changelog.rst index 4ebfc0c8..a8588d4c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,7 @@ ChangeLog *New:* - Add support for Django 3.1 + - Add support for Python 3.9 *Removed:* diff --git a/setup.cfg b/setup.cfg index 6ccfdcd6..1005f55b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,6 +25,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing diff --git a/tox.ini b/tox.ini index a18e143a..afc513b2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,9 +2,9 @@ minversion = 1.9 envlist = lint - py{36,37,38}-django22-alchemy-mongoengine, - py{36,37,38}-django30-alchemy-mongoengine, - py{36,37,38}-django31-alchemy-mongoengine, + py{36,37,38,39}-django22-alchemy-mongoengine, + py{36,37,38,39}-django30-alchemy-mongoengine, + py{36,37,38,39}-django31-alchemy-mongoengine, pypy3-django{22,30,31}-alchemy-mongoengine, docs examples @@ -25,14 +25,14 @@ whitelist_externals = make commands = make test [testenv:docs] -basepython = python3.8 +basepython = python3.9 extras = doc whitelist_externals = make commands = make doc spelling [testenv:examples] -basepython = python3.8 +basepython = python3.9 deps = -rexamples/requirements.txt From 7d6514de2c110679c88abdd05d8710faf26dfa22 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Mon, 2 Nov 2020 18:03:52 -0800 Subject: [PATCH 515/714] Use django.test.utils to initialize test database Removes the need to import and instantiate DiscoverRunner. This also allows passing verbosity=0 when creating the database to squelch some noise during tests: Creating test database for alias 'default'... Creating test database for alias 'replica'... Available since Django 1.11. --- tests/test_django.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 694b79ab..ad68610d 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -12,7 +12,6 @@ from django.conf import settings from django.db.models import signals from django.test import utils as django_test_utils -from django.test.runner import DiscoverRunner as DjangoTestSuiteRunner import factory.django @@ -34,18 +33,12 @@ def setUpModule(): django_test_utils.setup_test_environment() - runner = DjangoTestSuiteRunner() - runner_state = runner.setup_databases() - test_state.update({ - 'runner': runner, - 'runner_state': runner_state, - }) + runner_state = django_test_utils.setup_databases(verbosity=0, interactive=False) + test_state['runner_state'] = runner_state def tearDownModule(): - runner = test_state['runner'] - runner_state = test_state['runner_state'] - runner.teardown_databases(runner_state) + django_test_utils.teardown_databases(test_state['runner_state'], verbosity=0) django_test_utils.teardown_test_environment() From 80f66e984ccd202ab963f83bc7782799e3e49eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 2 Nov 2020 22:27:21 +0100 Subject: [PATCH 516/714] Run tests in Python development mode Can reveal additional issues in the library, and helps understanding failures by providing more context. https://docs.python.org/3/library/devmode.html#devmode --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 7be6b92c..b6faeba4 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,7 @@ testall: test: python \ -b \ + -X dev \ -Werror \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ From 5f8330f4a611ca17e100068c95ab8d35173ca14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 2 Nov 2020 11:45:04 +0100 Subject: [PATCH 517/714] Define test factories strategy in FactoryOptions Instead of assigning to Factory._meta.strategy. Keeps overrides focused on a single class and remove need to save and restore global default strategy. --- tests/test_base.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 4759bd1e..0b9ffa15 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -362,18 +362,11 @@ class SubTestObjectFactory(self.TestObjectFactory): class FactoryDefaultStrategyTestCase(unittest.TestCase): - def setUp(self): - self.default_strategy = base.Factory._meta.strategy - - def tearDown(self): - base.Factory._meta.strategy = self.default_strategy - def test_build_strategy(self): - base.Factory._meta.strategy = enums.BUILD_STRATEGY - class TestModelFactory(base.Factory): class Meta: model = TestModel + strategy = enums.BUILD_STRATEGY one = 'one' @@ -395,11 +388,10 @@ class Meta: self.assertTrue(test_model.id) def test_stub_strategy(self): - base.Factory._meta.strategy = enums.STUB_STRATEGY - class TestModelFactory(base.Factory): class Meta: model = TestModel + strategy = enums.STUB_STRATEGY one = 'one' @@ -408,11 +400,10 @@ class Meta: self.assertFalse(hasattr(test_model, 'id')) # We should have a plain old object def test_unknown_strategy(self): - base.Factory._meta.strategy = 'unknown' - class TestModelFactory(base.Factory): class Meta: model = TestModel + strategy = 'unknown' one = 'one' @@ -423,11 +414,10 @@ def test_stub_with_create_strategy(self): class TestModelFactory(base.StubFactory): class Meta: model = TestModel + strategy = enums.CREATE_STRATEGY one = 'one' - TestModelFactory._meta.strategy = enums.CREATE_STRATEGY - with self.assertRaises(base.StubFactory.UnsupportedStrategy): TestModelFactory() @@ -435,20 +425,20 @@ def test_stub_with_build_strategy(self): class TestModelFactory(base.StubFactory): class Meta: model = TestModel + strategy = enums.BUILD_STRATEGY one = 'one' - TestModelFactory._meta.strategy = enums.BUILD_STRATEGY obj = TestModelFactory() # For stubs, build() is an alias of stub(). self.assertFalse(isinstance(obj, TestModel)) def test_change_strategy(self): - @base.use_strategy(enums.CREATE_STRATEGY) class TestModelFactory(base.StubFactory): class Meta: model = TestModel + strategy = enums.CREATE_STRATEGY one = 'one' From 941750dcae66ae9f535ff81a6feaab2572680a22 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 15 Nov 2020 07:52:45 -0800 Subject: [PATCH 518/714] Modify PyPI URLs to avoid redirects The URL https://pypi.org/project/factory_boy/ always redirects to https://pypi.org/project/factory-boy/. $ curl -v -L --include -o /dev/null https://pypi.org/project/factory_boy/ (abridged output) > GET /project/factory_boy/ HTTP/2 < HTTP/2 301 < location: https://pypi.org/project/factory-boy/ > GET /project/factory-boy/ HTTP/2 < HTTP/2 200 For whatever reason, the project is registered as factory-boy on PyPI and changing that now would break the permanent redirects. $ curl -L https://pypi.org/pypi/factory_boy/json/ | jq .info.name "factory-boy" --- README.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index bb4c9953..95742775 100644 --- a/README.rst +++ b/README.rst @@ -9,15 +9,15 @@ factory_boy :alt: Latest Version .. image:: https://img.shields.io/pypi/pyversions/factory_boy.svg - :target: https://pypi.org/project/factory_boy/ + :target: https://pypi.org/project/factory-boy/ :alt: Supported Python versions .. image:: https://img.shields.io/pypi/wheel/factory_boy.svg - :target: https://pypi.org/project/factory_boy/ + :target: https://pypi.org/project/factory-boy/ :alt: Wheel status .. image:: https://img.shields.io/pypi/l/factory_boy.svg - :target: https://pypi.org/project/factory_boy/ + :target: https://pypi.org/project/factory-boy/ :alt: License factory_boy is a fixtures replacement based on thoughtbot's `factory_bot `_. @@ -77,14 +77,14 @@ Links * Documentation: https://factoryboy.readthedocs.io/ * Repository: https://github.com/FactoryBoy/factory_boy -* Package: https://pypi.org/project/factory_boy/ +* Package: https://pypi.org/project/factory-boy/ * Mailing-list: `factoryboy@googlegroups.com `_ | https://groups.google.com/forum/#!forum/factoryboy Download -------- -PyPI: https://pypi.org/project/factory_boy/ +PyPI: https://pypi.org/project/factory-boy/ .. code-block:: sh From a0967bb52284e4024d9cab1afed1db352c0e47a1 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 14 Nov 2020 06:03:30 -0800 Subject: [PATCH 519/714] Remove u prefix from strings in docs Removes a Python-2-ism from docs. Python 2 has not been supported since ea37bfeabe24113cc7ddb68b2886395f66541037 (release 3.0.0). --- docs/examples.rst | 6 +++--- docs/orms.rst | 2 +- docs/recipes.rst | 4 ++-- docs/reference.rst | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 09728a41..6a113dca 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -72,8 +72,8 @@ And now, we'll define the related factories: account = factory.SubFactory(AccountFactory) gender = factory.Iterator([objects.Profile.GENDER_MALE, objects.Profile.GENDER_FEMALE]) - firstname = u'John' - lastname = u'Doe' + firstname = 'John' + lastname = 'Doe' @@ -86,7 +86,7 @@ If we commonly use a specific variant of our objects, we can refine a factory ac class FemaleProfileFactory(ProfileFactory): gender = objects.Profile.GENDER_FEMALE - firstname = u'Jane' + firstname = 'Jane' account__username = factory.Sequence(lambda n: 'jane%s' % n) diff --git a/docs/orms.rst b/docs/orms.rst index 7d95aaf5..ad776671 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -432,7 +432,7 @@ A (very) simple example: sqlalchemy_session = session # the SQLAlchemy session object id = factory.Sequence(lambda n: n) - name = factory.Sequence(lambda n: u'User %d' % n) + name = factory.Sequence(lambda n: 'User %d' % n) .. code-block:: pycon diff --git a/docs/recipes.rst b/docs/recipes.rst index f07b81b2..a07d9ef5 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -132,9 +132,9 @@ Since version 2.9, the :meth:`~factory.django.mute_signals` decorator should be .. code-block:: pycon - >>> u = UserFactory(profile__title=u"Lord") + >>> u = UserFactory(profile__title="Lord") >>> u.get_profile().title - u"Lord" + "Lord" Such behavior can be extended to other situations where a signal interferes with factory_boy related factories. diff --git a/docs/reference.rst b/docs/reference.rst index 12d7baac..0c938693 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -883,7 +883,7 @@ return value of the method: class Meta: model = User - name = u"Jean" + name = "Jean" @factory.lazy_attribute def email(self): @@ -891,13 +891,13 @@ return value of the method: clean_name = (unicodedata.normalize('NFKD', self.name) .encode('ascii', 'ignore') .decode('utf8')) - return u'%s@example.com' % clean_name + return '%s@example.com' % clean_name .. code-block:: pycon - >>> joel = UserFactory(name=u"Joël") + >>> joel = UserFactory(name="Joël") >>> joel.email - u'joel@example.com' + 'joel@example.com' Sequence From 73ee5cccd84f6c30edc46580c9a563a62bef6c6f Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 31 Oct 2020 07:12:23 -0700 Subject: [PATCH 520/714] Use f-strings for simple string formatting Python f-strings were added in Python 3.6. factory_boy dropped support for Python 3.5 in 64f10a18525572a5158b753d8f20195553fa5c5f. Only simple expressions were changed. Normally f-strings are more readable but with complex expressions they can sometimes get unwieldy. --- examples/flask_alchemy/demoapp.py | 2 +- factory/base.py | 2 +- factory/builder.py | 4 ++-- factory/declarations.py | 8 ++++---- factory/django.py | 2 +- tests/test_using.py | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/flask_alchemy/demoapp.py b/examples/flask_alchemy/demoapp.py index db57167d..f8e762ff 100644 --- a/examples/flask_alchemy/demoapp.py +++ b/examples/flask_alchemy/demoapp.py @@ -33,4 +33,4 @@ def __init__(self, message, user): self.user = user def __repr__(self): - return '' % (self.user, self.message) + return f'' diff --git a/factory/base.py b/factory/base.py index c338c9aa..c1d5e112 100644 --- a/factory/base.py +++ b/factory/base.py @@ -90,7 +90,7 @@ def __str__(cls): if cls._meta.abstract: return '<%s (abstract)>' % cls.__name__ else: - return '<%s for %s>' % (cls.__name__, cls._meta.model) + return f'<{cls.__name__} for {cls._meta.model}>' class BaseMeta: diff --git a/factory/builder.py b/factory/builder.py index 09153f76..218962e7 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -234,7 +234,7 @@ def recurse(self, factory, declarations, force_sequence=None): return builder.build(parent_step=self, force_sequence=force_sequence) def __repr__(self): - return "".format(self.builder) + return f"" class StepBuilder: @@ -314,7 +314,7 @@ def recurse(self, factory_meta, extras): return self.__class__(factory_meta, extras, strategy=self.strategy) def __repr__(self): - return "" % (self.factory_meta, self.strategy) + return f"" class Resolver: diff --git a/factory/declarations.py b/factory/declarations.py index e54f6dd8..9e72cf8d 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -393,9 +393,9 @@ def get(self): def __repr__(self): if self.factory is None: - return '<_FactoryImport: %s.%s>' % (self.module, self.name) + return f'<_FactoryImport: {self.module}.{self.name}>' else: - return '<_FactoryImport: %s>' % self.factory.__class__ + return f'<_FactoryImport: {self.factory.__class__}>' class SubFactory(ParameteredAttribute): @@ -490,7 +490,7 @@ def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): used_phases = {phase for phase in phases.values() if phase is not None} if len(used_phases) > 1: - raise TypeError("Inconsistent phases for %r: %r" % (self, phases)) + raise TypeError(f"Inconsistent phases for {self!r}: {phases!r}") self.FACTORY_BUILDER_PHASE = used_phases.pop() if used_phases else enums.BuilderPhase.ATTRIBUTE_RESOLUTION @@ -530,7 +530,7 @@ def evaluate(self, instance, step, extra): return target def __repr__(self): - return 'Maybe(%r, yes=%r, no=%r)' % (self.decider, self.yes, self.no) + return f'Maybe({self.decider!r}, yes={self.yes!r}, no={self.no!r})' class Parameter(utils.OrderedBase): diff --git a/factory/django.py b/factory/django.py index 5405be71..bcbc4b81 100644 --- a/factory/django.py +++ b/factory/django.py @@ -96,7 +96,7 @@ def _load_model_class(cls, definition): def _get_manager(cls, model_class): if model_class is None: raise errors.AssociatedClassError( - "No model set on %s.%s.Meta" % (cls.__module__, cls.__name__)) + f"No model set on {cls.__module__}.{cls.__name__}.Meta") try: manager = model_class.objects diff --git a/tests/test_using.py b/tests/test_using.py index fe4a3ed5..300f6669 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -1507,7 +1507,7 @@ class Meta: two = factory.SubFactory( TestModelFactory, one=factory.Sequence(lambda n: 'x%dx' % n), - two=factory.LazyAttribute(lambda o: '%s%s' % (o.one, o.one)), + two=factory.LazyAttribute(lambda o: f'{o.one}{o.one}'), ) test_model = TestModel2Factory(one=42) From 8f7faa56a1efa9617ea4f97b38cc22aff4a257d6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 1 Nov 2020 15:04:22 -0800 Subject: [PATCH 521/714] Combine similar environments lines in tox.ini --- tox.ini | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index afc513b2..7097f0ca 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,9 @@ minversion = 1.9 envlist = lint - py{36,37,38,39}-django22-alchemy-mongoengine, - py{36,37,38,39}-django30-alchemy-mongoengine, - py{36,37,38,39}-django31-alchemy-mongoengine, - pypy3-django{22,30,31}-alchemy-mongoengine, + py{36,37,38,39,py3}-django22-alchemy-mongoengine + py{36,37,38,39,py3}-django30-alchemy-mongoengine + py{36,37,38,39,py3}-django31-alchemy-mongoengine docs examples linkcheck From 9d9788edbe0b81c0019898ccdc3ab721da42389f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 14 Nov 2020 17:19:20 +0100 Subject: [PATCH 522/714] Deprecate factory.use_strategy() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The purpose of this function is unclear. It is not used in the library and not really tested. Follow the Zen of Python, keep only one way of specifying a factory default strategy: through its options. Users can define the strategy when calling the factory with the dedicated methods (e.g. `MyFactory.build()`, or `MyFactory.create()`). Reviewed-by: Raphaël Barrois --- docs/changelog.rst | 18 ++++++++++++++++++ docs/reference.rst | 4 ++++ factory/base.py | 6 ++++++ tests/test_using.py | 3 +-- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index a8588d4c..e7863020 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,24 @@ ChangeLog ========= +4.0.0 (unreleased) +------------------ + +*Removed:* + + - :func:`factory.use_strategy()` + +3.2.0 (unreleased) +------------------ + +*Deprecated:* + + - :func:`factory.use_strategy`. Use :attr:`factory.FactoryOptions.strategy` instead. + The purpose of :func:`~factory.use_strategy` duplicates the factory option. Follow :pep:`20`: *There should be + one-- and preferably only one --obvious way to do it.* + + :func:`~factory.use_strategy()` will be removed in the next major version. + 3.1.1 (unreleased) ------------------ diff --git a/docs/reference.rst b/docs/reference.rst index 0c938693..83bc2c19 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -590,6 +590,10 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. function:: use_strategy(strategy) + .. deprecated:: 3.2 + + Use :py:attr:`factory.FactoryOptions.strategy` instead. + *Decorator* Change the default strategy of the decorated :class:`Factory` to the chosen :obj:`strategy`: diff --git a/factory/base.py b/factory/base.py index c1d5e112..a5e58427 100644 --- a/factory/base.py +++ b/factory/base.py @@ -3,6 +3,7 @@ import collections import logging +import warnings from . import builder, declarations, enums, errors, utils @@ -716,6 +717,11 @@ def use_strategy(new_strategy): This is an alternative to setting default_strategy in the class definition. """ + warnings.warn( + "use_strategy() is deprecated and will be removed in the future.", + DeprecationWarning, + ) + def wrapped_class(klass): klass._meta.strategy = new_strategy return klass diff --git a/tests/test_using.py b/tests/test_using.py index 300f6669..07dfbb47 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -349,14 +349,13 @@ class Meta: self.assertEqual(test_object.one, 'one') def test_inheriting_model_class(self): - @factory.use_strategy(factory.BUILD_STRATEGY) class TestObjectFactory(factory.Factory, TestObject): class Meta: model = TestObject one = 'one' - test_object = TestObjectFactory() + test_object = TestObjectFactory.build() self.assertEqual(test_object.one, 'one') def test_abstract(self): From f3dce5403116e94c721446d3a0926b72992f1801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 15 Jun 2019 21:50:21 +0200 Subject: [PATCH 523/714] Change PostGenerationMethodCall example User passwords do not depend on the generated object. The previous example was tied to Django behavior, while `factory_boy` is a general purpose library. Besides, this example gets in the way of removing the duplicate call to `.save()` after `postgeneration` hooks. Refs #366 Reviewed-by: Jon Dufresne --- docs/reference.rst | 72 ++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 53 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 83bc2c19..6f6776b4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1900,71 +1900,37 @@ Once the factory instance has been generated, the method specified in with any arguments specified in the :class:`PostGenerationMethodCall` declaration, by default. -For example, to set a default password on a generated User instance -during instantiation, we could make a declaration for a ``password`` -attribute like below: +For example, we could use ``PostGenerationMethodCall`` to register created +users in an external system. .. code-block:: python - class UserFactory(factory.Factory): - class Meta: - model = User + class User(models.Model): + name = models.CharField(max_length=191) - username = 'user' - password = factory.PostGenerationMethodCall('set_password', - 'defaultpassword') + def register(self, system, auth_token="ABC"): + self.registration_id = system.register(auth_token) -When we instantiate a user from the ``UserFactory``, the factory -will create a password attribute by calling ``User.set_password('defaultpassword')``. -Thus, by default, our users will have a password set to ``'defaultpassword'``. -.. code-block:: pycon + class UserFactory(factory.DjangoModelFactory): + class Meta: + model = User - >>> u = UserFactory() # Calls user.set_password('defaultpassword') - >>> u.check_password('defaultpassword') - True + name = 'user' + register = factory.PostGenerationMethodCall("register", DefaultRegistry()) If the :class:`PostGenerationMethodCall` declaration contained no arguments or one argument, an overriding value can be passed directly to the method through a keyword argument matching the attribute name. -For example we can override the default password specified in the declaration -above by simply passing in the desired password as a keyword argument to the -factory during instantiation. .. code-block:: pycon - >>> other_u = UserFactory(password='different') # Calls user.set_password('different') - >>> other_u.check_password('defaultpassword') - False - >>> other_u.check_password('different') - True - -.. note:: - - For Django models, unless the object method called by - :class:`PostGenerationMethodCall` saves the object back to the - database, we will have to explicitly remember to save the object back - if we performed a ``create()``. - - .. code-block:: pycon - - >>> u = UserFactory.create() # u.password has not been saved back to the database - >>> u.save() # we must remember to do it ourselves - - - We can avoid this by subclassing from :class:`DjangoModelFactory`, - instead, e.g., - - .. code-block:: python - - class UserFactory(factory.django.DjangoModelFactory): - class Meta: - model = User - - username = 'user' - password = factory.PostGenerationMethodCall('set_password', - 'defaultpassword') - + >>> # DefaultRegistry uses UUID for identifiers. + >>> UserFactory().registration_id + 'edf42c11-0065-43ad-ad3d-78ab7497aaae' + >>> # OtherRegistry uses int for identifiers. + >>> UserFactory(register=OtherRegistry()).registration_id + 123456 .. warning:: In order to keep a consistent and simple API, a :class:`PostGenerationMethodCall` allows *at most one* positional argument; all other parameters should be passed as @@ -1975,8 +1941,8 @@ defaults present in the :class:`PostGenerationMethodCall` declaration. .. code-block:: pycon - >>> UserFactory(password__disabled=True) # Calls user.set_password('', 'sha1', disabled=True) - + >>> # Calls user.register(DefaultRegistry(), auth_token="DEF") + >>> UserFactory(register__auth_token="DEF") Module-level functions ---------------------- From f3775af70890f77fc24facd6c7c3f8026aaf056a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 14 Nov 2020 17:45:25 +0100 Subject: [PATCH 524/714] Remove FakePostGenerationDeclaration Unused since its introduction in 6f202077a5c8156fe96f8a028f883c14962f5b95. Reviewed-by: Jon Dufresne --- factory/builder.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index 218962e7..bfe44676 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -2,7 +2,7 @@ import collections -from . import declarations, enums, errors, utils +from . import enums, errors, utils DeclarationWithContext = collections.namedtuple( 'DeclarationWithContext', @@ -140,18 +140,6 @@ def __repr__(self): return '' % self.as_dict() -class FakePostGenerationDeclaration(declarations.PostGenerationDeclaration): - """A fake post-generation declaration, providing simply a hardcoded value. - - Used to disable post-generation when the user has overridden a method. - """ - def __init__(self, value): - self.value = value - - def call(self, instance, step, context): - return self.value - - def parse_declarations(decls, base_pre=None, base_post=None): pre_declarations = base_pre.copy() if base_pre else DeclarationSet() post_declarations = base_post.copy() if base_post else DeclarationSet() From bacfd73de3bde2b4e680a7e28b2e3140b3a61d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 22 Nov 2020 14:59:05 +0100 Subject: [PATCH 525/714] Simplify handling of the locale argument to Faker Reviewed-by: Jon Dufresne --- factory/faker.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/factory/faker.py b/factory/faker.py index 1c9e28aa..dfd0bdaa 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -36,14 +36,11 @@ class Faker(declarations.ParameteredDeclaration): >>> foo = factory.Faker('name') """ def __init__(self, provider, **kwargs): - locale = kwargs.pop('locale', None) self.provider = provider - super().__init__( - locale=locale, - **kwargs) + super().__init__(**kwargs) def generate(self, params): - locale = params.pop('locale') + locale = params.pop('locale', None) subfaker = self._get_faker(locale) return subfaker.format(self.provider, **params) From d8b241c94c326ce6c241cf8b6c7c8745599e4583 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 1 Nov 2020 14:00:35 -0800 Subject: [PATCH 526/714] Migrate CI from Travis to GitHub actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub actions produces faster results than Travis, resulting in faster feedback for contributors. The linkcheck environment was not ported to GitHub actions. This environment consistently fails with the error: 429 Client Error: too many requests for url: https://github.com/… This sometimes happens in Travis, but is more frequent now with the increase in performance. See issue #771. The "basepython" tox configuration option was dropped as GitHub actions default to using the latest Python version which is now 3.9. This avoid additional configuration in both actions and tox when a new Python is released. Fixes #707 --- .github/workflows/check.yml | 33 ++++++++++++++++++++++++++ .github/workflows/test.yml | 47 +++++++++++++++++++++++++++++++++++++ .travis.yml | 38 ------------------------------ README.rst | 7 ++++-- tox.ini | 2 -- 5 files changed, 85 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 00000000..1575242e --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,33 @@ +name: Check + +on: + - push + - pull_request + +jobs: + build: + name: ${{ matrix.tox-environment }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + tox-environment: + - docs + - examples + - lint + + env: + TOXENV: ${{ matrix.tox-environment }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + + - name: Install dependencies + run: python -m pip install tox + + - name: Run + run: tox diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..5da662f6 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,47 @@ +name: Test + +on: + - push + - pull_request + +jobs: + build: + name: Python ${{ matrix.python-version }} / ${{ matrix.tox-environment }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + - pypy3 + tox-environment: + - django22-alchemy-mongoengine + - django30-alchemy-mongoengine + - django31-alchemy-mongoengine + + services: + mongodb: + image: mongo + ports: + - 27017:27017 + + env: + TOXENV: ${{ matrix.tox-environment }} + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: python -m pip install tox + + - name: Run tests + run: tox diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0908bebc..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: python -cache: pip - -script: - - tox - -install: - - pip install tox tox-travis - -matrix: - include: - - python: "3.6" - - python: "3.7" - - python: "3.8" - - python: "3.9" - - - python: "pypy3" - - # Documentation - - python: "3.9" - env: TOXENV=docs - before_install: - - sudo apt-get -y install libenchant1c2a - - python: "3.9" - env: TOXENV=linkcheck - - # Linting - - python: "3.9" - env: TOXENV=examples - - python: "3.9" - env: TOXENV=lint - -services: - - mongodb - -notifications: - email: false - irc: "irc.freenode.org#factory_boy" diff --git a/README.rst b/README.rst index 95742775..197791cb 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,11 @@ factory_boy =========== -.. image:: https://secure.travis-ci.org/FactoryBoy/factory_boy.svg?branch=master - :target: https://travis-ci.org/FactoryBoy/factory_boy/ +.. image:: https://github.com/FactoryBoy/factory_boy/workflows/Test/badge.svg + :target: https://github.com/FactoryBoy/factory_boy/actions?query=workflow%3ATest + +.. image:: https://github.com/FactoryBoy/factory_boy/workflows/Check/badge.svg + :target: https://github.com/FactoryBoy/factory_boy/actions?query=workflow%3ACheck .. image:: https://img.shields.io/pypi/v/factory_boy.svg :target: https://factoryboy.readthedocs.io/en/latest/changelog.html diff --git a/tox.ini b/tox.ini index 7097f0ca..ce49f581 100644 --- a/tox.ini +++ b/tox.ini @@ -24,14 +24,12 @@ whitelist_externals = make commands = make test [testenv:docs] -basepython = python3.9 extras = doc whitelist_externals = make commands = make doc spelling [testenv:examples] -basepython = python3.9 deps = -rexamples/requirements.txt From 88b81ead8e56a8c3eef268c299616fa3a958f091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 6 Dec 2020 19:12:52 +0100 Subject: [PATCH 527/714] Run tests against the next Django version Discover issues with the next release early. Only use the latest version of Python, Django takes care of their supported Python versions. Reviewed-by: Jon Dufresne --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ce49f581..f8774aaa 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py{36,37,38,39,py3}-django22-alchemy-mongoengine py{36,37,38,39,py3}-django30-alchemy-mongoengine py{36,37,38,39,py3}-django31-alchemy-mongoengine + py39-djangomaster-alchemy-mongoengine docs examples linkcheck @@ -16,7 +17,8 @@ deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 - django{22,30,31}: Pillow + djangomaster: https://github.com/django/django/archive/master.tar.gz + django{22,30,31,master}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 5bbe27e85dad45ef3f30aef283ed0eee99af9aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 15:51:50 +0100 Subject: [PATCH 528/714] Revert "Simplify handling of the locale argument to Faker" This reverts commit bacfd73de3bde2b4e680a7e28b2e3140b3a61d3a. The commit only fixed a surface issue, but didn't address the underlying issue: a call to factory.Maybe doesn't properly call the unroll_context of the declarations it's wrapping. --- factory/faker.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/factory/faker.py b/factory/faker.py index dfd0bdaa..1c9e28aa 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -36,11 +36,14 @@ class Faker(declarations.ParameteredDeclaration): >>> foo = factory.Faker('name') """ def __init__(self, provider, **kwargs): + locale = kwargs.pop('locale', None) self.provider = provider - super().__init__(**kwargs) + super().__init__( + locale=locale, + **kwargs) def generate(self, params): - locale = params.pop('locale', None) + locale = params.pop('locale') subfaker = self._get_faker(locale) return subfaker.format(self.provider, **params) From 15e11e76d7f3dcf961ed7d5e53b46437c1407180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 17:48:29 +0100 Subject: [PATCH 529/714] Cleanup ParameteredAttribute Remove remnants from the BuildStep / Builder migration (commit 6f202077a5c8156fe96f8a028f883c14962f5b95). --- factory/declarations.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 9e72cf8d..c7f7f324 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -272,28 +272,12 @@ class ParameteredAttribute(BaseDeclaration): Attributes: defaults (dict): Default values for the parameters. May be overridden by call-time parameters. - - Class attributes: - CONTAINERS_FIELD (str): name of the field, if any, where container - information (e.g for SubFactory) should be stored. If empty, - containers data isn't merged into generate() parameters. """ - CONTAINERS_FIELD = '__containers' - - # Whether to add the current object to the stack of containers - EXTEND_CONTAINERS = False - def __init__(self, **kwargs): super().__init__() self.defaults = kwargs - def _prepare_containers(self, obj, containers=()): - if self.EXTEND_CONTAINERS: - return (obj,) + tuple(containers) - - return containers - def evaluate(self, instance, step, extra): """Evaluate the current definition and fill its attributes. @@ -407,7 +391,6 @@ class SubFactory(ParameteredAttribute): factory (base.Factory): the wrapped factory """ - EXTEND_CONTAINERS = True # Whether to align the attribute's sequence counter to the holding # factory's sequence counter FORCE_SEQUENCE = False From e19142cb6e049e079cd4af36775715fcda47cb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 17:54:03 +0100 Subject: [PATCH 530/714] Perform context unrolling inside declarations Unrolling the extra context for a declaration might depend on the declaration's internals; that's typically the case with factory.Maybe: the inner declarations might depend on the actual declaration used. This adds `evaluate_pre` and a `evaluate_post` entrypoints to declarations, more readable with regard to which build phase they are used in. Each of those will perform unrolling before calling the semi-public actual function entrypoint (self.evaluate() for evaluate_pre, self.call() for evaluate_post). As a side effect, this fixes the issues with factory.Faker() when called inside a factory.Maybe(). Closes #785 #786 #787 #788 #790 #796. --- factory/builder.py | 31 ++++--------------------------- factory/declarations.py | 39 +++++++++++++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index bfe44676..eaf7393b 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -10,12 +10,6 @@ ) -PostGenerationContext = collections.namedtuple( - 'PostGenerationContext', - ['value_provided', 'value', 'extra'], -) - - class DeclarationSet: """A set of declarations, including the recursive parameters. @@ -274,21 +268,10 @@ def build(self, parent_step=None, force_sequence=None): postgen_results = {} for declaration_name in post.sorted(): declaration = post[declaration_name] - unrolled_context = declaration.declaration.unroll_context( - instance=instance, - step=step, - context=declaration.context, - ) - - postgen_context = PostGenerationContext( - value_provided='' in unrolled_context, - value=unrolled_context.get(''), - extra={k: v for k, v in unrolled_context.items() if k != ''}, - ) - postgen_results[declaration_name] = declaration.declaration.call( + postgen_results[declaration_name] = declaration.declaration.evaluate_post( instance=instance, step=step, - context=postgen_context, + overrides=declaration.context, ) self.factory_meta.use_postgeneration_results( instance=instance, @@ -358,16 +341,10 @@ def __getattr__(self, name): if enums.get_builder_phase(value) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: self.__pending.append(name) try: - context = value.unroll_context( - instance=self, - step=self.__step, - context=declaration.context, - ) - - value = value.evaluate( + value = value.evaluate_pre( instance=self, step=self.__step, - extra=context, + overrides=declaration.context, ) finally: last = self.__pending.pop() diff --git a/factory/declarations.py b/factory/declarations.py index c7f7f324..1f257505 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -3,6 +3,7 @@ import itertools import logging +import typing as T from . import enums, errors, utils @@ -34,6 +35,10 @@ def unroll_context(self, instance, step, context): subfactory = factory.base.DictFactory return step.recurse(subfactory, context, force_sequence=step.sequence) + def evaluate_pre(self, instance, step, overrides): + context = self.unroll_context(instance, step, overrides) + return self.evaluate(instance, step, context) + def evaluate(self, instance, step, extra): """Evaluate this declaration. @@ -477,36 +482,39 @@ def __init__(self, decider, yes_declaration=SKIP, no_declaration=SKIP): self.FACTORY_BUILDER_PHASE = used_phases.pop() if used_phases else enums.BuilderPhase.ATTRIBUTE_RESOLUTION - def call(self, instance, step, context): + def evaluate_post(self, instance, step, overrides): + """Handle post-generation declarations""" decider_phase = enums.get_builder_phase(self.decider) if decider_phase == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: # Note: we work on the *builder stub*, not on the actual instance. # This gives us access to all Params-level definitions. - choice = self.decider.evaluate(instance=step.stub, step=step, extra=context.extra) + choice = self.decider.evaluate_pre( + instance=step.stub, step=step, overrides=overrides) else: assert decider_phase == enums.BuilderPhase.POST_INSTANTIATION - choice = self.decider.call(instance, step, context) + choice = self.decider.evaluate_post( + instance=instance, step=step, overrides={}) target = self.yes if choice else self.no if enums.get_builder_phase(target) == enums.BuilderPhase.POST_INSTANTIATION: - return target.call( + return target.evaluate_post( instance=instance, step=step, - context=context, + overrides=overrides, ) else: # Flat value (can't be ATTRIBUTE_RESOLUTION, checked in __init__) return target - def evaluate(self, instance, step, extra): + def evaluate_pre(self, instance, step, overrides): choice = self.decider.evaluate(instance=instance, step=step, extra={}) target = self.yes if choice else self.no if isinstance(target, BaseDeclaration): - return target.evaluate( + return target.evaluate_pre( instance=instance, step=step, - extra=extra, + overrides=overrides, ) else: # Flat value (can't be POST_INSTANTIATION, checked in __init__) @@ -596,11 +604,26 @@ def __repr__(self): # =============== +class PostGenerationContext(T.NamedTuple): + value_provided: bool + value: T.Any + extra: T.Dict[str, T.Any] + + class PostGenerationDeclaration(BaseDeclaration): """Declarations to be called once the model object has been generated.""" FACTORY_BUILDER_PHASE = enums.BuilderPhase.POST_INSTANTIATION + def evaluate_post(self, instance, step, overrides): + context = self.unroll_context(instance, step, overrides) + postgen_context = PostGenerationContext( + value_provided=bool('' in context), + value=context.get(''), + extra={k: v for k, v in context.items() if k != ''}, + ) + return self.call(instance, step, postgen_context) + def call(self, instance, step, context): # pragma: no cover """Call this hook; no return value is expected. From 5692f64619bf009cf92bf0a8c6f77bf82f0e3d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 17:59:47 +0100 Subject: [PATCH 531/714] Add a new regression testing module That module should hold all tests used when reproducing (and fixing) an issue. --- tests/test_regression.py | 53 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_regression.py diff --git a/tests/test_regression.py b/tests/test_regression.py new file mode 100644 index 00000000..a9ea1c66 --- /dev/null +++ b/tests/test_regression.py @@ -0,0 +1,53 @@ +# Copyright: See the LICENSE file. + + +"""Regression tests related to issues found with the project""" + +import datetime +import typing as T +import unittest + +import factory + +# Example objects +# =============== + + +class Author(T.NamedTuple): + fullname: str + pseudonym: T.Optional[str] = None + + +class Book(T.NamedTuple): + title: str + author: Author + + +class PublishedBook(T.NamedTuple): + book: Book + published_on: datetime.date + countries: T.List[str] + + +class FakerRegressionTests(unittest.TestCase): + def test_locale_issue(self): + """Regression test for `KeyError: 'locale'` + + See #785 #786 #787 #788 #790 #796. + """ + class AuthorFactory(factory.Factory): + class Meta: + model = Author + + class Params: + unknown = factory.Trait( + fullname="", + ) + + fullname = factory.Faker("name") + + public_author = AuthorFactory(unknown=False) + self.assertIsNone(public_author.pseudonym) + + unknown_author = AuthorFactory(unknown=True) + self.assertEqual("", unknown_author.fullname) From 74918acd39e9200b04a1797f3e48dca61e01a96b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 18:06:21 +0100 Subject: [PATCH 532/714] Stop passing unnamed kwargs up in declarations The base class is an internal, non-public API; users shouldn't add custom classes deeper into the MRO. This change makes it easier to follow parameters through the various calls and classes. --- factory/declarations.py | 16 ++++++++-------- factory/fuzzy.py | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 1f257505..6b024bcc 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -66,8 +66,8 @@ class LazyFunction(BaseDeclaration): returning the computed value. """ - def __init__(self, function, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, function): + super().__init__() self.function = function def evaluate(self, instance, step, extra): @@ -83,8 +83,8 @@ class LazyAttribute(BaseDeclaration): returning the computed value. """ - def __init__(self, function, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, function): + super().__init__() self.function = function def evaluate(self, instance, step, extra): @@ -138,8 +138,8 @@ class SelfAttribute(BaseDeclaration): exist. """ - def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, attribute_name, default=_UNSPECIFIED): + super().__init__() depth = len(attribute_name) - len(attribute_name.lstrip('.')) attribute_name = attribute_name[depth:] @@ -246,8 +246,8 @@ class ContainerAttribute(BaseDeclaration): strict (bool): Whether evaluating should fail when the containers are not passed in (i.e used outside a SubFactory). """ - def __init__(self, function, strict=True, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, function, strict=True): + super().__init__() self.function = function self.strict = strict diff --git a/factory/fuzzy.py b/factory/fuzzy.py index 61f6eba8..ce89d7f7 100644 --- a/factory/fuzzy.py +++ b/factory/fuzzy.py @@ -39,8 +39,8 @@ class FuzzyAttribute(BaseFuzzyAttribute): random value. """ - def __init__(self, fuzzer, **kwargs): - super().__init__(**kwargs) + def __init__(self, fuzzer): + super().__init__() self.fuzzer = fuzzer def fuzz(self): @@ -64,8 +64,8 @@ class FuzzyText(BaseFuzzyAttribute): not important. """ - def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters, **kwargs): - super().__init__(**kwargs) + def __init__(self, prefix='', length=12, suffix='', chars=string.ascii_letters): + super().__init__() self.prefix = prefix self.suffix = suffix self.length = length @@ -85,11 +85,11 @@ class FuzzyChoice(BaseFuzzyAttribute): getter (callable or None): a function to parse returned values """ - def __init__(self, choices, getter=None, **kwargs): + def __init__(self, choices, getter=None): self.choices = None self.choices_generator = choices self.getter = getter - super().__init__(**kwargs) + super().__init__() def fuzz(self): if self.choices is None: @@ -103,7 +103,7 @@ def fuzz(self): class FuzzyInteger(BaseFuzzyAttribute): """Random integer within a given range.""" - def __init__(self, low, high=None, step=1, **kwargs): + def __init__(self, low, high=None, step=1): if high is None: high = low low = 0 @@ -112,7 +112,7 @@ def __init__(self, low, high=None, step=1, **kwargs): self.high = high self.step = step - super().__init__(**kwargs) + super().__init__() def fuzz(self): return random.randgen.randrange(self.low, self.high + 1, self.step) @@ -121,7 +121,7 @@ def fuzz(self): class FuzzyDecimal(BaseFuzzyAttribute): """Random decimal within a given range.""" - def __init__(self, low, high=None, precision=2, **kwargs): + def __init__(self, low, high=None, precision=2): if high is None: high = low low = 0.0 @@ -130,7 +130,7 @@ def __init__(self, low, high=None, precision=2, **kwargs): self.high = high self.precision = precision - super().__init__(**kwargs) + super().__init__() def fuzz(self): base = decimal.Decimal(str(random.randgen.uniform(self.low, self.high))) @@ -140,7 +140,7 @@ def fuzz(self): class FuzzyFloat(BaseFuzzyAttribute): """Random float within a given range.""" - def __init__(self, low, high=None, precision=15, **kwargs): + def __init__(self, low, high=None, precision=15): if high is None: high = low low = 0 @@ -149,7 +149,7 @@ def __init__(self, low, high=None, precision=15, **kwargs): self.high = high self.precision = precision - super().__init__(**kwargs) + super().__init__() def fuzz(self): base = random.randgen.uniform(self.low, self.high) @@ -159,8 +159,8 @@ def fuzz(self): class FuzzyDate(BaseFuzzyAttribute): """Random date within a given date range.""" - def __init__(self, start_date, end_date=None, **kwargs): - super().__init__(**kwargs) + def __init__(self, start_date, end_date=None): + super().__init__() if end_date is None: if random.randgen.state_set: cls_name = self.__class__.__name__ @@ -197,8 +197,8 @@ def _now(self): 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().__init__(**kwargs) + force_microsecond=None): + super().__init__() if end_dt is None: if random.randgen.state_set: From 372f3c26665f32216631cf214375091a47a9b6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 18:08:47 +0100 Subject: [PATCH 533/714] Handle default extra context for all declarations Instead of relying on a specific ParameteredDeclaration subclass, allow each BaseDeclaration to save arbitrary kwargs, and retrieve them when unrolling the context. This mostly makes the ParameteredDeclaration class obsolete. --- factory/declarations.py | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 6b024bcc..26837b83 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -24,16 +24,24 @@ class BaseDeclaration(utils.OrderedBase): #: Set to False on declarations that perform their own unrolling. UNROLL_CONTEXT_BEFORE_EVALUATION = True + def __init__(self, **defaults): + super().__init__() + self._defaults = defaults or {} + def unroll_context(self, instance, step, context): + full_context = dict() + full_context.update(self._defaults) + full_context.update(context) + if not self.UNROLL_CONTEXT_BEFORE_EVALUATION: - return context - if not any(enums.get_builder_phase(v) for v in context.values()): + return full_context + if not any(enums.get_builder_phase(v) for v in full_context.values()): # Optimization for simple contexts - don't do anything. - return context + return full_context import factory.base subfactory = factory.base.DictFactory - return step.recurse(subfactory, context, force_sequence=step.sequence) + return step.recurse(subfactory, full_context, force_sequence=step.sequence) def evaluate_pre(self, instance, step, overrides): context = self.unroll_context(instance, step, overrides) @@ -279,10 +287,6 @@ class ParameteredAttribute(BaseDeclaration): May be overridden by call-time parameters. """ - def __init__(self, **kwargs): - super().__init__() - self.defaults = kwargs - def evaluate(self, instance, step, extra): """Evaluate the current definition and fill its attributes. @@ -297,11 +301,7 @@ def evaluate(self, instance, step, extra): extra (dict): additional, call-time added kwargs for the step. """ - defaults = dict(self.defaults) - if extra: - defaults.update(extra) - - return self.generate(step, defaults) + return self.generate(step, extra) def generate(self, step, params): """Actually generate the related attribute. @@ -331,16 +331,6 @@ class ParameteredDeclaration(BaseDeclaration): by call-time parameters. Accepts BaseDeclaration subclasses. """ - def __init__(self, **defaults): - self.defaults = defaults - super().__init__() - - def unroll_context(self, instance, step, context): - merged_context = {} - merged_context.update(self.defaults) - merged_context.update(context) - return super().unroll_context(instance, step, merged_context) - def evaluate(self, instance, step, extra): return self.generate(extra) From 824c6e01f91dcb07d16f51578300da3c99b6a336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 18:10:07 +0100 Subject: [PATCH 534/714] Switch away from ParameteredDeclaration Now that BaseDeclaration handles arbitrary kwargs as a set of default parameters for the declaration (and exposes them through its `unroll_context` default), the ParameteredDeclaration is no longer required. --- factory/declarations.py | 8 ++++---- factory/django.py | 6 +++--- factory/faker.py | 8 ++++---- tests/test_faker.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 26837b83..e7791be9 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -377,7 +377,7 @@ def __repr__(self): return f'<_FactoryImport: {self.factory.__class__}>' -class SubFactory(ParameteredAttribute): +class SubFactory(BaseDeclaration): """Base class for attributes based upon a sub-factory. Attributes: @@ -399,7 +399,7 @@ def get_factory(self): """Retrieve the wrapped factory.Factory subclass.""" return self.factory_wrapper.get() - def generate(self, step, params): + def evaluate(self, instance, step, extra): """Evaluate the current definition and fill its attributes. Args: @@ -411,11 +411,11 @@ def generate(self, step, params): logger.debug( "SubFactory: Instantiating %s.%s(%s), create=%r", subfactory.__module__, subfactory.__name__, - utils.log_pprint(kwargs=params), + utils.log_pprint(kwargs=extra), step, ) force_sequence = step.sequence if self.FORCE_SEQUENCE else None - return step.recurse(subfactory, params, force_sequence=force_sequence) + return step.recurse(subfactory, extra, force_sequence=force_sequence) class Dict(SubFactory): diff --git a/factory/django.py b/factory/django.py index bcbc4b81..9e8954a3 100644 --- a/factory/django.py +++ b/factory/django.py @@ -173,7 +173,7 @@ def _after_postgeneration(cls, instance, create, results=None): instance.save() -class FileField(declarations.ParameteredDeclaration): +class FileField(declarations.BaseDeclaration): """Helper to fill in django.db.models.FileField from a Factory.""" DEFAULT_FILENAME = 'example.dat' @@ -219,9 +219,9 @@ def _make_content(self, params): filename = params.get('filename', default_filename) return filename, content - def generate(self, params): + def evaluate(self, instance, step, extra): """Fill in the field.""" - filename, content = self._make_content(params) + filename, content = self._make_content(extra) return django_files.File(content.file, filename) diff --git a/factory/faker.py b/factory/faker.py index 1c9e28aa..6ed2e28c 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -21,7 +21,7 @@ class Meta: from . import declarations -class Faker(declarations.ParameteredDeclaration): +class Faker(declarations.BaseDeclaration): """Wrapper for 'faker' values. Args: @@ -42,10 +42,10 @@ def __init__(self, provider, **kwargs): locale=locale, **kwargs) - def generate(self, params): - locale = params.pop('locale') + def evaluate(self, instance, step, extra): + locale = extra.pop('locale') subfaker = self._get_faker(locale) - return subfaker.format(self.provider, **params) + return subfaker.format(self.provider, **extra) _FAKER_REGISTRY = {} _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE diff --git a/tests/test_faker.py b/tests/test_faker.py index fb9b7218..00355b77 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -50,7 +50,7 @@ def _setup_advanced_mock_faker(self, locale=None, **handlers): def test_simple_biased(self): self._setup_mock_faker(name="John Doe") faker_field = factory.Faker('name') - self.assertEqual("John Doe", faker_field.generate({'locale': None})) + self.assertEqual("John Doe", faker_field.evaluate(None, None, {'locale': None})) def test_full_factory(self): class Profile: From 82d6144a4c590858107b707ebacc5863a107a548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 18:11:24 +0100 Subject: [PATCH 535/714] Remove ParameteredDeclaration It was part of the private API; its features have been merged into the BaseDeclaration class, hence that class is no longer required. --- factory/declarations.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index e7791be9..5e150ca1 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -320,29 +320,6 @@ def generate(self, step, params): raise NotImplementedError() -class ParameteredDeclaration(BaseDeclaration): - """A declaration with parameters. - - The parameters can be any factory-enabled declaration, and will be resolved - before the call to the user-defined code in `self.generate()`. - - Attributes: - defaults (dict): Default values for the parameters; can be overridden - by call-time parameters. Accepts BaseDeclaration subclasses. - """ - - def evaluate(self, instance, step, extra): - return self.generate(extra) - - def generate(self, params): - """Generate a value for this declaration. - - Args: - params (dict): the parameters, after a factory evaluation. - """ - raise NotImplementedError() - - class _FactoryWrapper: """Handle a 'factory' arg. From 0eb3b3bfc75b6fd7f20ea9814cf79631d8ea6923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 18:15:43 +0100 Subject: [PATCH 536/714] Add `make TAGS` to run ctags The TAGS name follows the GNU convention, as described in `info make`. The resulting file is ignored by git. --- .gitignore | 1 + Makefile | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/.gitignore b/.gitignore index bed0ddc2..f2ee4207 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ build/ dist/ htmlcov/ MANIFEST +tags diff --git a/Makefile b/Makefile index b6faeba4..9474bae5 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ SETUP_PY=setup.py COVERAGE = python $(shell which coverage) FLAKE8 = flake8 ISORT = isort +CTAGS = ctags all: default @@ -87,6 +88,16 @@ coverage: .PHONY: test testall example-test lint coverage +# Development +# =========== + +# DOC: Generate a "tags" file +TAGS: + $(CTAGS) --recurse $(PACKAGE) $(TESTS_DIR) + +.PHONY: TAGS + + # Documentation # ============= From 178f816c85f90800618703eb9b97c6b7e43dc823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 23 Dec 2020 18:24:54 +0100 Subject: [PATCH 537/714] Update ChangeLog - Dropping support for obsolete Python/Django versions should not be performed in a patch release --- docs/changelog.rst | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e7863020..3b6eff72 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,17 +11,6 @@ ChangeLog 3.2.0 (unreleased) ------------------ -*Deprecated:* - - - :func:`factory.use_strategy`. Use :attr:`factory.FactoryOptions.strategy` instead. - The purpose of :func:`~factory.use_strategy` duplicates the factory option. Follow :pep:`20`: *There should be - one-- and preferably only one --obvious way to do it.* - - :func:`~factory.use_strategy()` will be removed in the next major version. - -3.1.1 (unreleased) ------------------- - *New:* - Add support for Django 3.1 @@ -32,6 +21,20 @@ ChangeLog - Drop support for Django 1.11. This version `is not maintained anymore `__. - Drop support for Python 3.5. This version `is not maintained anymore `__. +*Deprecated:* + + - :func:`factory.use_strategy`. Use :attr:`factory.FactoryOptions.strategy` instead. + The purpose of :func:`~factory.use_strategy` duplicates the factory option. Follow :pep:`20`: *There should be + one-- and preferably only one --obvious way to do it.* + + :func:`~factory.use_strategy()` will be removed in the next major version. + +*Bug fix:* + + - :issue:`785` :issue:`786` :issue:`787` :issue:`788` :issue:`790` :issue:`796`: Calls to :class:`factory.Faker` + and :class:`factory.django.FileField` within a :class:`~factory.Trait` or :class:`~factory.Maybe` no longer lead to + a ``KeyError`` crash. + 3.1.0 (2020-10-02) ------------------ From 8a9b148af443aa3edd336b1d43b322142eadaf61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 28 Dec 2020 09:29:57 +0100 Subject: [PATCH 538/714] Show the actual line for DeprecationWarning By default, a call to 'warnings.warn()' will mention the line where that call is performed. Being a library, our goal is to point to the user code provoking the DeprecationWarning - using stacklevel=2 to refer to the caller of the 'use_strategy' function. --- factory/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/factory/base.py b/factory/base.py index a5e58427..36b2359a 100644 --- a/factory/base.py +++ b/factory/base.py @@ -720,6 +720,7 @@ def use_strategy(new_strategy): warnings.warn( "use_strategy() is deprecated and will be removed in the future.", DeprecationWarning, + stacklevel=2, ) def wrapped_class(klass): From 2c4e53c6bc30578c7be3e3b149ae5f1af8f09115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 28 Dec 2020 09:32:59 +0100 Subject: [PATCH 539/714] Adjust ChangeLog formatting for zest releaser zest.releaser will attempt to detect "(unreleased)" in changelog headers as pointers to the next version header. The section listing future removals could be confused for the header for "current version", rewrite it to avoid such mistakes. --- docs/changelog.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3b6eff72..21262ef1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,12 +1,7 @@ ChangeLog ========= -4.0.0 (unreleased) ------------------- - -*Removed:* - - - :func:`factory.use_strategy()` +.. Note for v4.x: don't forget to check "Deprecated" sections for removal. 3.2.0 (unreleased) ------------------ From e58684d00e7f186ce7dcda74a95190d61f0fb593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 28 Dec 2020 09:48:24 +0100 Subject: [PATCH 540/714] Preparing release 3.2.0 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 21262ef1..82dc8085 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.2.0 (unreleased) +3.2.0 (2020-12-28) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index 1005f55b..7a85f345 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.1.1.dev0 +version = 3.2.0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From ec4211a90cf6a6b58690d2fc7bfa2f911076e28e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 28 Dec 2020 09:48:43 +0100 Subject: [PATCH 541/714] Back to development: 3.2.1 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 82dc8085..b61278b8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,12 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. +3.2.1 (unreleased) +------------------ + +- Nothing changed yet. + + 3.2.0 (2020-12-28) ------------------ diff --git a/setup.cfg b/setup.cfg index 7a85f345..afbe5b0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.2.0 +version = 3.2.1.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From d4a9f4ae3b79ef7c3e821be16d58d385a0283abd Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 12 Feb 2021 11:05:54 -0600 Subject: [PATCH 542/714] Docs: Fix extra quote --- docs/internals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals.rst b/docs/internals.rst index 951ba54d..d71afbec 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -59,7 +59,7 @@ First, decide the strategy: Then, we'll pass the strategy and passed-in overrides to the :meth:`~Factory._generate` method. -.. note:: According to the project road map, a future version will use a :meth:`~Factory._generate_batch`` at its core instead. +.. note:: According to the project road map, a future version will use a :meth:`~Factory._generate_batch` at its core instead. A factory's :meth:`~Factory._generate` function actually delegates to a ``StepBuilder()`` object. This object will carry the overall "build an object" context (strategy, depth, and possibly other). From e083ff5e393847c23311e98fe0c66e86b149dabb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 31 Jul 2018 15:42:54 +0200 Subject: [PATCH 543/714] Introduce factory.declaration.Transformer Transforms a value using provided `transform` function. Values coming from the declaration and values overridden through keywords arguments are transformed before the generated object attribute is set. Removes the need to save objects with a post generation hook twice to the database. Facilitates overriding Django passwords when instantiating the factory. Fixes #316 Fixes #366 --- docs/changelog.rst | 28 ++++++++++++++++-- docs/orms.rst | 37 ++++++++++++++++++++++++ docs/reference.rst | 31 ++++++++++++++++++++ factory/__init__.py | 1 + factory/builder.py | 6 +++- factory/declarations.py | 16 +++++++++++ factory/django.py | 20 ++++++++++++- tests/djapp/models.py | 4 +++ tests/test_declarations.py | 6 ++++ tests/test_django.py | 59 ++++++++++++++++++++++++++++++++++++++ tests/test_transformer.py | 46 +++++++++++++++++++++++++++++ 11 files changed, 250 insertions(+), 4 deletions(-) create mode 100644 tests/test_transformer.py diff --git a/docs/changelog.rst b/docs/changelog.rst index b61278b8..dd8cbd7b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,11 +3,35 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.2.1 (unreleased) +3.3.0 (unreleased) ------------------ -- Nothing changed yet. +*New:* + + - :issue:`366`: Add :class:`factory.django.Password` to generate Django :class:`~django.contrib.auth.models.User` + passwords. + +*Deprecated:* + + - :class:`~factory.django.DjangoModelFactory` will stop issuing a second call to + :meth:`~django.db.models.Model.save` on the created instance when :ref:`post-generation-hooks` return a value. + To help with the transition, :class:`factory.django.DjangoModelFactory._after_postgeneration` raises a + :class:`DeprecationWarning` when calling :meth:`~django.db.models.Model.save`. Inspect your + :class:`~factory.django.DjangoModelFactory` subclasses: + + - If the :meth:`~django.db.models.Model.save` call is not needed after :class:`~factory.PostGeneration`, set + :attr:`factory.django.DjangoOptions.skip_postgeneration_save` to ``True`` in the factory meta. + + - Otherwise, the instance has been modified by :class:`~factory.PostGeneration` hooks and needs to be + :meth:`~django.db.models.Model.save`\ d. Either: + + - call :meth:`django.db.models.Model.save` in the :class:`~factory.PostGeneration` hook that modifies the + instance, or + - override :class:`~factory.django.DjangoModelFactory._after_postgeneration` to + :meth:`~django.db.models.Model.save` the instance. + +*Removed:* 3.2.0 (2020-12-28) ------------------ diff --git a/docs/orms.rst b/docs/orms.rst index ad776671..9397e4ac 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -109,10 +109,47 @@ All factories for a Django :class:`~django.db.models.Model` should use the >>> john.email # The email value was not updated "john@example.com" + .. attribute:: skip_postgeneration_save + + Transitional option to prevent + :meth:`~factory.django.DjangoModelFactory._after_postgeneration` from + issuing a duplicate call to :meth:`~django.db.models.Model.save` on the + created instance when :class:`factory.PostGeneration` hooks return a + value. + Extra fields """""""""""" +.. class:: Password + + Applies :func:`~django.contrib.auth.hashers.make_password` to the + clear-text argument before to generate the object. + + .. method:: __init__(self, password) + + :param str password: Default password. + + .. code-block:: python + + class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.User + + password = factory.django.Password('pw') + + .. code-block:: pycon + + >>> from django.contrib.auth.hashers import check_password + >>> # Create user with the default password from the factory. + >>> user = UserFactory.create() + >>> check_password('pw', user.password) + True + >>> # Override user password at call time. + >>> other_user = UserFactory.create(password='other_pw') + >>> check_password('other_pw', other_user.password) + True + .. class:: FileField diff --git a/docs/reference.rst b/docs/reference.rst index 6f6776b4..d425cf73 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -904,6 +904,36 @@ return value of the method: 'joel@example.com' +Transformer +""""""""""" + +.. class:: Transformer(transform, value) + +A :class:`Transformer` applies a ``transform`` function to the provided value +before to set the transformed value on the generated object. + +It expects two arguments: + +- ``transform``: function taking the value as parameter and returning the + transformed value, +- ``value``: the default value. + +.. code-block:: python + + class UpperFactory(Factory): + name = Transformer(lambda x: x.upper(), "Joe") + + class Meta: + model = Upper + +.. code-block:: pycon + + >>> UpperFactory().name + 'JOE' + >>> UpperFactory(name="John").name + 'JOHN' + + Sequence """""""" @@ -1592,6 +1622,7 @@ apply the effects of one or the other declaration: defined in the :attr:`~Factory.Params` section of your factory to handle the computation. +.. _post-generation-hooks: Post-generation hooks """"""""""""""""""""" diff --git a/factory/__init__.py b/factory/__init__.py index 2f627000..b0ada75d 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -26,6 +26,7 @@ Sequence, SubFactory, Trait, + Transformer, ) from .enums import BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY from .errors import FactoryError diff --git a/factory/builder.py b/factory/builder.py index eaf7393b..dfc96d1a 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -2,7 +2,7 @@ import collections -from . import enums, errors, utils +from . import declarations, enums, errors, utils DeclarationWithContext = collections.namedtuple( 'DeclarationWithContext', @@ -156,6 +156,10 @@ def parse_declarations(decls, base_pre=None, base_post=None): # Set it as `key__` magic_key = post_declarations.join(k, '') extra_post[magic_key] = v + elif k in pre_declarations and isinstance( + pre_declarations[k].declaration, declarations.Transformer + ): + extra_maybenonpost[k] = pre_declarations[k].declaration.function(v) else: extra_maybenonpost[k] = v diff --git a/factory/declarations.py b/factory/declarations.py index 5e150ca1..fe2e34d9 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -100,6 +100,22 @@ def evaluate(self, instance, step, extra): return self.function(instance) +class Transformer(LazyFunction): + """Transform value using given function. + + Attributes: + transform (function): returns the transformed value. + value: passed as the first argument to the transform function. + """ + + def __init__(self, transform, value, *args, **kwargs): + super().__init__(transform, *args, **kwargs) + self.value = value + + def evaluate(self, instance, step, extra): + return self.function(self.value) + + class _UNSPECIFIED: pass diff --git a/factory/django.py b/factory/django.py index 9e8954a3..78b044ca 100644 --- a/factory/django.py +++ b/factory/django.py @@ -8,7 +8,9 @@ import io import logging import os +import warnings +from django.contrib.auth.hashers import make_password from django.core import files as django_files from django.db import IntegrityError @@ -47,6 +49,7 @@ def _build_default_options(self): return super()._build_default_options() + [ base.OptionDefault('django_get_or_create', (), inherit=True), base.OptionDefault('database', DEFAULT_DB_ALIAS, inherit=True), + base.OptionDefault('skip_postgeneration_save', False, inherit=True), ] def _get_counter_reference(self): @@ -165,14 +168,29 @@ def _create(cls, model_class, *args, **kwargs): manager = cls._get_manager(model_class) return manager.create(*args, **kwargs) + # DEPRECATED. Remove this override with the next major release. @classmethod def _after_postgeneration(cls, instance, create, results=None): """Save again the instance if creating and at least one hook ran.""" - if create and results: + if create and results and not cls._meta.skip_postgeneration_save: + warnings.warn( + f"{cls.__name__}._after_postgeneration will stop saving the instance " + "after postgeneration hooks in the next major release.\n" + "If the save call is extraneous, set skip_postgeneration_save=True " + f"in the {cls.__name__}.Meta.\n" + "To keep saving the instance, move the save call to your " + "postgeneration hooks or override _after_postgeneration.", + DeprecationWarning, + ) # Some post-generation hooks ran, and may have modified us. instance.save() +class Password(declarations.Transformer): + def __init__(self, password, *args, **kwargs): + super().__init__(make_password, password, *args, **kwargs) + + class FileField(declarations.BaseDeclaration): """Helper to fill in django.db.models.FileField from a Factory.""" diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 1373c771..f0553e6d 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -75,6 +75,10 @@ class WithDefaultValue(models.Model): foo = models.CharField(max_length=20, default='') +class WithPassword(models.Model): + pw = models.CharField(max_length=128) + + WITHFILE_UPLOAD_TO = 'django' WITHFILE_UPLOAD_DIR = os.path.join(settings.MEDIA_ROOT, WITHFILE_UPLOAD_TO) diff --git a/tests/test_declarations.py b/tests/test_declarations.py index 3b9cfd1f..c9458ffe 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -132,6 +132,12 @@ def test_getter(self): self.assertEqual(3, utils.evaluate_declaration(it, force_sequence=3)) +class TransformerTestCase(unittest.TestCase): + def test_transform(self): + t = declarations.Transformer(lambda x: x.upper(), 'foo') + self.assertEqual("FOO", utils.evaluate_declaration(t)) + + class PostGenerationDeclarationTestCase(unittest.TestCase): def test_post_generation(self): call_params = [] diff --git a/tests/test_django.py b/tests/test_django.py index ad68610d..65d52530 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -10,6 +10,7 @@ import django from django import test as django_test from django.conf import settings +from django.contrib.auth.hashers import check_password from django.db.models import signals from django.test import utils as django_test_utils @@ -97,6 +98,16 @@ class Meta: model = models.ConcreteGrandSon +PASSWORD = 's0_s3cr3t' + + +class WithPasswordFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.WithPassword + + pw = factory.django.Password(password=PASSWORD) + + class WithFileFactory(factory.django.DjangoModelFactory): class Meta: model = models.WithFile @@ -414,6 +425,9 @@ class PointedRelatedFactory(PointedFactory): factory_related_name='pointed', ) + class Meta: + skip_postgeneration_save = True + class PointerExtraFactory(PointerFactory): pointed__foo = 'extra_new_foo' @@ -430,6 +444,9 @@ class Params: ) ) + class Meta: + skip_postgeneration_save = True + cls.PointedFactory = PointedFactory cls.PointerFactory = PointerFactory cls.PointedRelatedFactory = PointedRelatedFactory @@ -492,6 +509,21 @@ def test_create_pointed_related_with_trait(self): self.assertEqual(pointed.pointer.bar, 'with_trait') +class DjangoPasswordTestCase(django_test.TestCase): + def test_build(self): + u = WithPasswordFactory.build() + self.assertTrue(check_password(PASSWORD, u.pw)) + + def test_build_with_kwargs(self): + password = 'V3R¥.S€C®€T' + u = WithPasswordFactory.build(pw=password) + self.assertTrue(check_password(password, u.pw)) + + def test_create(self): + u = WithPasswordFactory.create() + self.assertTrue(check_password(PASSWORD, u.pw)) + + class DjangoFileFieldTestCase(django_test.TestCase): def tearDown(self): @@ -909,6 +941,7 @@ def test_class_decorator_with_subfactory(self): class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): class Meta: model = models.WithSignals + skip_postgeneration_save = True @factory.post_generation def post(obj, create, extracted, **kwargs): @@ -995,6 +1028,7 @@ def test_class_decorator_with_muted_related_factory(self): class UndecoratedFactory(factory.django.DjangoModelFactory): class Meta: model = models.PointerModel + skip_postgeneration_save = True pointed = factory.RelatedFactory(self.WithSignalsDecoratedFactory) UndecoratedFactory() @@ -1015,3 +1049,28 @@ class Meta: # Our CustomManager will remove the 'arg=' argument, # invalid for the actual model. ObjFactory.create(arg='invalid') + + +class DjangoModelFactoryDuplicateSaveDeprecationTest(django_test.TestCase): + class StandardFactoryWithPost(StandardFactory): + @factory.post_generation + def post_action(obj, create, extracted, **kwargs): + return 3 + + def test_create_warning(self): + with self.assertWarns(DeprecationWarning) as cm: + self.StandardFactoryWithPost.create() + + [msg] = cm.warning.args + self.assertEqual( + msg, + "StandardFactoryWithPost._after_postgeneration will stop saving the " + "instance after postgeneration hooks in the next major release.\n" + "If the save call is extraneous, set skip_postgeneration_save=True in the " + "StandardFactoryWithPost.Meta.\n" + "To keep saving the instance, move the save call to your postgeneration " + "hooks or override _after_postgeneration.", + ) + + def test_build_no_warning(self): + self.StandardFactoryWithPost.build() diff --git a/tests/test_transformer.py b/tests/test_transformer.py new file mode 100644 index 00000000..00658454 --- /dev/null +++ b/tests/test_transformer.py @@ -0,0 +1,46 @@ +# Copyright: See the LICENSE file. + +from unittest import TestCase + +from factory import Factory, Transformer + + +class TransformCounter: + calls_count = 0 + + @classmethod + def __call__(cls, x): + cls.calls_count += 1 + return x.upper() + + @classmethod + def reset(cls): + cls.calls_count = 0 + + +transform = TransformCounter() + + +class Upper: + def __init__(self, name): + self.name = name + + +class UpperFactory(Factory): + name = Transformer(transform, "value") + + class Meta: + model = Upper + + +class TransformerTest(TestCase): + def setUp(self): + transform.reset() + + def test_transform_count(self): + self.assertEqual("VALUE", UpperFactory().name) + self.assertEqual(transform.calls_count, 1) + + def test_transform_kwarg(self): + self.assertEqual("TEST", UpperFactory(name="test").name) + self.assertEqual(transform.calls_count, 1) From ade3b8a697af4cab84c858cee53d03249a02126f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 3 Feb 2021 21:05:30 +0100 Subject: [PATCH 544/714] Set long description content type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes twine check warning: ``` Checking dist/factory_boy-3.2.1.dev0.tar.gz: PASSED, with warnings warning: `long_description_content_type` missing. defaulting to `text/x-rst`. ``` Reviewed-by: Raphaël Barrois --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index afbe5b0a..3f0998b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,8 @@ name = factory_boy version = 3.2.1.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst +# https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data +long_description_content_type = text/x-rst author = Mark Sandstrom author_email = mark@deliciouslynerdy.com maintainer = Raphaël Barrois From b59ef2fd7513ccd6fbba62b0a535ec004d697b0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 22 Dec 2020 21:45:24 +0100 Subject: [PATCH 545/714] Restore linkcheck builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds are scheduled once a week. Fixes #771 Reviewed-by: Raphaël Barrois --- .github/workflows/linkcheck.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/linkcheck.yml diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml new file mode 100644 index 00000000..62144ef1 --- /dev/null +++ b/.github/workflows/linkcheck.yml @@ -0,0 +1,21 @@ +name: Linkcheck + +on: + schedule: + - cron: '11 11 * * 1' + +jobs: + build: + name: Linkcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + + - name: Install dependencies + run: python -m pip install tox + + - name: Run linkcheck + run: tox -e linkcheck From 3a827f098cc3ac8003e81b18e42e0d14a9b2e2e3 Mon Sep 17 00:00:00 2001 From: gregoiredx Date: Tue, 20 Oct 2020 18:01:33 +0200 Subject: [PATCH 546/714] =?UTF-8?q?Don=E2=80=99t=20override=20signal=20rec?= =?UTF-8?q?eivers=20registered=20during=20mute=5Fsignals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CREDITS | 1 + docs/changelog.rst | 4 ++++ factory/django.py | 2 +- tests/djapp/models.py | 9 +++++++++ tests/test_django.py | 10 ++++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index fe426d35..caa75380 100644 --- a/CREDITS +++ b/CREDITS @@ -39,6 +39,7 @@ The project has received contributions from (in alphabetical order): * Flavio Curella * François Freitag * George Hickman +* Grégoire Deveaux * Hervé Cauwelier * Hugo Osvaldo Barrera * Ilya Baryshev diff --git a/docs/changelog.rst b/docs/changelog.rst index dd8cbd7b..13909638 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -41,6 +41,10 @@ ChangeLog - Add support for Django 3.1 - Add support for Python 3.9 +*Bugfix:* + + - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. + *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. diff --git a/factory/django.py b/factory/django.py index 78b044ca..7e5427fe 100644 --- a/factory/django.py +++ b/factory/django.py @@ -303,7 +303,7 @@ def __exit__(self, exc_type, exc_value, traceback): logger.debug('mute_signals: Restoring signal handlers %r', receivers) - signal.receivers = receivers + signal.receivers += receivers with signal.lock: # Django uses some caching for its signals. # Since we're bypassing signal.connect and signal.disconnect, diff --git a/tests/djapp/models.py b/tests/djapp/models.py index f0553e6d..fb34e907 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -6,6 +6,7 @@ from django.conf import settings from django.db import models +from django.db.models import signals try: from PIL import Image @@ -101,6 +102,14 @@ class WithImage(models.Model): class WithSignals(models.Model): foo = models.CharField(max_length=20) + def __init__(self, post_save_signal_receiver=None): + super().__init__() + if post_save_signal_receiver: + signals.post_save.connect( + post_save_signal_receiver, + sender=self.__class__, + ) + class CustomManager(models.Manager): diff --git a/tests/test_django.py b/tests/test_django.py index 65d52530..48b94329 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -909,6 +909,16 @@ def test_context_manager(self): self.assertSignalsReactivated() + def test_receiver_created_during_model_instantiation_is_not_lost(self): + with factory.django.mute_signals(signals.post_save): + instance = WithSignalsFactory(post_save_signal_receiver=self.handlers.created_during_instantiation) + self.assertTrue(self.handlers.created_during_instantiation.called) + + self.handlers.created_during_instantiation.reset_mock() + instance.save() + + self.assertTrue(self.handlers.created_during_instantiation.called) + def test_signal_cache(self): with factory.django.mute_signals(signals.pre_save, signals.post_save): signals.post_save.connect(self.handlers.mute_block_receiver) From b53b2be578a92b48934248ebeedb99d8215f17a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 18 Feb 2021 10:57:15 +0100 Subject: [PATCH 547/714] Move #799 changelog entry to the unreleased section :facepalm: --- docs/changelog.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 13909638..82fde406 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,10 @@ ChangeLog - :issue:`366`: Add :class:`factory.django.Password` to generate Django :class:`~django.contrib.auth.models.User` passwords. +*Bugfix:* + + - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. + *Deprecated:* - :class:`~factory.django.DjangoModelFactory` will stop issuing a second call to @@ -41,10 +45,6 @@ ChangeLog - Add support for Django 3.1 - Add support for Python 3.9 -*Bugfix:* - - - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. - *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. From 2901d08d51940437620d0846a110430a02cf5096 Mon Sep 17 00:00:00 2001 From: Shahriar Tajbakhsh Date: Tue, 9 Feb 2021 12:10:16 +0000 Subject: [PATCH 548/714] Avoid SQLAlchemyModelFactory name clash with a field named session Internal helpers `_create` and `_get_or_create` expected to receive the SQLAlchemy session as a keyword argument. Passing a value for a field named session as a keyword argument to a SQLAlchemyModelFactory subclass caused a name clash: TypeError: got multiple values for keyword argument 'session'. Fix #775 --- CREDITS | 1 + docs/changelog.rst | 4 ++++ factory/alchemy.py | 10 +++++----- tests/alchemyapp/models.py | 7 +++++++ tests/test_alchemy.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/CREDITS b/CREDITS index caa75380..b7ba1ab2 100644 --- a/CREDITS +++ b/CREDITS @@ -73,6 +73,7 @@ The project has received contributions from (in alphabetical order): * Samuel Paccoud * Saul Shanabrook * Sean Löfgren +* Shahriar Tajbakhsh * Tom * alex-netquity * anentropic diff --git a/docs/changelog.rst b/docs/changelog.rst index 82fde406..913bd666 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,10 @@ ChangeLog - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. + - :issue:`775`: Change the signature for :meth:`~factory.alchemy.SQLAlchemyModelFactory._save` and + :meth:`~factory.alchemy.SQLAlchemyModelFactory._get_or_create` to avoid argument names clashes with a field named + ``session``. + *Deprecated:* - :class:`~factory.django.DjangoModelFactory` will stop issuing a second call to diff --git a/factory/alchemy.py b/factory/alchemy.py index 2d248872..ef7a591a 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -51,7 +51,7 @@ def _generate(cls, strategy, params): return super()._generate(strategy, params) @classmethod - def _get_or_create(cls, model_class, session, *args, **kwargs): + def _get_or_create(cls, model_class, session, args, kwargs): key_fields = {} for field in cls._meta.sqlalchemy_get_or_create: if field not in kwargs: @@ -66,7 +66,7 @@ def _get_or_create(cls, model_class, session, *args, **kwargs): if not obj: try: - obj = cls._save(model_class, session, *args, **key_fields, **kwargs) + obj = cls._save(model_class, session, args, {**key_fields, **kwargs}) except IntegrityError as e: session.rollback() get_or_create_params = { @@ -95,11 +95,11 @@ def _create(cls, model_class, *args, **kwargs): if session is None: raise RuntimeError("No session provided.") if cls._meta.sqlalchemy_get_or_create: - return cls._get_or_create(model_class, session, *args, **kwargs) - return cls._save(model_class, session, *args, **kwargs) + return cls._get_or_create(model_class, session, args, kwargs) + return cls._save(model_class, session, args, kwargs) @classmethod - def _save(cls, model_class, session, *args, **kwargs): + def _save(cls, model_class, session, args, kwargs): session_persistence = cls._meta.sqlalchemy_session_persistence obj = model_class(*args, **kwargs) diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 013a56ae..8e76b5bd 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -43,4 +43,11 @@ class NonIntegerPk(Base): id = Column(Unicode(20), primary_key=True) +class SpecialFieldModel(Base): + __tablename__ = 'SpecialFieldModelTable' + + id = Column(Integer(), primary_key=True) + session = Column(Unicode(20)) + + Base.metadata.create_all(engine) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 2205ca1e..95e7f857 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -261,3 +261,34 @@ def test_build_does_not_raises_exception_when_no_session_was_set(self): inst1 = NoSessionFactory.build() self.assertEqual(inst0.id, 0) self.assertEqual(inst1.id, 1) + + +class NameConflictTests(unittest.TestCase): + """Regression test for `TypeError: _save() got multiple values for argument 'session'` + + See #775. + """ + def test_no_name_conflict_on_save(self): + class SpecialFieldWithSaveFactory(SQLAlchemyModelFactory): + class Meta: + model = models.SpecialFieldModel + sqlalchemy_session = models.session + + id = factory.Sequence(lambda n: n) + session = '' + + saved_child = SpecialFieldWithSaveFactory() + self.assertEqual(saved_child.session, "") + + def test_no_name_conflict_on_get_or_create(self): + class SpecialFieldWithGetOrCreateFactory(SQLAlchemyModelFactory): + class Meta: + model = models.SpecialFieldModel + sqlalchemy_get_or_create = ('session',) + sqlalchemy_session = models.session + + id = factory.Sequence(lambda n: n) + session = '' + + get_or_created_child = SpecialFieldWithGetOrCreateFactory() + self.assertEqual(get_or_created_child.session, "") From 37f962720814dff42d7a6a848ccfd200fc7f5ae2 Mon Sep 17 00:00:00 2001 From: CheesyFeet Date: Wed, 17 Mar 2021 11:23:20 +0000 Subject: [PATCH 549/714] Fix typo. --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index 9397e4ac..19b91578 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -514,7 +514,7 @@ Here is an example layout: .. code-block:: python - # myprojet/test/common.py + # myproject/test/common.py from sqlalchemy import orm Session = orm.scoped_session(orm.sessionmaker()) From c21024cec0442ff844fafec2b7a5a7404228db06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 29 Mar 2021 14:12:11 +0200 Subject: [PATCH 550/714] Linkcheck: Retry failed URLs Some sites are not reliable. Avoid failing the build on transient failures. --- docs/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index b885b99d..43e63fff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,6 +73,10 @@ html_static_path = ['_static'] +# -- linkcheck --------------------------------------------------------------- +linkcheck_retries = 3 + + # -- intersphinx ------------------------------------------------------------- intersphinx_mapping = { 'https://docs.python.org/': None, From 90faf354a016835c82b83106338a63b0779fe1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 3 May 2021 14:05:48 +0200 Subject: [PATCH 551/714] =?UTF-8?q?Remove=20dead=20link=20to=20author?= =?UTF-8?q?=E2=80=99s=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 913bd666..a7aa7ee4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -441,7 +441,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *Bug fix:* - - Fix badly written assert containing state-changing code, spotted by `chsigi `_ (:pr:`126`) + - Fix badly written assert containing state-changing code, spotted by ``chsigi`` (:pr:`126`) - Don't crash when handling objects whose ``__repr__`` is non-pure-ASCII bytes on Python 2, discovered by `mbertheau `_ (:issue:`123`) and `strycore `_ (:pr:`127`) From 17e2b6703f1b8c8bbf3ad59ce06f83e2e3c9cbf5 Mon Sep 17 00:00:00 2001 From: Ben Hampson <77866043+Ben-Hampson@users.noreply.github.com> Date: Tue, 25 May 2021 13:24:05 +0100 Subject: [PATCH 552/714] README.rst fix: Correct the default build strategy UserFactory() == UserFactory.build() UserFactory() != UserFactory.create() --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 197791cb..ae4e6a00 100644 --- a/README.rst +++ b/README.rst @@ -174,7 +174,7 @@ You can use the Factory class as a shortcut for the default build strategy: .. code-block:: python - # Same as UserFactory.create() + # Same as UserFactory.build() user = UserFactory() From 2d088c9353bc66cc18fdd54175ad8c1f584b3ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 30 May 2021 12:18:06 +0200 Subject: [PATCH 553/714] Clarify nuance between build and create strategies The readme used the term "build strategy" to refer to both the "build" strategy, and the model instantiation strategy; for that second meaning, the wording is now "Instantiation strategy". See #859. --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index ae4e6a00..d0eb091c 100644 --- a/README.rst +++ b/README.rst @@ -155,7 +155,7 @@ More details can be found in the ORM section. Using factories """"""""""""""" -factory_boy supports several different build strategies: build, create, and stub: +factory_boy supports several different instantiation strategies: build, create, and stub: .. code-block:: python @@ -170,11 +170,11 @@ factory_boy supports several different build strategies: build, create, and stub obj = UserFactory.stub() -You can use the Factory class as a shortcut for the default build strategy: +You can use the Factory class as a shortcut for the default instantiation strategy: .. code-block:: python - # Same as UserFactory.build() + # Same as UserFactory.create() user = UserFactory() From f3ab3bd51a83b5a9f31bd303d7a20d9435f50586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Mendes?= Date: Thu, 27 May 2021 09:03:15 -0300 Subject: [PATCH 554/714] Fix a typo in docs. --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index d425cf73..2412172c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -703,7 +703,7 @@ Faker .. code-block:: python - class UserFactory(fatory.Factory): + class UserFactory(factory.Factory): class Meta: model = User From 11a86c48c41c9027c6e8d33856b4981eb496bddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Mendes?= Date: Thu, 27 May 2021 10:16:44 -0300 Subject: [PATCH 555/714] Fix another typo in docs. --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 2412172c..cc584a54 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -718,7 +718,7 @@ Faker .. code-block:: python - class TripFactory(fatory.Factory): + class TripFactory(factory.Factory): class Meta: model = Trip From 5caf8547f50d96f52f4d08d4aaa626f1662628ed Mon Sep 17 00:00:00 2001 From: sarahboyce Date: Sun, 20 Jun 2021 17:11:48 +0200 Subject: [PATCH 556/714] docs: update the references to sequence counters to consistently count from 0 add sarah boyce to credits file --- CREDITS | 1 + docs/introduction.rst | 13 +++++++++++-- docs/orms.rst | 4 ++-- docs/recipes.rst | 4 ++-- docs/reference.rst | 27 +++++++++++++++------------ 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/CREDITS b/CREDITS index b7ba1ab2..6ceb21ca 100644 --- a/CREDITS +++ b/CREDITS @@ -71,6 +71,7 @@ The project has received contributions from (in alphabetical order): * Rob Zyskowski * Robrecht De Rouck * Samuel Paccoud +* Sarah Boyce * Saul Shanabrook * Sean Löfgren * Shahriar Tajbakhsh diff --git a/docs/introduction.rst b/docs/introduction.rst index d26e4b9f..e9ad3248 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -99,11 +99,19 @@ This is achieved with the :class:`~factory.Sequence` declaration: .. code-block:: pycon + >>> # The sequence counter starts at 0 by default >>> UserFactory() >>> UserFactory() + >>> # A value can be provided for a sequence-driven field + >>> # but this still increments the sequence counter + >>> UserFactory(username="ada.lovelace") + + >>> UserFactory() + + .. 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 @@ -116,6 +124,7 @@ This is achieved with the :class:`~factory.Sequence` declaration: def username(n): return 'user%d' % n + To set or reset the sequence counter see :ref:`Forcing a sequence counter `. LazyFunction ------------ @@ -165,7 +174,7 @@ taking the object being built and returning the value for the field: .. code-block:: pycon >>> UserFactory() - + >>> # The LazyAttribute handles overridden fields >>> UserFactory(username='john') @@ -173,7 +182,7 @@ taking the object being built and returning the value for the field: >>> # They can be directly overridden as well >>> UserFactory(email='doe@example.com') - + .. note:: As for :class:`~factory.Sequence`, a :meth:`~factory.@lazy_attribute` decorator is available: diff --git a/docs/orms.rst b/docs/orms.rst index 19b91578..e464fc4e 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -476,9 +476,9 @@ A (very) simple example: >>> session.query(User).all() [] >>> UserFactory() - + >>> session.query(User).all() - [] + [] Managing sessions diff --git a/docs/recipes.rst b/docs/recipes.rst index a07d9ef5..41ae84ad 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -545,13 +545,13 @@ In order to get a dict, we'll just have to swap the model; the easiest way is to class Meta: model = models.User - first_name = factory.Sequence(lambda n: "Agent %03d" % n) + first_name = factory.Sequence(lambda n: "Agent %03d" % n) # Agent 000, Agent 001, Agent 002 username = factory.Faker('user_name') .. code-block:: pycon >>> factory.build(dict, FACTORY_CLASS=UserFactory) - {'first_name': "Agent 001", 'username': 'john_doe'} + {'first_name': "Agent 000", 'username': 'john_doe'} Fuzzying Django model field choices diff --git a/docs/reference.rst b/docs/reference.rst index cc584a54..fec8f7ae 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -956,10 +956,12 @@ This declaration takes a single argument, a function accepting a single paramete .. code-block:: pycon >>> UserFactory().phone - '123-555-0001' + '123-555-0000' >>> UserFactory().phone - '123-555-0002' + '123-555-0001' +.. note:: The sequence counter starts at 0 and can be set or reset, + see :ref:`Forcing a sequence counter `. Decorator ~~~~~~~~~ @@ -985,9 +987,9 @@ be the sequence counter - this might be confusing: .. code-block:: pycon - >>> UserFactory().phone + >>> UserFactory().phone # current sequence counter at 9999 '000-555-9999' - >>> UserFactory().phone + >>> UserFactory().phone # current sequence counter at 10000 '001-555-0000' @@ -1010,10 +1012,10 @@ The sequence counter is shared across all :class:`Sequence` attributes of the >>> u = UserFactory() >>> u.phone, u.office - '0041', 'A23-B041' + '0040', 'A23-B040' >>> u2 = UserFactory() >>> u2.phone, u2.office - '0042', 'A23-B042' + '0041', 'A23-B041' Inheritance @@ -1039,16 +1041,17 @@ is shared across the :class:`Factory` classes: >>> u = UserFactory() >>> u.phone - '123-555-0001' + '123-555-0000' >>> e = EmployeeFactory() >>> e.phone, e.office_phone - '123-555-0002', '0002' + '123-555-0001', '0001' >>> u2 = UserFactory() >>> u2.phone - '123-555-0003' + '123-555-0002' +.. _forcing-a-sequence-counter: Forcing a sequence counter ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1107,9 +1110,9 @@ It takes a single argument, a function whose two parameters are, in order: .. code-block:: pycon >>> UserFactory().email - 'john@s1.example.com' + 'john@s0.example.com' >>> UserFactory(login='jack').email - 'jack@s2.example.com' + 'jack@s1.example.com' Decorator @@ -1305,7 +1308,7 @@ That declaration takes a single argument, a dot-delimited path to the attribute class Meta: model = User - birthdate = factory.Sequence(lambda n: datetime.date(2000, 1, 1) + datetime.timedelta(days=n)) + birthdate = factory.fuzzy.FuzzyDate() birthmonth = factory.SelfAttribute('birthdate.month') .. code-block:: pycon From 6a5ef784e31187ab056160a3e21c9435697a8fa6 Mon Sep 17 00:00:00 2001 From: Ben Hampson <77866043+Ben-Hampson@users.noreply.github.com> Date: Tue, 25 May 2021 15:11:38 +0100 Subject: [PATCH 557/714] Add import to Django Example Add `from django.db.models.signals import post_save` for the `mute_signals` example. --- docs/recipes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index 41ae84ad..62b193c0 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -106,6 +106,8 @@ Since version 2.9, the :meth:`~factory.django.mute_signals` decorator should be .. code-block:: python + from django.db.models.signals import post_save + @factory.django.mute_signals(post_save) class ProfileFactory(factory.django.DjangoModelFactory): class Meta: From de4e127d752c86f486cf5eec6636a5de17372c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 24 Apr 2021 09:54:53 +0200 Subject: [PATCH 558/714] Add support for Django 3.2 Reviewed-by: rbarrois Reviewed-by: sarahboyce --- .github/workflows/test.yml | 1 + docs/changelog.rst | 1 + setup.cfg | 1 + tox.ini | 4 +++- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5da662f6..1777507b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: - django22-alchemy-mongoengine - django30-alchemy-mongoengine - django31-alchemy-mongoengine + - django32-alchemy-mongoengine services: mongodb: diff --git a/docs/changelog.rst b/docs/changelog.rst index a7aa7ee4..db71dec7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog - :issue:`366`: Add :class:`factory.django.Password` to generate Django :class:`~django.contrib.auth.models.User` passwords. + - Add support for Django 3.2 *Bugfix:* diff --git a/setup.cfg b/setup.cfg index 3f0998b3..28028c47 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ classifiers = Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 + Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index f8774aaa..ac79729b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py{36,37,38,39,py3}-django22-alchemy-mongoengine py{36,37,38,39,py3}-django30-alchemy-mongoengine py{36,37,38,39,py3}-django31-alchemy-mongoengine + py{36,37,38,39,py3}-django32-alchemy-mongoengine py39-djangomaster-alchemy-mongoengine docs examples @@ -17,8 +18,9 @@ deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 + django32: Django>=3.2,<3.3 djangomaster: https://github.com/django/django/archive/master.tar.gz - django{22,30,31,master}: Pillow + django{22,30,31,32,master}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 764ffabd3fbfc825fb3b6e8986a9a6d4879051f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 25 Jun 2021 09:47:30 +0200 Subject: [PATCH 559/714] Drop support for end-of-life Django 3.0 https://www.djangoproject.com/download/#supported-versions Reviewed-by: Jon Dufresne --- .github/workflows/test.yml | 1 - docs/changelog.rst | 2 ++ setup.cfg | 1 - tox.ini | 4 +--- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1777507b..e9384265 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: - pypy3 tox-environment: - django22-alchemy-mongoengine - - django30-alchemy-mongoengine - django31-alchemy-mongoengine - django32-alchemy-mongoengine diff --git a/docs/changelog.rst b/docs/changelog.rst index db71dec7..d2045ab5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -42,6 +42,8 @@ ChangeLog *Removed:* + - Drop support for Django 3.0 + 3.2.0 (2020-12-28) ------------------ diff --git a/setup.cfg b/setup.cfg index 28028c47..8c4e502b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 2.2 - Framework :: Django :: 3.0 Framework :: Django :: 3.1 Framework :: Django :: 3.2 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index ac79729b..e517f29e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,6 @@ minversion = 1.9 envlist = lint py{36,37,38,39,py3}-django22-alchemy-mongoengine - py{36,37,38,39,py3}-django30-alchemy-mongoengine py{36,37,38,39,py3}-django31-alchemy-mongoengine py{36,37,38,39,py3}-django32-alchemy-mongoengine py39-djangomaster-alchemy-mongoengine @@ -16,11 +15,10 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = django22: Django>=2.2,<2.3 - django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 django32: Django>=3.2,<3.3 djangomaster: https://github.com/django/django/archive/master.tar.gz - django{22,30,31,32,master}: Pillow + django{22,31,32,master}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 0439342c626265ab6bf0bdac2738bd4d6c4d2606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 18 Feb 2021 12:07:08 +0100 Subject: [PATCH 560/714] Deprecate doc note about second call to ``save()`` for post generation Helps keeping track of what should be removed in release 4.0. Reviewed-by: Jon Dufresne Refs #316 Refs #366 --- docs/recipes.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/recipes.rst b/docs/recipes.rst index 62b193c0..9a1a3752 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -143,6 +143,10 @@ factory_boy related factories. Any factories that call these classes with :class:`~factory.SubFactory` will also need to be decorated in the same manner. +.. + _DEPRECATED: Release 4.0: post_generation and RelatedFactory will stop + issuing calls to save(). Refs issues 316 and 366. + .. 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()``. From 023ae9042f9aea14f65691be4509dadcb01e7f0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 2 Jul 2021 09:59:05 +0200 Subject: [PATCH 561/714] Set USE_TZ=True for the Django test project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Django 5.0 will change the default from USE_TZ=False to USE_TZ=True. https://docs.djangoproject.com/en/dev/ref/settings/#use-tz Apply the default immediately to avoid the following deprecation warning in Django 4.0+: django.utils.deprecation.RemovedInDjango50Warning: The default value of USE_TZ will change from False to True in Django 5.0. Set USE_TZ to False in your project settings if you want to keep the current default behavior. Reviewed-by: Raphaël Barrois --- tests/djapp/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/djapp/settings.py b/tests/djapp/settings.py index d14e3e5c..13f7d366 100644 --- a/tests/djapp/settings.py +++ b/tests/djapp/settings.py @@ -29,3 +29,7 @@ MIDDLEWARE_CLASSES = () SECRET_KEY = 'testing.' + +# TODO: Will be the default after Django 5.0. Remove this setting when +# Django 5.0 is the last supported version. +USE_TZ = True From 4c1ec758d1c41dd4bc7890e57962feed425813d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 24 Apr 2021 09:50:29 +0200 Subject: [PATCH 562/714] Restore Django main build The build was not ported to GitHub actions in d8b241c94c326ce6c241cf8b6c7c8745599e4583. In the meantime, Django renamed their development branch from master to main. Reviewed-by: Jon Dufresne --- .github/workflows/test.yml | 3 +++ tox.ini | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e9384265..5b7c64fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,9 @@ jobs: - django22-alchemy-mongoengine - django31-alchemy-mongoengine - django32-alchemy-mongoengine + include: + - python-version: 3.9 + tox-environment: djangomain-alchemy-mongoengine services: mongodb: diff --git a/tox.ini b/tox.ini index e517f29e..90dfb0fc 100644 --- a/tox.ini +++ b/tox.ini @@ -17,8 +17,8 @@ deps = django22: Django>=2.2,<2.3 django31: Django>=3.1,<3.2 django32: Django>=3.2,<3.3 - djangomaster: https://github.com/django/django/archive/master.tar.gz - django{22,31,32,master}: Pillow + djangomain: https://github.com/django/django/archive/main.tar.gz + django{22,31,32,main}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From c72f790760446a577a435d2576985f22b33adb8f Mon Sep 17 00:00:00 2001 From: Matthieu Rigal Date: Tue, 3 Nov 2020 16:29:02 +0100 Subject: [PATCH 563/714] docs: Update Many-to-Many Recipe to use bulk_add --- docs/recipes.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 9a1a3752..6afa9d1a 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -191,14 +191,12 @@ hook: @factory.post_generation def groups(self, create, extracted, **kwargs): - if not create: - # Simple build, do nothing. + if not create or not extracted: + # Simple build, or nothing to add, do nothing. return - if extracted: - # A list of groups were passed in, use them - for group in extracted: - self.groups.add(group) + # Add the iterable of groups using bulk addition + self.groups.add(*extracted) .. OHAI_VIM** From 6d421c7d6327641d0af92a5a9d1cfba9f2fc5ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 2 Jul 2021 16:50:41 +0200 Subject: [PATCH 564/714] Rewrite extra post-declarations loop in parse_declarations Avoid iterating twice over the extra_maybenonpost items. Clarify all items from extra_maybenonpost are split into two dicts. Reviewed-by: Jon Dufresne --- factory/builder.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/factory/builder.py b/factory/builder.py index dfc96d1a..9d810fbe 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -167,19 +167,17 @@ def parse_declarations(decls, base_pre=None, base_post=None): post_declarations.update(extra_post) # Fill in extra post-declaration context + extra_pre_declarations = {} + extra_post_declarations = {} post_overrides = post_declarations.filter(extra_maybenonpost) - post_declarations.update({ - k: v - for k, v in extra_maybenonpost.items() - if k in post_overrides - }) - - # Anything else is pre_declarations - pre_declarations.update({ - k: v - for k, v in extra_maybenonpost.items() - if k not in post_overrides - }) + for k, v in extra_maybenonpost.items(): + if k in post_overrides: + extra_post_declarations[k] = v + else: + # Anything else is pre_declarations + extra_pre_declarations[k] = v + pre_declarations.update(extra_pre_declarations) + post_declarations.update(extra_post_declarations) return pre_declarations, post_declarations From bb37ef88e2c6b115d9b68d3b4cb7d423b43e4b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 3 Sep 2021 17:11:06 +0200 Subject: [PATCH 565/714] testing: properly reset sequences When tests assert a given sequence ID for the created instance, ensure the factory's sequence counter is reset beforehand. Closes #884 --- tests/test_alchemy.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 95e7f857..03410838 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -257,6 +257,7 @@ def test_create_raises_exception_when_no_session_was_set(self): NoSessionFactory.create() def test_build_does_not_raises_exception_when_no_session_was_set(self): + NoSessionFactory.reset_sequence() # Make sure we start at test ID 0 inst0 = NoSessionFactory.build() inst1 = NoSessionFactory.build() self.assertEqual(inst0.id, 0) From 00bd29292bd99aca4b8659bece464ed30c200c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 3 Sep 2021 17:29:24 +0200 Subject: [PATCH 566/714] docs: Add packaging tips See #865 for extra details --- README.rst | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.rst b/README.rst index d0eb091c..b039790c 100644 --- a/README.rst +++ b/README.rst @@ -415,3 +415,43 @@ To avoid running ``mongoengine`` tests (e.g no MongoDB server installed), run: .. code-block:: sh $ make SKIP_MONGOENGINE=1 test + + +Packaging +--------- + +For users interesting in packaging FactoryBoy into downstream distribution channels +(e.g. ``.deb``, ``.rpm``, ``.ebuild``), the following tips might be helpful: + +Dependencies +"""""""""""" + +The package's run-time dependencies are listed in ``setup.cfg``. +The dependencies useful for building and testing the library are covered by the +``dev`` and ``doc`` extras. + +Moreover, all development / testing tasks are driven through ``make(1)``. + +Building +"""""""" + +In order to run the build steps (currently only for docs), run: + +.. code-block:: sh + + python setup.py egg_info + make doc + +Testing +""""""" + +When testing for the active Python environment, run the following: + +.. code-block:: sh + + make test + +.. note:: + + You must make sure that the ``factory`` module is importable, as it is imported from + the testing code. From 371815b219d0f91c3032fdd479fc2ef7d1d3af6b Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Wed, 13 Oct 2021 22:01:53 +0100 Subject: [PATCH 567/714] Fix path of moved class This class was moved in https://github.com/FactoryBoy/factory_boy/commit/639e5cd1c6baf1cb19d9134545e29fbb5ba16d99 --- docs/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index fec8f7ae..76f913fd 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1946,7 +1946,7 @@ users in an external system. self.registration_id = system.register(auth_token) - class UserFactory(factory.DjangoModelFactory): + class UserFactory(factory.django.DjangoModelFactory): class Meta: model = User From 1d387b480ca2437d6bcb8b1fab26e6369355fca2 Mon Sep 17 00:00:00 2001 From: gregoiredx Date: Tue, 20 Oct 2020 18:01:33 +0200 Subject: [PATCH 568/714] =?UTF-8?q?Don=E2=80=99t=20override=20signal=20rec?= =?UTF-8?q?eivers=20registered=20during=20mute=5Fsignals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CREDITS | 1 + docs/changelog.rst | 4 ++++ factory/django.py | 2 +- tests/djapp/models.py | 9 +++++++++ tests/test_django.py | 10 ++++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CREDITS b/CREDITS index fe426d35..caa75380 100644 --- a/CREDITS +++ b/CREDITS @@ -39,6 +39,7 @@ The project has received contributions from (in alphabetical order): * Flavio Curella * François Freitag * George Hickman +* Grégoire Deveaux * Hervé Cauwelier * Hugo Osvaldo Barrera * Ilya Baryshev diff --git a/docs/changelog.rst b/docs/changelog.rst index b61278b8..7de117f9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,10 @@ ChangeLog - Add support for Django 3.1 - Add support for Python 3.9 +*Bugfix:* + + - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. + *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. diff --git a/factory/django.py b/factory/django.py index 9e8954a3..8d29a93b 100644 --- a/factory/django.py +++ b/factory/django.py @@ -285,7 +285,7 @@ def __exit__(self, exc_type, exc_value, traceback): logger.debug('mute_signals: Restoring signal handlers %r', receivers) - signal.receivers = receivers + signal.receivers += receivers with signal.lock: # Django uses some caching for its signals. # Since we're bypassing signal.connect and signal.disconnect, diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 1373c771..6198a5f9 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -6,6 +6,7 @@ from django.conf import settings from django.db import models +from django.db.models import signals try: from PIL import Image @@ -97,6 +98,14 @@ class WithImage(models.Model): class WithSignals(models.Model): foo = models.CharField(max_length=20) + def __init__(self, post_save_signal_receiver=None): + super().__init__() + if post_save_signal_receiver: + signals.post_save.connect( + post_save_signal_receiver, + sender=self.__class__, + ) + class CustomManager(models.Manager): diff --git a/tests/test_django.py b/tests/test_django.py index ad68610d..462cb9c1 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -877,6 +877,16 @@ def test_context_manager(self): self.assertSignalsReactivated() + def test_receiver_created_during_model_instantiation_is_not_lost(self): + with factory.django.mute_signals(signals.post_save): + instance = WithSignalsFactory(post_save_signal_receiver=self.handlers.created_during_instantiation) + self.assertTrue(self.handlers.created_during_instantiation.called) + + self.handlers.created_during_instantiation.reset_mock() + instance.save() + + self.assertTrue(self.handlers.created_during_instantiation.called) + def test_signal_cache(self): with factory.django.mute_signals(signals.pre_save, signals.post_save): signals.post_save.connect(self.handlers.mute_block_receiver) From 15e3245d864514f653785ba1f2aa47c2da1d65d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 18 Feb 2021 10:57:15 +0100 Subject: [PATCH 569/714] Move #799 changelog entry to the unreleased section :facepalm: --- docs/changelog.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7de117f9..1079f2a7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,9 @@ ChangeLog 3.2.1 (unreleased) ------------------ -- Nothing changed yet. +*Bugfix:* + + - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. 3.2.0 (2020-12-28) @@ -17,10 +19,6 @@ ChangeLog - Add support for Django 3.1 - Add support for Python 3.9 -*Bugfix:* - - - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. - *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. From f2c1e11853712d6356a7d73c17a11ac4a19e23ec Mon Sep 17 00:00:00 2001 From: Shahriar Tajbakhsh Date: Tue, 9 Feb 2021 12:10:16 +0000 Subject: [PATCH 570/714] Avoid SQLAlchemyModelFactory name clash with a field named session Internal helpers `_create` and `_get_or_create` expected to receive the SQLAlchemy session as a keyword argument. Passing a value for a field named session as a keyword argument to a SQLAlchemyModelFactory subclass caused a name clash: TypeError: got multiple values for keyword argument 'session'. Fix #775 --- CREDITS | 1 + docs/changelog.rst | 4 ++++ factory/alchemy.py | 10 +++++----- tests/alchemyapp/models.py | 7 +++++++ tests/test_alchemy.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/CREDITS b/CREDITS index caa75380..b7ba1ab2 100644 --- a/CREDITS +++ b/CREDITS @@ -73,6 +73,7 @@ The project has received contributions from (in alphabetical order): * Samuel Paccoud * Saul Shanabrook * Sean Löfgren +* Shahriar Tajbakhsh * Tom * alex-netquity * anentropic diff --git a/docs/changelog.rst b/docs/changelog.rst index 1079f2a7..506183da 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,10 @@ ChangeLog - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. + - :issue:`775`: Change the signature for :meth:`~factory.alchemy.SQLAlchemyModelFactory._save` and + :meth:`~factory.alchemy.SQLAlchemyModelFactory._get_or_create` to avoid argument names clashes with a field named + ``session``. + 3.2.0 (2020-12-28) ------------------ diff --git a/factory/alchemy.py b/factory/alchemy.py index 2d248872..ef7a591a 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -51,7 +51,7 @@ def _generate(cls, strategy, params): return super()._generate(strategy, params) @classmethod - def _get_or_create(cls, model_class, session, *args, **kwargs): + def _get_or_create(cls, model_class, session, args, kwargs): key_fields = {} for field in cls._meta.sqlalchemy_get_or_create: if field not in kwargs: @@ -66,7 +66,7 @@ def _get_or_create(cls, model_class, session, *args, **kwargs): if not obj: try: - obj = cls._save(model_class, session, *args, **key_fields, **kwargs) + obj = cls._save(model_class, session, args, {**key_fields, **kwargs}) except IntegrityError as e: session.rollback() get_or_create_params = { @@ -95,11 +95,11 @@ def _create(cls, model_class, *args, **kwargs): if session is None: raise RuntimeError("No session provided.") if cls._meta.sqlalchemy_get_or_create: - return cls._get_or_create(model_class, session, *args, **kwargs) - return cls._save(model_class, session, *args, **kwargs) + return cls._get_or_create(model_class, session, args, kwargs) + return cls._save(model_class, session, args, kwargs) @classmethod - def _save(cls, model_class, session, *args, **kwargs): + def _save(cls, model_class, session, args, kwargs): session_persistence = cls._meta.sqlalchemy_session_persistence obj = model_class(*args, **kwargs) diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 013a56ae..8e76b5bd 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -43,4 +43,11 @@ class NonIntegerPk(Base): id = Column(Unicode(20), primary_key=True) +class SpecialFieldModel(Base): + __tablename__ = 'SpecialFieldModelTable' + + id = Column(Integer(), primary_key=True) + session = Column(Unicode(20)) + + Base.metadata.create_all(engine) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 2205ca1e..95e7f857 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -261,3 +261,34 @@ def test_build_does_not_raises_exception_when_no_session_was_set(self): inst1 = NoSessionFactory.build() self.assertEqual(inst0.id, 0) self.assertEqual(inst1.id, 1) + + +class NameConflictTests(unittest.TestCase): + """Regression test for `TypeError: _save() got multiple values for argument 'session'` + + See #775. + """ + def test_no_name_conflict_on_save(self): + class SpecialFieldWithSaveFactory(SQLAlchemyModelFactory): + class Meta: + model = models.SpecialFieldModel + sqlalchemy_session = models.session + + id = factory.Sequence(lambda n: n) + session = '' + + saved_child = SpecialFieldWithSaveFactory() + self.assertEqual(saved_child.session, "") + + def test_no_name_conflict_on_get_or_create(self): + class SpecialFieldWithGetOrCreateFactory(SQLAlchemyModelFactory): + class Meta: + model = models.SpecialFieldModel + sqlalchemy_get_or_create = ('session',) + sqlalchemy_session = models.session + + id = factory.Sequence(lambda n: n) + session = '' + + get_or_created_child = SpecialFieldWithGetOrCreateFactory() + self.assertEqual(get_or_created_child.session, "") From 00fc267791408acaa5cfe511b52ca954af9020b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sat, 24 Apr 2021 09:54:53 +0200 Subject: [PATCH 571/714] Add support for Django 3.2 Reviewed-by: rbarrois Reviewed-by: sarahboyce --- .github/workflows/test.yml | 1 + docs/changelog.rst | 4 ++++ setup.cfg | 1 + tox.ini | 4 +++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5da662f6..1777507b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: - django22-alchemy-mongoengine - django30-alchemy-mongoengine - django31-alchemy-mongoengine + - django32-alchemy-mongoengine services: mongodb: diff --git a/docs/changelog.rst b/docs/changelog.rst index 506183da..ca7c32cc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,10 @@ ChangeLog 3.2.1 (unreleased) ------------------ +*New:* + + - Add support for Django 3.2 + *Bugfix:* - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. diff --git a/setup.cfg b/setup.cfg index afbe5b0a..1c2b2f7c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Framework :: Django :: 2.2 Framework :: Django :: 3.0 Framework :: Django :: 3.1 + Framework :: Django :: 3.2 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index f8774aaa..ac79729b 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ envlist = py{36,37,38,39,py3}-django22-alchemy-mongoengine py{36,37,38,39,py3}-django30-alchemy-mongoengine py{36,37,38,39,py3}-django31-alchemy-mongoengine + py{36,37,38,39,py3}-django32-alchemy-mongoengine py39-djangomaster-alchemy-mongoengine docs examples @@ -17,8 +18,9 @@ deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 django31: Django>=3.1,<3.2 + django32: Django>=3.2,<3.3 djangomaster: https://github.com/django/django/archive/master.tar.gz - django{22,30,31,master}: Pillow + django{22,30,31,32,master}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 88458348b01c347e31cf1524a71482923d406a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 26 Oct 2021 16:02:25 +0200 Subject: [PATCH 572/714] Preparing release 3.2.1 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ca7c32cc..21be7484 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.2.1 (unreleased) +3.2.1 (2021-10-26) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index 1c2b2f7c..acd45e21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.2.1.dev0 +version = 3.2.1 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst author = Mark Sandstrom From 928fd2246c7d6d3c59463f3ce8b1f9f71a1f1ac7 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Tue, 15 Feb 2022 20:29:01 +0200 Subject: [PATCH 573/714] Drop support for EOL python 3.6, add support for python 3.10 and django-4.0 --- .github/workflows/test.yml | 23 +++++++++++++---------- README.rst | 2 +- docs/changelog.rst | 4 ++++ examples/django_demo/django_demo/urls.py | 6 +++++- setup.cfg | 6 +++--- tox.ini | 12 ++++++------ 6 files changed, 32 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b7c64fa..b8a89fb2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,19 +13,22 @@ jobs: fail-fast: false matrix: python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - pypy3 + - "3.8" + - "3.9" + - "3.10" + - "pypy-3.8" tox-environment: - - django22-alchemy-mongoengine - - django31-alchemy-mongoengine - django32-alchemy-mongoengine + - django40-alchemy-mongoengine include: - - python-version: 3.9 - tox-environment: djangomain-alchemy-mongoengine - + - python-version: "3.7" + tox-environment: django22-alchemy-mongoengine + - python-version: "pypy-3.7" + tox-environment: django22-alchemy-mongoengine + - python-version: "3.7" + tox-environment: django32-alchemy-mongoengine + - python-version: "pypy-3.7" + tox-environment: django32-alchemy-mongoengine services: mongodb: image: mongo diff --git a/README.rst b/README.rst index b039790c..625edf5d 100644 --- a/README.rst +++ b/README.rst @@ -401,7 +401,7 @@ To test with a specific framework version, you may use a ``tox`` target: $ tox --listenvs # run tests inside a specific environment - $ tox -e py36-django20-alchemy13-mongoengine017 + $ tox -e py310-djangomain-alchemy-mongoengine Valid options are: diff --git a/docs/changelog.rst b/docs/changelog.rst index d2045ab5..e45e4be1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,8 @@ ChangeLog - :issue:`366`: Add :class:`factory.django.Password` to generate Django :class:`~django.contrib.auth.models.User` passwords. - Add support for Django 3.2 + - Add support for Django 4.0 + - Add support for Python 3.10 *Bugfix:* @@ -43,6 +45,8 @@ ChangeLog *Removed:* - Drop support for Django 3.0 + - Drop support for Django 3.1 + - Drop support for Python 3.6 3.2.0 (2020-12-28) ------------------ diff --git a/examples/django_demo/django_demo/urls.py b/examples/django_demo/django_demo/urls.py index 2bf9b41a..667a5516 100644 --- a/examples/django_demo/django_demo/urls.py +++ b/examples/django_demo/django_demo/urls.py @@ -13,7 +13,11 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -from django.conf.urls import url +try: + # TODO: After dropping Django 2.2, switch to `path` + from django.urls import re_path as url +except ImportError: + from django.conf.urls import url from django.contrib import admin urlpatterns = [ diff --git a/setup.cfg b/setup.cfg index 8c4e502b..3ae6d65f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,18 +16,18 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 2.2 - Framework :: Django :: 3.1 Framework :: Django :: 3.2 + Framework :: Django :: 4.0 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing @@ -36,7 +36,7 @@ classifiers = [options] zip_safe = false packages = factory -python_requires = >=3.6 +python_requires = >=3.7 install_requires = Faker>=0.7.0 [options.extras_require] diff --git a/tox.ini b/tox.ini index 90dfb0fc..0ce0d706 100644 --- a/tox.ini +++ b/tox.ini @@ -2,10 +2,10 @@ minversion = 1.9 envlist = lint - py{36,37,38,39,py3}-django22-alchemy-mongoengine - py{36,37,38,39,py3}-django31-alchemy-mongoengine - py{36,37,38,39,py3}-django32-alchemy-mongoengine - py39-djangomaster-alchemy-mongoengine + py{37,38,39,py3}-django22-alchemy-mongoengine + py{37,38,39,310,py3}-django32-alchemy-mongoengine + py{38,39,310,py3}-django40-alchemy-mongoengine + py310-djangomain-alchemy-mongoengine docs examples linkcheck @@ -15,10 +15,10 @@ toxworkdir = {env:TOX_WORKDIR:.tox} [testenv] deps = django22: Django>=2.2,<2.3 - django31: Django>=3.1,<3.2 django32: Django>=3.2,<3.3 + django40: Django>=4.0,<4.1 djangomain: https://github.com/django/django/archive/main.tar.gz - django{22,31,32,main}: Pillow + django{22,32,40,main}: Pillow alchemy: SQLAlchemy mongoengine: mongoengine From 669aa84daa296c55bf76586d7085f8e466bb52a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 15 Feb 2022 11:19:41 +0100 Subject: [PATCH 574/714] Fix documentation build warning about extlinks Use extlinks Sphinx extension instead of directly linking to the GitHub issue. --- docs/fuzzy.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index dbcd9fdf..898bae85 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -6,8 +6,7 @@ Fuzzy attributes .. note:: Now that FactoryBoy includes the :class:`factory.Faker` class, most of these built-in fuzzers are deprecated in favor of their `Faker `_ equivalents. Further - discussion here: - ``_ + discussion in :issue:`271`. Some tests may be interested in testing with fuzzy, random values. From d2447abbbf4da99d9e76909b8657524cf8e1c48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 6 May 2022 10:49:08 +0200 Subject: [PATCH 575/714] Specify uuidRepresentation for mongoengine https://github.com/MongoEngine/mongoengine/blob/master/docs/changelog.rst#changes-in-0240: > UUIDs are encoded with the pythonLegacy encoding by default instead of the newer and cross platform standard encoding. Existing UUIDs will need to be migrated before changing the encoding, and this should be done explicitly by the user rather than switching to a new default by MongoEngine. This default will change at a later date, but to allow specifying and then migrating to the new format a default json_options has been provided. Co-authored-by: Javier Buzzi --- tests/test_mongoengine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index bc1915b0..44f1e364 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -57,6 +57,7 @@ def setUpClass(cls): read_preference=mongo_rp.ReadPreference.PRIMARY, # PyMongo>=2.1 has a 20s timeout, use 100ms instead serverselectiontimeoutms=cls.server_timeout_ms, + uuidRepresentation='standard', ) @classmethod From cd10bb952f4f3b5cfb19fb6397313d1f805fbb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Wed, 11 May 2022 14:55:50 +0200 Subject: [PATCH 576/714] Improve doc of "Choosing from a populated table" models.Language.objects.all() does gets evaluated at runtime but that returns a QuerySet, django only hit the database when we start iterating in the queryset. Previous documentation was a bit misleading. I would find more intuitive to recommend to use `language = factory.Iterator(models.Language.objects.all)` but I havn't changed that here, let me know if you prefer that I change it that way --- docs/recipes.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 6afa9d1a..93ed77b4 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -52,8 +52,9 @@ simply use a :class:`factory.Iterator` on the chosen queryset: language = factory.Iterator(models.Language.objects.all()) -Here, ``models.Language.objects.all()`` won't be evaluated until the -first call to ``UserFactory``; thus avoiding DB queries at import time. +Here, ``models.Language.objects.all()`` is a QuerySet and will only hit the database +when factory_boy will start iterating on it, i.e on the first call to ``UserFactory``; +thus avoiding DB queries at import time. Reverse dependencies (reverse ForeignKey) From 31e8a41827aea805e0475f4e6aceaa19d84cdaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bastien=20G=C3=A9rard?= Date: Fri, 13 May 2022 11:51:10 +0200 Subject: [PATCH 577/714] Update docs/recipes.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Freitag --- docs/recipes.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/recipes.rst b/docs/recipes.rst index 93ed77b4..a1717ad5 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -52,9 +52,10 @@ simply use a :class:`factory.Iterator` on the chosen queryset: language = factory.Iterator(models.Language.objects.all()) -Here, ``models.Language.objects.all()`` is a QuerySet and will only hit the database -when factory_boy will start iterating on it, i.e on the first call to ``UserFactory``; -thus avoiding DB queries at import time. +Here, ``models.Language.objects.all()`` is a +:class:`~django.db.models.query.QuerySet` and will only hit the database when +``factory_boy`` starts iterating on it, i.e on the first call to +``UserFactory``; thus avoiding DB queries at import time. Reverse dependencies (reverse ForeignKey) From ed107a8b399d2a58b1bc67a924b16e5ecae258ef Mon Sep 17 00:00:00 2001 From: Hugo Pellissari Pavan Date: Fri, 13 May 2022 13:40:07 -0300 Subject: [PATCH 578/714] Support creating SQLAlchemy sessions from a callable Allows setting the database session through the sqlalchemy_session_factory attribute, receiving a Callable that returns a sqlalchemy.orm.Session instance. This ensures better control to dynamically set the Session in the factory. --- docs/changelog.rst | 2 ++ docs/orms.rst | 19 +++++++++++++++++++ factory/alchemy.py | 12 ++++++++++++ tests/test_alchemy.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index e45e4be1..34f3cac0 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,8 @@ ChangeLog - :issue:`366`: Add :class:`factory.django.Password` to generate Django :class:`~django.contrib.auth.models.User` passwords. + - :issue:`304`: Add :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session_factory` to dynamically + create sessions for use by the :class:`~factory.alchemy.SQLAlchemyModelFactory`. - Add support for Django 3.2 - Add support for Django 4.0 - Add support for Python 3.10 diff --git a/docs/orms.rst b/docs/orms.rst index e464fc4e..3d255a31 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -369,6 +369,25 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: SQLAlchemy session to use to communicate with the database when creating an object through this :class:`SQLAlchemyModelFactory`. + .. attribute:: sqlalchemy_session_factory + + .. versionadded:: 3.3.0 + + :class:`~collections.abc.Callable` returning a :class:`~sqlalchemy.orm.Session` instance to use to communicate + with the database. You can either provide the session through this attribute, or through + :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session`, but not both at the same time. + + .. code-block:: python + + from . import common + + class UserFactory(factory.alchemy.SQLAlchemyModelFactory): + class Meta: + model = User + sqlalchemy_session_factory = lambda: common.Session() + + username = 'john' + .. attribute:: sqlalchemy_session_persistence Control the action taken by ``sqlalchemy_session`` at the end of a create call. diff --git a/factory/alchemy.py b/factory/alchemy.py index ef7a591a..cf20b537 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -22,10 +22,18 @@ def _check_sqlalchemy_session_persistence(self, meta, value): (meta, VALID_SESSION_PERSISTENCE_TYPES, value) ) + @staticmethod + def _check_has_sqlalchemy_session_set(meta, value): + if value and meta.sqlalchemy_session: + raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both") + def _build_default_options(self): return super()._build_default_options() + [ base.OptionDefault('sqlalchemy_get_or_create', (), inherit=True), base.OptionDefault('sqlalchemy_session', None, inherit=True), + base.OptionDefault( + 'sqlalchemy_session_factory', None, inherit=True, checker=self._check_has_sqlalchemy_session_set + ), base.OptionDefault( 'sqlalchemy_session_persistence', None, @@ -90,6 +98,10 @@ def _get_or_create(cls, model_class, session, args, kwargs): @classmethod def _create(cls, model_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" + session_factory = cls._meta.sqlalchemy_session_factory + if session_factory: + cls._meta.sqlalchemy_session = session_factory() + session = cls._meta.sqlalchemy_session if session is None: diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 03410838..005fb0fa 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -264,6 +264,34 @@ def test_build_does_not_raises_exception_when_no_session_was_set(self): self.assertEqual(inst1.id, 1) +class SQLAlchemySessionFactoryTestCase(unittest.TestCase): + + def test_create_get_session_from_sqlalchemy_session_factory(self): + class SessionGetterFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_session = None + sqlalchemy_session_factory = lambda: models.session + + id = factory.Sequence(lambda n: n) + + SessionGetterFactory.create() + self.assertEqual(SessionGetterFactory._meta.sqlalchemy_session, models.session) + # Reuse the session obtained from sqlalchemy_session_factory. + SessionGetterFactory.create() + + def test_create_raise_exception_sqlalchemy_session_factory_not_callable(self): + message = "^Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both$" + with self.assertRaisesRegex(RuntimeError, message): + class SessionAndGetterFactory(SQLAlchemyModelFactory): + class Meta: + model = models.StandardModel + sqlalchemy_session = models.session + sqlalchemy_session_factory = lambda: models.session + + id = factory.Sequence(lambda n: n) + + class NameConflictTests(unittest.TestCase): """Regression test for `TypeError: _save() got multiple values for argument 'session'` From cf298493adfeb269151dce1adc84cbc8e3065985 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 16 May 2022 12:00:59 +0200 Subject: [PATCH 579/714] Docs: Import factory after mangling sys.path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sphinx conf.py mangles sys.path to add the top-level directory, so that factory_boy is available. Don’t import the package before the path has been updated. --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 43e63fff..bd57ca4b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,13 +9,13 @@ import os import sys -import factory - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.dirname(os.path.abspath('.'))) +# Must be imported after the parent directory was added to sys.path for global sphinx installation. +import factory # noqa # -- Project information ----------------------------------------------------- From 92816251872848524d247fb3d7e65abe7cbe7151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 30 May 2022 12:00:04 +0200 Subject: [PATCH 580/714] docs: Use %s placeholder for extlinks https://www.sphinx-doc.org/en/master/usage/extensions/extlinks.html#confval-extlinks --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index bd57ca4b..f636c01b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,8 +41,8 @@ ] extlinks = { - 'issue': ('https://github.com/FactoryBoy/factory_boy/issues/%s', 'issue #'), - 'pr': ('https://github.com/FactoryBoy/factory_boy/pull/%s', 'pull request #'), + 'issue': ('https://github.com/FactoryBoy/factory_boy/issues/%s', 'issue %s'), + 'pr': ('https://github.com/FactoryBoy/factory_boy/pull/%s', 'pull request %s'), } # Add any paths that contain templates here, relative to this directory. From 8130d28401a1b29ca6f81ab580f3f5a04ce2ea78 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 25 May 2022 12:34:24 +0300 Subject: [PATCH 581/714] Refactor FileField. This is a minor refactor done for performance optimization. --- factory/django.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/factory/django.py b/factory/django.py index 7e5427fe..7989f33a 100644 --- a/factory/django.py +++ b/factory/django.py @@ -202,26 +202,29 @@ def _make_data(self, params): def _make_content(self, params): path = '' + + from_path = params.get('from_path') + from_file = params.get('from_file') + from_func = params.get('from_func') - _content_params = [params.get('from_path'), params.get('from_file'), params.get('from_func')] - if len([p for p in _content_params if p]) > 1: + if sum(bool(p) for p in (from_path, from_file, from_func)) > 1: raise ValueError( "At most one argument from 'from_file', 'from_path', and 'from_func' should " "be non-empty when calling factory.django.FileField." ) - if params.get('from_path'): - path = params['from_path'] + if from_path: + path = from_path with open(path, 'rb') as f: content = django_files.base.ContentFile(f.read()) - elif params.get('from_file'): - f = params['from_file'] + elif from_file: + f = from_file content = django_files.File(f) path = content.name - elif params.get('from_func'): - func = params['from_func'] + elif from_func: + func = from_func content = django_files.File(func()) path = content.name From 288b35e7a065d779e01eeb21c46a5263f97ae00f Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Sun, 29 May 2022 15:22:38 +0300 Subject: [PATCH 582/714] Address code review. Co-authored-by: Javier Buzzi --- factory/django.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/django.py b/factory/django.py index 7989f33a..b4be9920 100644 --- a/factory/django.py +++ b/factory/django.py @@ -207,7 +207,7 @@ def _make_content(self, params): from_file = params.get('from_file') from_func = params.get('from_func') - if sum(bool(p) for p in (from_path, from_file, from_func)) > 1: + if len([p for p in (from_path, from_file, from_func) if p]) > 1: raise ValueError( "At most one argument from 'from_file', 'from_path', and 'from_func' should " "be non-empty when calling factory.django.FileField." From 7541994f2f68b68fbca8d5d434e7f0111cb915b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 30 May 2022 11:54:08 +0200 Subject: [PATCH 583/714] Remove trailing whitespace --- factory/django.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/django.py b/factory/django.py index b4be9920..23b93425 100644 --- a/factory/django.py +++ b/factory/django.py @@ -202,7 +202,7 @@ def _make_data(self, params): def _make_content(self, params): path = '' - + from_path = params.get('from_path') from_file = params.get('from_file') from_func = params.get('from_func') From 0999e78db2a6149d1ff2d407f96f5e0ee0743502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 31 May 2022 17:05:26 +0200 Subject: [PATCH 584/714] Factorize out SQLAlchemy transaction management --- tests/test_alchemy.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 005fb0fa..d52c4af9 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -72,11 +72,15 @@ class Meta: text = factory.Sequence(lambda n: "text%s" % n) -class SQLAlchemyPkSequenceTestCase(unittest.TestCase): +class TransactionTestCase(unittest.TestCase): + def tearDown(self): + models.session.rollback() + + +class SQLAlchemyPkSequenceTestCase(TransactionTestCase): def setUp(self): super().setUp() StandardFactory.reset_sequence(1) - NonIntegerPkFactory._meta.sqlalchemy_session.rollback() def test_pk_first(self): std = StandardFactory.build() @@ -109,10 +113,7 @@ def test_pk_force_value(self): self.assertEqual(0, std2.id) -class SQLAlchemyGetOrCreateTests(unittest.TestCase): - def setUp(self): - models.session.rollback() - +class SQLAlchemyGetOrCreateTests(TransactionTestCase): def test_simple_call(self): obj1 = WithGetOrCreateFieldFactory(foo='foo1') obj2 = WithGetOrCreateFieldFactory(foo='foo1') @@ -144,10 +145,7 @@ def test_multicall(self): ) -class MultipleGetOrCreateFieldsTest(unittest.TestCase): - def setUp(self): - models.session.rollback() - +class MultipleGetOrCreateFieldsTest(TransactionTestCase): def test_one_defined(self): obj1 = WithMultipleGetOrCreateFieldsFactory() obj2 = WithMultipleGetOrCreateFieldsFactory(slug=obj1.slug) @@ -216,11 +214,10 @@ class Meta: model = models.StandardModel -class SQLAlchemyNonIntegerPkTestCase(unittest.TestCase): - def setUp(self): - super().setUp() +class SQLAlchemyNonIntegerPkTestCase(TransactionTestCase): + def tearDown(self): + super().tearDown() NonIntegerPkFactory.reset_sequence() - NonIntegerPkFactory._meta.sqlalchemy_session.rollback() def test_first(self): nonint = NonIntegerPkFactory.build() @@ -250,7 +247,7 @@ def test_force_pk(self): self.assertEqual('foo0', nonint2.id) -class SQLAlchemyNoSessionTestCase(unittest.TestCase): +class SQLAlchemyNoSessionTestCase(TransactionTestCase): def test_create_raises_exception_when_no_session_was_set(self): with self.assertRaises(RuntimeError): @@ -264,8 +261,7 @@ def test_build_does_not_raises_exception_when_no_session_was_set(self): self.assertEqual(inst1.id, 1) -class SQLAlchemySessionFactoryTestCase(unittest.TestCase): - +class SQLAlchemySessionFactoryTestCase(TransactionTestCase): def test_create_get_session_from_sqlalchemy_session_factory(self): class SessionGetterFactory(SQLAlchemyModelFactory): class Meta: @@ -292,7 +288,7 @@ class Meta: id = factory.Sequence(lambda n: n) -class NameConflictTests(unittest.TestCase): +class NameConflictTests(TransactionTestCase): """Regression test for `TypeError: _save() got multiple values for argument 'session'` See #775. From 299634a89256c3be85a933e821427856b5bf2305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 1 Jun 2022 11:39:13 +0200 Subject: [PATCH 585/714] Clear SQLAlchemy tables after each test Not as efficient as wrapping the test in a transaction, but allows to reset sequences and remove all table data, so that each test starts from a clean slate. --- tests/alchemyapp/models.py | 3 --- tests/test_alchemy.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 8e76b5bd..6c1b080a 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -48,6 +48,3 @@ class SpecialFieldModel(Base): id = Column(Integer(), primary_key=True) session = Column(Unicode(20)) - - -Base.metadata.create_all(engine) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index d52c4af9..2463ca1b 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -73,8 +73,12 @@ class Meta: class TransactionTestCase(unittest.TestCase): + def setUp(self): + models.Base.metadata.create_all(models.engine) + def tearDown(self): models.session.rollback() + models.Base.metadata.drop_all(models.engine) class SQLAlchemyPkSequenceTestCase(TransactionTestCase): From f31773b7705719012c212e860f1d23e66a1bd01a Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 2 Jun 2022 06:35:25 -0400 Subject: [PATCH 586/714] Squash history - rebase with remote/master --- .github/workflows/test.yml | 44 ++++++++++++++++++++++++++------------ Makefile | 2 ++ tests/alchemyapp/models.py | 27 ++++++++++++++++++++++- tests/djapp/models.py | 2 +- tests/djapp/settings_pg.py | 41 +++++++++++++++++++++++++++++++++++ tests/test_alchemy.py | 26 +++++++++++++++++++++- tests/test_django.py | 24 +++++++++++++++++++-- tox.ini | 43 +++++++++++++++++++++++++++++++------ 8 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 tests/djapp/settings_pg.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8a89fb2..1c540d0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,35 +6,48 @@ on: jobs: build: - name: Python ${{ matrix.python-version }} / ${{ matrix.tox-environment }} + name: Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}, Database ${{ matrix.database-type }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: + - "3.7" - "3.8" - "3.9" - "3.10" + - "pypy-3.7" - "pypy-3.8" - tox-environment: - - django32-alchemy-mongoengine - - django40-alchemy-mongoengine - include: - - python-version: "3.7" - tox-environment: django22-alchemy-mongoengine - - python-version: "pypy-3.7" - tox-environment: django22-alchemy-mongoengine - - python-version: "3.7" - tox-environment: django32-alchemy-mongoengine - - python-version: "pypy-3.7" - tox-environment: django32-alchemy-mongoengine + django-version: + - "2.2" + - "3.2" + - "4.0" + - "main" + database-type: + - "sqlite" + - "postgres" + exclude: + - { django-version: "2.2", python-version: "3.9" } + - { django-version: "2.2", python-version: "3.10" } + - { django-version: "4.0", python-version: "pypy-3.7" } + - { django-version: "4.0", python-version: "3.7" } + - { django-version: "main", python-version: "pypy-3.7" } + - { django-version: "main", python-version: "3.7" } + services: mongodb: image: mongo ports: - 27017:27017 + postgresdb: + image: postgres:alpine + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: password + env: TOXENV: ${{ matrix.tox-environment }} @@ -47,7 +60,10 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: python -m pip install tox + run: python -m pip install tox-gh-actions - name: Run tests run: tox + env: + DJANGO: ${{ matrix.django-version }} + DATABASE_TYPE: ${{ matrix.database-type }} diff --git a/Makefile b/Makefile index 9474bae5..93e36997 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,9 @@ test: -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ + -Wdefault:"distutils Version classes are deprecated. Use packaging.version instead":DeprecationWarning:: \ -m unittest +# TODO: Remove "distutils Version classes are deprecated" when django 2.2 is dropped # DOC: Test the examples example-test: diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 6c1b080a..2ed443dc 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -2,13 +2,38 @@ """Helpers for testing SQLAlchemy apps.""" +import os from sqlalchemy import Column, Integer, Unicode, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker +try: + import psycopg2 # noqa: F401 + USING_POSTGRES = True +except ImportError: + try: + # pypy does not support `psycopg2` or `psycopg2-binary` + # This is a package that only gets installed with pypy, and it needs to be + # initialized for it to work properly. It mimic `psycopg2` 1-to-1 + from psycopg2cffi import compat + compat.register() + USING_POSTGRES = True + except ImportError: + USING_POSTGRES = False + +if USING_POSTGRES: + pg_database = 'alch_' + os.environ.get('POSTGRES_DATABASE', 'factory_boy_test') + pg_user = os.environ.get('POSTGRES_USER', 'postgres') + pg_password = os.environ.get('POSTGRES_PASSWORD', 'password') + pg_host = os.environ.get('POSTGRES_HOST', 'localhost') + pg_port = os.environ.get('POSTGRES_PORT', '5432') + engine_name = f'postgresql+psycopg2://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_database}' +else: + engine_name = 'sqlite://' + session = scoped_session(sessionmaker()) -engine = create_engine('sqlite://') +engine = create_engine(engine_name) session.configure(bind=engine) Base = declarative_base() diff --git a/tests/djapp/models.py b/tests/djapp/models.py index fb34e907..3ac731b2 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -25,7 +25,7 @@ class NonIntegerPk(models.Model): class MultifieldModel(models.Model): slug = models.SlugField(max_length=20, unique=True) - text = models.CharField(max_length=20) + text = models.CharField(max_length=200) class MultifieldUniqueModel(models.Model): diff --git a/tests/djapp/settings_pg.py b/tests/djapp/settings_pg.py new file mode 100644 index 00000000..c0d9353f --- /dev/null +++ b/tests/djapp/settings_pg.py @@ -0,0 +1,41 @@ +# Copyright: See the LICENSE file. + +"""Settings for factory_boy/Django tests.""" + +import os + +from .settings import * # noqa: F401, F403 + +try: + # pypy does not support `psycopg2` or `psycopg2-binary` + # This is a package that only gets installed with pypy, and it needs to be + # initialized for it to work properly. It mimic `psycopg2` 1-to-1 + from psycopg2cffi import compat + compat.register() +except ImportError: + pass + +postgres_user = os.getenv('POSTGRES_USER', 'postgres') +postgres_name = os.environ.get('POSTGRES_DATABASE', 'factory_boy_test') +postgres_password = os.getenv('POSTGRES_PASSWORD', 'password') +postgres_host = os.getenv('POSTGRES_HOST', 'localhost') +postgres_port = os.getenv('POSTGRES_PORT', '5432') + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': postgres_name, + 'USER': postgres_user, + 'PASSWORD': postgres_password, + 'HOST': postgres_host, + 'PORT': postgres_port, + }, + 'replica': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'NAME': postgres_name + '_rp', + 'USER': postgres_user, + 'PASSWORD': postgres_password, + 'HOST': postgres_host, + 'PORT': postgres_port, + } +} diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 2463ca1b..c56d13c3 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -13,6 +13,30 @@ from .alchemyapp import models +def setUpModule(): + if models.USING_POSTGRES: + engine_name = (f'postgresql+psycopg2://{models.pg_user}:{models.pg_password}' + f'@{models.pg_host}:{models.pg_port}/postgres') + template_engine = models.create_engine(engine_name, echo=False) + + conn = template_engine.connect() + conn.execute("ROLLBACK") + try: + conn.execute(f"DROP DATABASE {models.pg_database}") + except sqlalchemy.exc.ProgrammingError: + # Could not drop the database, probably does not exist + conn.execute("ROLLBACK") + except sqlalchemy.exc.OperationalError: + # Could not drop database because it's being accessed by other users (psql prompt open?) + conn.execute("ROLLBACK") + + conn.execute("ROLLBACK") + conn.execute(f"CREATE DATABASE {models.pg_database}") + conn.close() + + template_engine.dispose() + + class StandardFactory(SQLAlchemyModelFactory): class Meta: model = models.StandardModel @@ -143,7 +167,7 @@ def test_multicall(self): list( obj.slug for obj in models.session.query( models.MultiFieldModel.slug - ) + ).order_by(models.MultiFieldModel.slug) ), ["alt", "main"], ) diff --git a/tests/test_django.py b/tests/test_django.py index 48b94329..0cce501a 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -11,8 +11,11 @@ from django import test as django_test from django.conf import settings from django.contrib.auth.hashers import check_password +from django.core.management import color +from django.db import connections from django.db.models import signals from django.test import utils as django_test_utils +from faker import Factory as FakerFactory import factory.django @@ -23,6 +26,8 @@ except ImportError: Image = None +faker = FakerFactory.create() + # Setup Django before importing Django models. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') django.setup() @@ -164,7 +169,16 @@ class Meta: self.assertEqual(obj, models.StandardModel.objects.using('replica').get()) -class DjangoPkSequenceTestCase(django_test.TestCase): +class DjangoResetTestCase(django_test.TestCase): + def reset_database_sequences(self, *models): + using = factory.django.DEFAULT_DB_ALIAS + with connections[using].cursor() as cursor: + sequence_sql = connections[using].ops.sequence_reset_sql(color.no_style(), models) + for command in sequence_sql: + cursor.execute(command) + + +class DjangoPkSequenceTestCase(DjangoResetTestCase): def setUp(self): super().setUp() StandardFactory.reset_sequence() @@ -180,6 +194,8 @@ def test_pk_many(self): self.assertEqual('foo1', std2.foo) def test_pk_creation(self): + self.reset_database_sequences(StandardFactory._meta.model) + std1 = StandardFactory.create() self.assertEqual('foo0', std1.foo) self.assertEqual(1, std1.pk) @@ -194,6 +210,8 @@ def test_pk_force_value(self): self.assertEqual('foo0', std1.foo) # sequence is unrelated to pk self.assertEqual(10, std1.pk) + self.reset_database_sequences(StandardFactory._meta.model) + StandardFactory.reset_sequence() std2 = StandardFactory.create() self.assertEqual('foo0', std2.foo) @@ -374,7 +392,7 @@ def test_force_pk(self): self.assertEqual('foo0', nonint2.pk) -class DjangoAbstractBaseSequenceTestCase(django_test.TestCase): +class DjangoAbstractBaseSequenceTestCase(DjangoResetTestCase): def test_auto_sequence_son(self): """The sequence of the concrete son of an abstract model should be autonomous.""" obj = ConcreteSonFactory() @@ -397,6 +415,8 @@ class ConcreteSonFactory(AbstractBaseFactory): class Meta: model = models.ConcreteSon + self.reset_database_sequences(models.ConcreteSon) + obj = ConcreteSonFactory() self.assertEqual(1, obj.pk) self.assertEqual("foo0", obj.foo) diff --git a/tox.ini b/tox.ini index 0ce0d706..d5725014 100644 --- a/tox.ini +++ b/tox.ini @@ -2,25 +2,54 @@ minversion = 1.9 envlist = lint - py{37,38,39,py3}-django22-alchemy-mongoengine - py{37,38,39,310,py3}-django32-alchemy-mongoengine - py{38,39,310,py3}-django40-alchemy-mongoengine - py310-djangomain-alchemy-mongoengine + py{37,38,39,py37,py38}-django22-{sqlite,postgres} + py{37,38,39,310,py37,py38}-django32-{sqlite,postgres} + py{38,39,310,py38}-django40-{sqlite,postgres} + py310-djangomain-{sqlite,postgres} docs examples linkcheck toxworkdir = {env:TOX_WORKDIR:.tox} +[gh-actions] +python = + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + pypy-3.7: pypy37 + pypy-3.8: pypy38 + +[gh-actions:env] +DJANGO = + 2.2: django22 + 3.2: django32 + 4.0: django40 + main: djangomain +DATABASE_TYPE = + sqlite: sqlite + postgres: postgres + [testenv] +passenv = + MONGO_HOST + POSTGRES_HOST + POSTGRES_DATABASE deps = + Pillow + SQLAlchemy + mongoengine django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 djangomain: https://github.com/django/django/archive/main.tar.gz - django{22,32,40,main}: Pillow - alchemy: SQLAlchemy - mongoengine: mongoengine + py{37,38,39,310}-django{22,32,40,main}-postgres: psycopg2-binary + py{py37,py38}-django{22,32,40,main}-postgres: psycopg2cffi + +setenv = + py: DJANGO_SETTINGS_MODULE=tests.djapp.settings + postgres: DJANGO_SETTINGS_MODULE=tests.djapp.settings_pg whitelist_externals = make commands = make test From 0aab1d4c69a564847d786aaf414484921b361617 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 2 Jun 2022 13:50:39 -0400 Subject: [PATCH 587/714] Not used anymore. --- tests/test_django.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index 0cce501a..2401dae6 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -15,7 +15,6 @@ from django.db import connections from django.db.models import signals from django.test import utils as django_test_utils -from faker import Factory as FakerFactory import factory.django @@ -26,8 +25,6 @@ except ImportError: Image = None -faker = FakerFactory.create() - # Setup Django before importing Django models. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.djapp.settings') django.setup() From 57e1c9238d3caa8c71b8467d5c317876cd2c785d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 3 Jun 2022 12:06:21 +0200 Subject: [PATCH 588/714] Use a TextField for MultifieldModel The length of the text is unknown. --- tests/djapp/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 3ac731b2..088008e5 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -25,7 +25,7 @@ class NonIntegerPk(models.Model): class MultifieldModel(models.Model): slug = models.SlugField(max_length=20, unique=True) - text = models.CharField(max_length=200) + text = models.TextField() class MultifieldUniqueModel(models.Model): From 8b1cbdb08a07b1f03b4041652c44d894692011f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 3 Jun 2022 12:06:40 +0200 Subject: [PATCH 589/714] Unify to os.environ.get --- tests/djapp/settings_pg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/djapp/settings_pg.py b/tests/djapp/settings_pg.py index c0d9353f..a76a7510 100644 --- a/tests/djapp/settings_pg.py +++ b/tests/djapp/settings_pg.py @@ -15,11 +15,11 @@ except ImportError: pass -postgres_user = os.getenv('POSTGRES_USER', 'postgres') +postgres_user = os.environ.get('POSTGRES_USER', 'postgres') postgres_name = os.environ.get('POSTGRES_DATABASE', 'factory_boy_test') -postgres_password = os.getenv('POSTGRES_PASSWORD', 'password') -postgres_host = os.getenv('POSTGRES_HOST', 'localhost') -postgres_port = os.getenv('POSTGRES_PORT', '5432') +postgres_password = os.environ.get('POSTGRES_PASSWORD', 'password') +postgres_host = os.environ.get('POSTGRES_HOST', 'localhost') +postgres_port = os.environ.get('POSTGRES_PORT', '5432') DATABASES = { 'default': { From 6d8152c3d47c0d69238c925afecf0cc47c80ad4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 3 Jun 2022 12:07:27 +0200 Subject: [PATCH 590/714] Missed django.db.backends.postgresql_psycopg2 --- tests/djapp/settings_pg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/djapp/settings_pg.py b/tests/djapp/settings_pg.py index a76a7510..de54922c 100644 --- a/tests/djapp/settings_pg.py +++ b/tests/djapp/settings_pg.py @@ -31,7 +31,7 @@ 'PORT': postgres_port, }, 'replica': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'NAME': postgres_name + '_rp', 'USER': postgres_user, 'PASSWORD': postgres_password, From 61f13f314f17daa4cf1fea28b740a3c710b14542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 3 Jun 2022 12:08:24 +0200 Subject: [PATCH 591/714] Remove sqlalchemy setUpModule --- tests/test_alchemy.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index c56d13c3..54ae3179 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -13,30 +13,6 @@ from .alchemyapp import models -def setUpModule(): - if models.USING_POSTGRES: - engine_name = (f'postgresql+psycopg2://{models.pg_user}:{models.pg_password}' - f'@{models.pg_host}:{models.pg_port}/postgres') - template_engine = models.create_engine(engine_name, echo=False) - - conn = template_engine.connect() - conn.execute("ROLLBACK") - try: - conn.execute(f"DROP DATABASE {models.pg_database}") - except sqlalchemy.exc.ProgrammingError: - # Could not drop the database, probably does not exist - conn.execute("ROLLBACK") - except sqlalchemy.exc.OperationalError: - # Could not drop database because it's being accessed by other users (psql prompt open?) - conn.execute("ROLLBACK") - - conn.execute("ROLLBACK") - conn.execute(f"CREATE DATABASE {models.pg_database}") - conn.close() - - template_engine.dispose() - - class StandardFactory(SQLAlchemyModelFactory): class Meta: model = models.StandardModel From 869e1b0bb104a6881f50117c961031f8a34b91a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 3 Jun 2022 12:21:30 +0200 Subject: [PATCH 592/714] Prepare database for SQLAlchemy tests --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1c540d0b..db6fd269 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -47,6 +47,7 @@ jobs: - 5432:5432 env: POSTGRES_PASSWORD: password + POSTGRES_DB: alch_factory_boy_test env: TOXENV: ${{ matrix.tox-environment }} From 46a1d2962d329850cf259771aefb4e0ca1e208d1 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 3 Jun 2022 10:26:23 -0400 Subject: [PATCH 593/714] Rebase remote/master --- .github/workflows/test.yml | 57 +++++++++++++++++++++++++++++++++++++- README.rst | 20 ++++++------- tests/test_alchemy.py | 5 +++- tests/test_django.py | 19 +++++++------ tests/test_mongoengine.py | 9 +++--- tests/test_using.py | 13 +++++++++ tox.ini | 42 ++++++++++++++++++++-------- 7 files changed, 126 insertions(+), 39 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index db6fd269..69289ab5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,8 +4,12 @@ on: - push - pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: - build: + django: name: Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}, Database ${{ matrix.database-type }} runs-on: ubuntu-latest @@ -68,3 +72,54 @@ jobs: env: DJANGO: ${{ matrix.django-version }} DATABASE_TYPE: ${{ matrix.database-type }} + + others: + name: Python ${{ matrix.python-version[0] }}, ${{ matrix.mode-type }}, Database ${{ matrix.database-type }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: + - ["3.7", "py37"] + - ["3.8", "py38"] + - ["3.9", "py39"] + - ["3.10", "py310"] + - ["pypy-3.7", "pypy37"] + - ["pypy-3.8", "pypy38"] + database-type: + - "sqlite" + - "postgres" + - "mongo" + mode-type: + - "alchemy" + - "mongo" + - "none" + + services: + mongodb: + image: mongo + ports: + - 27017:27017 + + postgresdb: + image: postgres:alpine + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: password + POSTGRES_DB: alch_factory_boy_test + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version[0] }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version[0] }} + + - name: Install dependencies + run: python -m pip install tox-gh-actions + + - name: Run tests + run: tox -e ${{ matrix.python-version[1] }}-${{ matrix.mode-type }}-${{ matrix.database-type }} diff --git a/README.rst b/README.rst index 625edf5d..d933d14d 100644 --- a/README.rst +++ b/README.rst @@ -400,21 +400,17 @@ To test with a specific framework version, you may use a ``tox`` target: # list all tox environments $ tox --listenvs - # run tests inside a specific environment - $ tox -e py310-djangomain-alchemy-mongoengine + # run tests inside a specific environment (django/mongoengine/SQLAlchemy are not installed) + $ tox -e py310-none-sqlite -Valid options are: + # run tests inside a specific environment (django) + $ tox -e py310-djangomain-sqlite -* ``DJANGO`` for ``Django`` -* ``MONGOENGINE`` for ``mongoengine`` -* ``ALCHEMY`` for ``SQLAlchemy`` + # run tests inside a specific environment (alchemy) + $ tox -e py310-alchemy-postgres - -To avoid running ``mongoengine`` tests (e.g no MongoDB server installed), run: - -.. code-block:: sh - - $ make SKIP_MONGOENGINE=1 test + # run tests inside a specific environment (mongoengine) + $ tox -e py310-mongo-sqlite Packaging diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 54ae3179..3fde59ed 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -5,7 +5,10 @@ import unittest from unittest import mock -import sqlalchemy +try: + import sqlalchemy +except ImportError: + raise unittest.SkipTest("sqlalchemy tests disabled.") import factory from factory.alchemy import SQLAlchemyModelFactory diff --git a/tests/test_django.py b/tests/test_django.py index 2401dae6..ac3f6fb9 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -7,14 +7,17 @@ import unittest from unittest import mock -import django -from django import test as django_test -from django.conf import settings -from django.contrib.auth.hashers import check_password -from django.core.management import color -from django.db import connections -from django.db.models import signals -from django.test import utils as django_test_utils +try: + import django + from django import test as django_test + from django.conf import settings + from django.contrib.auth.hashers import check_password + from django.core.management import color + from django.db import connections + from django.db.models import signals + from django.test import utils as django_test_utils +except ImportError: + raise unittest.SkipTest("django tests disabled.") import factory.django diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index 44f1e364..bc930fca 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -5,7 +5,10 @@ import os import unittest -import mongoengine +try: + import mongoengine +except ImportError: + raise unittest.SkipTest("mongodb tests disabled.") import factory from factory.mongoengine import MongoEngineFactory @@ -35,10 +38,6 @@ class Meta: address = factory.SubFactory(AddressFactory) -SKIP_MONGODB = bool(os.environ.get('SKIP_MONGOENGINE') == '1') - - -@unittest.skipIf(SKIP_MONGODB, "mongodb tests disabled.") class MongoEngineTestCase(unittest.TestCase): db_name = os.environ.get('MONGO_DATABASE', 'factory_boy_test') diff --git a/tests/test_using.py b/tests/test_using.py index 07dfbb47..0f4e9436 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -14,6 +14,12 @@ from . import utils +try: + import django # noqa: F401 + SKIP_DJANGO = True +except ImportError: + SKIP_DJANGO = True + class TestObject: def __init__(self, one=None, two=None, three=None, four=None, five=None): @@ -141,6 +147,7 @@ def test_create(self): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') + @unittest.skipIf(SKIP_DJANGO, "django tests disabled.") def test_create_custom_base(self): obj = factory.create(FakeModel, foo='bar', FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(obj.id, 2) @@ -156,6 +163,7 @@ def test_create_batch(self): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') + @unittest.skipIf(SKIP_DJANGO, "django tests disabled.") def test_create_batch_custom_base(self): objs = factory.create_batch( FakeModel, @@ -196,6 +204,7 @@ def test_generate_create(self): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') + @unittest.skipIf(SKIP_DJANGO, "django tests disabled.") def test_generate_create_custom_base(self): obj = factory.generate( FakeModel, @@ -231,6 +240,7 @@ def test_generate_batch_create(self): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') + @unittest.skipIf(SKIP_DJANGO, "django tests disabled.") def test_generate_batch_create_custom_base(self): objs = factory.generate_batch( FakeModel, @@ -267,6 +277,7 @@ def test_simple_generate_create(self): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') + @unittest.skipIf(SKIP_DJANGO, "django tests disabled.") def test_simple_generate_create_custom_base(self): obj = factory.simple_generate(FakeModel, True, foo='bar', FACTORY_CLASS=factory.django.DjangoModelFactory) self.assertEqual(obj.id, 2) @@ -292,6 +303,7 @@ def test_simple_generate_batch_create(self): self.assertEqual(obj.id, None) self.assertEqual(obj.foo, 'bar') + @unittest.skipIf(SKIP_DJANGO, "django tests disabled.") def test_simple_generate_batch_create_custom_base(self): objs = factory.simple_generate_batch( FakeModel, @@ -2046,6 +2058,7 @@ def __init__(self, **kwargs): self.id = None +@unittest.skipIf(SKIP_DJANGO, "django tests disabled.") class DjangoModelFactoryTestCase(unittest.TestCase): def test_simple(self): class FakeModelFactory(factory.django.DjangoModelFactory): diff --git a/tox.ini b/tox.ini index d5725014..ba6e9e25 100644 --- a/tox.ini +++ b/tox.ini @@ -2,13 +2,16 @@ minversion = 1.9 envlist = lint + docs + examples + linkcheck + py{37,38,39,310,py37,py38}-none-{sqlite,postgres} + py{37,38,39,310,py37,py38}-mongo-{sqlite,postgres} + py{37,38,39,310,py37,py38}-alchemy-{sqlite,postgres} py{37,38,39,py37,py38}-django22-{sqlite,postgres} py{37,38,39,310,py37,py38}-django32-{sqlite,postgres} py{38,39,310,py38}-django40-{sqlite,postgres} py310-djangomain-{sqlite,postgres} - docs - examples - linkcheck toxworkdir = {env:TOX_WORKDIR:.tox} @@ -37,15 +40,10 @@ passenv = POSTGRES_HOST POSTGRES_DATABASE deps = - Pillow - SQLAlchemy - mongoengine - django22: Django>=2.2,<2.3 - django32: Django>=3.2,<3.3 - django40: Django>=4.0,<4.1 - djangomain: https://github.com/django/django/archive/main.tar.gz - py{37,38,39,310}-django{22,32,40,main}-postgres: psycopg2-binary - py{py37,py38}-django{22,32,40,main}-postgres: psycopg2cffi + alchemy: SQLAlchemy + mongo: mongoengine + py{37,38,39,310}-postgres: psycopg2-binary + py{py37,py38}-postgres: psycopg2cffi setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings @@ -54,6 +52,26 @@ setenv = whitelist_externals = make commands = make test +[testenv:django22] +deps = + Django>=2.2,<2.3 + Pillow + +[testenv:django32] +deps = + Django>=3.2,<3.3 + Pillow + +[testenv:django40] +deps = + Django>=4.0,<4.1 + Pillow + +[testenv:djangomain] +deps = + https://github.com/django/django/archive/main.tar.gz + Pillow + [testenv:docs] extras = doc From 3601e971f92c04fd588726c4125d01d740c0959a Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 3 Jun 2022 10:37:46 -0400 Subject: [PATCH 594/714] Fixes django tests --- tox.ini | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/tox.ini b/tox.ini index ba6e9e25..26347fd5 100644 --- a/tox.ini +++ b/tox.ini @@ -40,8 +40,13 @@ passenv = POSTGRES_HOST POSTGRES_DATABASE deps = + Pillow alchemy: SQLAlchemy mongo: mongoengine + django22: Django>=2.2,<2.3 + django32: Django>=3.2,<3.3 + django40: Django>=4.0,<4.1 + djangomain: https://github.com/django/django/archive/main.tar.gz py{37,38,39,310}-postgres: psycopg2-binary py{py37,py38}-postgres: psycopg2cffi @@ -52,26 +57,6 @@ setenv = whitelist_externals = make commands = make test -[testenv:django22] -deps = - Django>=2.2,<2.3 - Pillow - -[testenv:django32] -deps = - Django>=3.2,<3.3 - Pillow - -[testenv:django40] -deps = - Django>=4.0,<4.1 - Pillow - -[testenv:djangomain] -deps = - https://github.com/django/django/archive/main.tar.gz - Pillow - [testenv:docs] extras = doc From a375a376b0ea03c3640701b617dd9c3bf34d2e01 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 3 Jun 2022 10:55:55 -0400 Subject: [PATCH 595/714] There is no need to run no deps with and without postgres, one pass will suffice --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69289ab5..c984e5ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -95,6 +95,8 @@ jobs: - "alchemy" - "mongo" - "none" + exclude: + - { mode-type: "none", database-type: "postgres" } services: mongodb: From 529b53fe0c4f77d20ce6687e803eae05b5061522 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 7 Jun 2022 10:19:34 -0400 Subject: [PATCH 596/714] Update test_using.py --- tests/test_using.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_using.py b/tests/test_using.py index 0f4e9436..5b2200a6 100644 --- a/tests/test_using.py +++ b/tests/test_using.py @@ -16,7 +16,7 @@ try: import django # noqa: F401 - SKIP_DJANGO = True + SKIP_DJANGO = False except ImportError: SKIP_DJANGO = True From 0adb82d24988e01ccf35667c1790f4c50b50a2b4 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 7 Jun 2022 16:26:08 +0200 Subject: [PATCH 597/714] cutting back on the amount of tests --- .github/workflows/test.yml | 70 ++------------------------------------ 1 file changed, 2 insertions(+), 68 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c984e5ed..249b941a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,8 +9,8 @@ concurrency: cancel-in-progress: true jobs: - django: - name: Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}, Database ${{ matrix.database-type }} + tests: + name: Python ${{ matrix.python-version }}, Database ${{ matrix.database-type }} runs-on: ubuntu-latest strategy: @@ -23,21 +23,9 @@ jobs: - "3.10" - "pypy-3.7" - "pypy-3.8" - django-version: - - "2.2" - - "3.2" - - "4.0" - - "main" database-type: - "sqlite" - "postgres" - exclude: - - { django-version: "2.2", python-version: "3.9" } - - { django-version: "2.2", python-version: "3.10" } - - { django-version: "4.0", python-version: "pypy-3.7" } - - { django-version: "4.0", python-version: "3.7" } - - { django-version: "main", python-version: "pypy-3.7" } - - { django-version: "main", python-version: "3.7" } services: mongodb: @@ -70,58 +58,4 @@ jobs: - name: Run tests run: tox env: - DJANGO: ${{ matrix.django-version }} DATABASE_TYPE: ${{ matrix.database-type }} - - others: - name: Python ${{ matrix.python-version[0] }}, ${{ matrix.mode-type }}, Database ${{ matrix.database-type }} - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - python-version: - - ["3.7", "py37"] - - ["3.8", "py38"] - - ["3.9", "py39"] - - ["3.10", "py310"] - - ["pypy-3.7", "pypy37"] - - ["pypy-3.8", "pypy38"] - database-type: - - "sqlite" - - "postgres" - - "mongo" - mode-type: - - "alchemy" - - "mongo" - - "none" - exclude: - - { mode-type: "none", database-type: "postgres" } - - services: - mongodb: - image: mongo - ports: - - 27017:27017 - - postgresdb: - image: postgres:alpine - ports: - - 5432:5432 - env: - POSTGRES_PASSWORD: password - POSTGRES_DB: alch_factory_boy_test - - steps: - - uses: actions/checkout@v2 - - - name: Set up Python ${{ matrix.python-version[0] }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version[0] }} - - - name: Install dependencies - run: python -m pip install tox-gh-actions - - - name: Run tests - run: tox -e ${{ matrix.python-version[1] }}-${{ matrix.mode-type }}-${{ matrix.database-type }} From 246e53acca1252b83e9376fe0700105e44594502 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Tue, 7 Jun 2022 10:37:24 -0400 Subject: [PATCH 598/714] Update README.rst --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index d933d14d..ffc96426 100644 --- a/README.rst +++ b/README.rst @@ -410,6 +410,7 @@ To test with a specific framework version, you may use a ``tox`` target: $ tox -e py310-alchemy-postgres # run tests inside a specific environment (mongoengine) + # Note: do not be fooled by "sqlite" in the name, mongodb needs to be up and running $ tox -e py310-mongo-sqlite From 01c528f703e05fe72f8ff33a20f679bd2f9b1f84 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 9 Jun 2022 17:24:35 +0200 Subject: [PATCH 599/714] Simplify tests --- tox.ini | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 26347fd5..7839f7c0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,7 @@ envlist = docs examples linkcheck - py{37,38,39,310,py37,py38}-none-{sqlite,postgres} - py{37,38,39,310,py37,py38}-mongo-{sqlite,postgres} - py{37,38,39,310,py37,py38}-alchemy-{sqlite,postgres} + py{37,38,39,310,py37,py38}-none-{sqlite} py{37,38,39,py37,py38}-django22-{sqlite,postgres} py{37,38,39,310,py37,py38}-django32-{sqlite,postgres} py{38,39,310,py38}-django40-{sqlite,postgres} @@ -41,8 +39,8 @@ passenv = POSTGRES_DATABASE deps = Pillow - alchemy: SQLAlchemy - mongo: mongoengine + django: SQLAlchemy ; to avoid being installed in "none" + django: mongoengine ; to avoid being installed in "none" django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 From ea981975bdf8c91c607e74b9ee00509eb359c228 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 9 Jun 2022 17:31:17 +0200 Subject: [PATCH 600/714] a little debuging to make sure nothing is being skipped.. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 93e36997..b5768752 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ test: -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ -Wdefault:"distutils Version classes are deprecated. Use packaging.version instead":DeprecationWarning:: \ - -m unittest + -m unittest -v # TODO: Remove "distutils Version classes are deprecated" when django 2.2 is dropped # DOC: Test the examples From af40f571402930808187bd60b8ff9f3426061bd0 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 9 Jun 2022 17:35:02 +0200 Subject: [PATCH 601/714] Previous attempt failed -- it was skipping sqlalchemy and mongo --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 7839f7c0..8dfdd0b2 100644 --- a/tox.ini +++ b/tox.ini @@ -39,8 +39,8 @@ passenv = POSTGRES_DATABASE deps = Pillow - django: SQLAlchemy ; to avoid being installed in "none" - django: mongoengine ; to avoid being installed in "none" + django{22,32,40,main}: SQLAlchemy ; to avoid being installed in "none" + django{22,32,40,main}: mongoengine ; to avoid being installed in "none" django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 From ce8bec38640be6c1186b41e3cd29cce2a18524d7 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 9 Jun 2022 17:37:05 +0200 Subject: [PATCH 602/714] No comments for now.. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 8dfdd0b2..dfaf49e9 100644 --- a/tox.ini +++ b/tox.ini @@ -39,8 +39,8 @@ passenv = POSTGRES_DATABASE deps = Pillow - django{22,32,40,main}: SQLAlchemy ; to avoid being installed in "none" - django{22,32,40,main}: mongoengine ; to avoid being installed in "none" + django{22,32,40,main}: SQLAlchemy + django{22,32,40,main}: mongoengine django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 From 3a5277f52ee55e22ecbe840bff5f033d247c6cba Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 9 Jun 2022 17:43:07 +0200 Subject: [PATCH 603/714] More verbose tests --- Makefile | 2 +- tox.ini | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index b5768752..93e36997 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ test: -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ -Wdefault:"distutils Version classes are deprecated. Use packaging.version instead":DeprecationWarning:: \ - -m unittest -v + -m unittest # TODO: Remove "distutils Version classes are deprecated" when django 2.2 is dropped # DOC: Test the examples diff --git a/tox.ini b/tox.ini index dfaf49e9..968ba08c 100644 --- a/tox.ini +++ b/tox.ini @@ -6,10 +6,10 @@ envlist = examples linkcheck py{37,38,39,310,py37,py38}-none-{sqlite} - py{37,38,39,py37,py38}-django22-{sqlite,postgres} - py{37,38,39,310,py37,py38}-django32-{sqlite,postgres} - py{38,39,310,py38}-django40-{sqlite,postgres} - py310-djangomain-{sqlite,postgres} + py{37,38,39,py37,py38}-django22-mongo-alchemy-{sqlite,postgres} + py{37,38,39,310,py37,py38}-django32-mongo-alchemy-{sqlite,postgres} + py{38,39,310,py38}-django40-mongo-alchemy-{sqlite,postgres} + py310-djangomain-mongo-alchemy-{sqlite,postgres} toxworkdir = {env:TOX_WORKDIR:.tox} @@ -38,9 +38,9 @@ passenv = POSTGRES_HOST POSTGRES_DATABASE deps = - Pillow - django{22,32,40,main}: SQLAlchemy - django{22,32,40,main}: mongoengine + alchemy: SQLAlchemy + mongo: mongoengine + django{22,32,40,main}: Pillow django22: Django>=2.2,<2.3 django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 From 60bcedcb9016fd51db9024f556c0bd4900a9a7f0 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 9 Jun 2022 11:55:02 -0400 Subject: [PATCH 604/714] Removes unneeded test setup conf, updates docs --- README.rst | 9 +++++---- tox.ini | 5 ----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index ffc96426..42933c1e 100644 --- a/README.rst +++ b/README.rst @@ -401,17 +401,18 @@ To test with a specific framework version, you may use a ``tox`` target: $ tox --listenvs # run tests inside a specific environment (django/mongoengine/SQLAlchemy are not installed) - $ tox -e py310-none-sqlite + $ tox -e py310-none # run tests inside a specific environment (django) - $ tox -e py310-djangomain-sqlite + $ tox -e py310-djangomain + $ tox -e py310-djangomain-postgres # run tests inside a specific environment (alchemy) + $ tox -e py310-alchemy $ tox -e py310-alchemy-postgres # run tests inside a specific environment (mongoengine) - # Note: do not be fooled by "sqlite" in the name, mongodb needs to be up and running - $ tox -e py310-mongo-sqlite + $ tox -e py310-mongo Packaging diff --git a/tox.ini b/tox.ini index 968ba08c..a418a90e 100644 --- a/tox.ini +++ b/tox.ini @@ -23,11 +23,6 @@ python = pypy-3.8: pypy38 [gh-actions:env] -DJANGO = - 2.2: django22 - 3.2: django32 - 4.0: django40 - main: djangomain DATABASE_TYPE = sqlite: sqlite postgres: postgres From a461e2f8cc06ec09f4b2d1412e3674fbf24cc8e2 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 13 Jun 2022 12:16:59 -0400 Subject: [PATCH 605/714] Update tox.ini MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Freitag --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index a418a90e..5acc7423 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = docs examples linkcheck - py{37,38,39,310,py37,py38}-none-{sqlite} + py{37,38,39,310,py37,py38}-sqlite py{37,38,39,py37,py38}-django22-mongo-alchemy-{sqlite,postgres} py{37,38,39,310,py37,py38}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,py38}-django40-mongo-alchemy-{sqlite,postgres} From af073aeb10fbe43401caa23e5a1f8dd0842ff273 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 13 Jun 2022 12:17:16 -0400 Subject: [PATCH 606/714] Update tests/test_django.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Freitag --- tests/test_django.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index ac3f6fb9..1a0882b8 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -9,16 +9,17 @@ try: import django - from django import test as django_test - from django.conf import settings - from django.contrib.auth.hashers import check_password - from django.core.management import color - from django.db import connections - from django.db.models import signals - from django.test import utils as django_test_utils except ImportError: raise unittest.SkipTest("django tests disabled.") +from django import test as django_test +from django.conf import settings +from django.contrib.auth.hashers import check_password +from django.core.management import color +from django.db import connections +from django.db.models import signals +from django.test import utils as django_test_utils + import factory.django from . import testdata From 53e26f3ba190d73e5d6bbd1e5915fbef0a41c2f5 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 13 Jun 2022 12:17:30 -0400 Subject: [PATCH 607/714] Update README.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: François Freitag --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 42933c1e..2b7ecd4f 100644 --- a/README.rst +++ b/README.rst @@ -401,7 +401,7 @@ To test with a specific framework version, you may use a ``tox`` target: $ tox --listenvs # run tests inside a specific environment (django/mongoengine/SQLAlchemy are not installed) - $ tox -e py310-none + $ tox -e py310 # run tests inside a specific environment (django) $ tox -e py310-djangomain From 9aef4e501a94246814a9dbabb90b21f393b29acc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 23 Jun 2022 09:36:37 +0200 Subject: [PATCH 608/714] Drop unused directive TOX_WORKDIR The directive is spelled TOX_WORK_DIR, so it is ineffective. Also, using `.tox` is the default. Co-authored-by: Javier Buzzi --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index 5acc7423..acdd2d37 100644 --- a/tox.ini +++ b/tox.ini @@ -11,8 +11,6 @@ envlist = py{38,39,310,py38}-django40-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} -toxworkdir = {env:TOX_WORKDIR:.tox} - [gh-actions] python = 3.7: py37 From 05e7d9690737444b8a56c24d7e15d2f3af89937b Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 14 Jun 2022 13:40:15 +0200 Subject: [PATCH 609/714] Create PostgreSQL database for SQLAlchemy tests Previously, the test suite expected the existence of an `alch_factory_boy_test` database. In order to facilitate local development, create it when the test suite runs, and destroy it afterwards. --- .github/workflows/test.yml | 1 - tests/test_alchemy.py | 13 +++++++++++++ tox.ini | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 249b941a..75a9d1c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,6 @@ jobs: - 5432:5432 env: POSTGRES_PASSWORD: password - POSTGRES_DB: alch_factory_boy_test env: TOXENV: ${{ matrix.tox-environment }} diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 3fde59ed..1ad62547 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -10,6 +10,8 @@ except ImportError: raise unittest.SkipTest("sqlalchemy tests disabled.") +from sqlalchemy_utils import create_database, database_exists, drop_database + import factory from factory.alchemy import SQLAlchemyModelFactory @@ -75,6 +77,17 @@ class Meta: text = factory.Sequence(lambda n: "text%s" % n) +if models.USING_POSTGRES: + # sqlite test database gets created/destroyed automatically, postgres does not. + + def setUpModule(): + if not database_exists(models.engine.url): + create_database(models.engine.url) + + def tearDownModule(): + drop_database(models.engine.url) + + class TransactionTestCase(unittest.TestCase): def setUp(self): models.Base.metadata.create_all(models.engine) diff --git a/tox.ini b/tox.ini index acdd2d37..8866ce0f 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,7 @@ passenv = POSTGRES_DATABASE deps = alchemy: SQLAlchemy + alchemy: sqlalchemy_utils mongo: mongoengine django{22,32,40,main}: Pillow django22: Django>=2.2,<2.3 From 6c243a73c1b7a15d9ab054ec219f8d7fe353abde Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 28 Jun 2022 11:41:26 +0200 Subject: [PATCH 610/714] Improve SQLAlchemy tests session cleanup The remove method rolls back the transaction, but also releases associated objects. https://docs.sqlalchemy.org/en/14/orm/contextual.html#sqlalchemy.orm.scoping.scoped_session.remove --- tests/test_alchemy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 1ad62547..9cac036b 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -93,7 +93,7 @@ def setUp(self): models.Base.metadata.create_all(models.engine) def tearDown(self): - models.session.rollback() + models.session.remove() models.Base.metadata.drop_all(models.engine) From 3e8246f777f6dcc83a16b91df35aa4fbc452fb7c Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 28 Jun 2022 11:49:46 +0200 Subject: [PATCH 611/714] Add PyPy 3.9 to the test matrix --- .github/workflows/test.yml | 1 + tox.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75a9d1c1..be7f93dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,7 @@ jobs: - "3.10" - "pypy-3.7" - "pypy-3.8" + - "pypy-3.9" database-type: - "sqlite" - "postgres" diff --git a/tox.ini b/tox.ini index 8866ce0f..2816715c 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,7 @@ python = 3.10: py310 pypy-3.7: pypy37 pypy-3.8: pypy38 + pypy-3.9: pypy39 [gh-actions:env] DATABASE_TYPE = From 59ac7c60b68660bdca0ec46f3523c08b0eedcfa4 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 28 Jun 2022 11:51:42 +0200 Subject: [PATCH 612/714] Add sqlalchemy_utils as a development dependency Missed in 05e7d9690737444b8a56c24d7e15d2f3af89937b. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 3ae6d65f..63ba80ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ dev = isort Pillow SQLAlchemy + sqlalchemy_utils mongoengine wheel>=0.32.0 tox From 141f5b2267b1f72b52d87e8f22360449f4c7822b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 28 Jun 2022 14:47:54 +0200 Subject: [PATCH 613/714] Use unittest test discovery for coverage Missed in 239bdeac12fba5b0abb3599be3165d91343923ef. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 93e36997..a89d376a 100644 --- a/Makefile +++ b/Makefile @@ -82,7 +82,7 @@ lint: coverage: $(COVERAGE) erase - $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch $(SETUP_PY) test + $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch -m unittest $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" $(COVERAGE) html "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" From e69607e890ee1fbfd8bae32732a68dbc4b9d00a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 28 Jun 2022 14:32:05 +0200 Subject: [PATCH 614/714] Add Javier to the CREDITS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Javier already contributed many improvements to the library, thanks! ★ --- CREDITS | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS b/CREDITS index 6ceb21ca..3c13c247 100644 --- a/CREDITS +++ b/CREDITS @@ -48,6 +48,7 @@ The project has received contributions from (in alphabetical order): * Issa Jubril * Ivan Miric * Janusz Skonieczny +* Javier Buzzi (https://github.com/kingbuzzman) * Jeff Widman (https://github.com/jeffwidman) * Jon Dufresne * Jonathan Tushman From 31c36c68d0e4936a43dc7b8e77f5fe69a61f21ff Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 28 Jun 2022 14:40:51 +0200 Subject: [PATCH 615/714] Move coverage metadata to setup.cfg Avoids repeating across coverage invocations. --- Makefile | 6 +++--- setup.cfg | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a89d376a..461318bb 100644 --- a/Makefile +++ b/Makefile @@ -82,9 +82,9 @@ lint: coverage: $(COVERAGE) erase - $(COVERAGE) run "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" --branch -m unittest - $(COVERAGE) report "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" - $(COVERAGE) html "--include=$(PACKAGE)/*.py,$(TESTS_DIR)/*.py" + $(COVERAGE) run --branch -m unittest + $(COVERAGE) report + $(COVERAGE) html .PHONY: test testall example-test lint coverage diff --git a/setup.cfg b/setup.cfg index 63ba80ab..f05f70cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -81,3 +81,8 @@ include_trailing_comma = True force_grid_wrap = 0 use_parentheses = True line_length = 88 + +[coverage:report] +include= + factory/*.py + tests/*.py From 8608b38c291f81cd6671d78557d40fa745bf09ae Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Tue, 28 Jun 2022 17:54:15 -0400 Subject: [PATCH 616/714] Fixes pypy39 tests --- tox.ini | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tox.ini b/tox.ini index 2816715c..59d962d8 100644 --- a/tox.ini +++ b/tox.ini @@ -5,10 +5,10 @@ envlist = docs examples linkcheck - py{37,38,39,310,py37,py38}-sqlite - py{37,38,39,py37,py38}-django22-mongo-alchemy-{sqlite,postgres} - py{37,38,39,310,py37,py38}-django32-mongo-alchemy-{sqlite,postgres} - py{38,39,310,py38}-django40-mongo-alchemy-{sqlite,postgres} + py{37,38,39,310,py37,py38,py39}-sqlite + py{37,38,39,py37,py38,py39}-django22-mongo-alchemy-{sqlite,postgres} + py{37,38,39,310,py37,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} + py{38,39,310,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} [gh-actions] @@ -41,7 +41,7 @@ deps = django40: Django>=4.0,<4.1 djangomain: https://github.com/django/django/archive/main.tar.gz py{37,38,39,310}-postgres: psycopg2-binary - py{py37,py38}-postgres: psycopg2cffi + pypy{37,38,39}-postgres: psycopg2cffi setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings From 4e4c67a2d4e111d82a1f2fa33682d6d5cbe572be Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Fri, 1 Jul 2022 10:04:42 +0200 Subject: [PATCH 617/714] Enable coverage context collection Annotates coverage lines with the test functions that cover them. --- setup.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.cfg b/setup.cfg index f05f70cd..163fdf08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -82,7 +82,13 @@ force_grid_wrap = 0 use_parentheses = True line_length = 88 +[coverage:run] +dynamic_context = test_function + [coverage:report] include= factory/*.py tests/*.py + +[coverage:html] +show_contexts = True From 91ccfd1ebc9804254b610ee7864b375179db6d22 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 4 Jul 2022 17:49:49 +0200 Subject: [PATCH 618/714] Remove TOXENV variable The tox-gh-actions GitHub action detects the environment. --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be7f93dc..ec179ef9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,9 +41,6 @@ jobs: env: POSTGRES_PASSWORD: password - env: - TOXENV: ${{ matrix.tox-environment }} - steps: - uses: actions/checkout@v2 From a961dd1835be88f3ff93765a9d5fcff5ba4cf054 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Wed, 29 Jun 2022 05:08:13 -0400 Subject: [PATCH 619/714] Cache pip dependencies across lint and test runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Speeds up tests, and nicer to PyPI bandwidth. Caches unused for 7 days are cleared by GitHub. https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy Pip checks if a newer version of the dependency is available on PyPI and use it instead of the cache when possible. https://pip.pypa.io/en/stable/cli/pip_install/#satisfying-requirements The linkcheck run was excluded as the cache would only grow. It’s a scheduled build occurring once a week, caching wouldn’t be as beneficial. --- .github/workflows/check.yml | 1 + .github/workflows/test.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1575242e..a6937464 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -25,6 +25,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 + cache: pip - name: Install dependencies run: python -m pip install tox diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec179ef9..4b804f33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,6 +48,7 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} + cache: pip - name: Install dependencies run: python -m pip install tox-gh-actions From ac6da2d4e140aeca8505c02016099ec3fdc250f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 5 Jul 2022 11:45:08 +0200 Subject: [PATCH 620/714] Fix typo in GitHub action check for cache key --- .github/workflows/check.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a6937464..a0333f3c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -25,7 +25,8 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 - cache: pip + with: + cache: pip - name: Install dependencies run: python -m pip install tox From 3c4ecc58d2a775975ba8ccbd9b1c478b37f173f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 5 Jul 2022 11:46:51 +0200 Subject: [PATCH 621/714] Bump GitHub actions version The python-version key is now mandatory. --- .github/workflows/check.yml | 5 +++-- .github/workflows/linkcheck.yml | 6 ++++-- .github/workflows/test.yml | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a0333f3c..db4cf8e6 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -21,11 +21,12 @@ jobs: TOXENV: ${{ matrix.tox-environment }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: + python-version: '3' cache: pip - name: Install dependencies diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index 62144ef1..bbfdafd3 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -9,10 +9,12 @@ jobs: name: Linkcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 + with: + python-version: '3' - name: Install dependencies run: python -m pip install tox diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b804f33..104b1fb6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,10 +42,10 @@ jobs: POSTGRES_PASSWORD: password steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} cache: pip From 6dab977f33528ddade915ba0ab037a9b9cd4a42c Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 18 Jul 2022 07:32:42 -0400 Subject: [PATCH 622/714] Update changelog.rst --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 34f3cac0..9045d28f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -61,7 +61,7 @@ ChangeLog *Removed:* - Drop support for Django 1.11. This version `is not maintained anymore `__. - - Drop support for Python 3.5. This version `is not maintained anymore `__. + - Drop support for Python 3.5. This version `is not maintained anymore `__. *Deprecated:* @@ -118,7 +118,7 @@ The following aliases were removed: *Removed:* - - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. + - Drop support for Python 2 and 3.4. These versions `are not maintained anymore `__. - Drop support for Django 2.0 and 2.1. These versions `are not maintained anymore `__. - Remove deprecated ``force_flush`` from ``SQLAlchemyModelFactory`` options. Use ``sqlalchemy_session_persistence = "flush"`` instead. From a252ef04727e7911c9f752cd7ff330a519f6c2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 30 Aug 2021 15:59:30 +0200 Subject: [PATCH 623/714] Remove confusing return in post_generation example The return value of post generation hooks is stored for advanced uses (i.e. saving instances for DjangoModelFactory after the post generation hooks completed), but this use is undocumented and not part of the public API. After issue #316 is completed, the save on post generation for DjangoModelFactory will be removed. That will also drop the last use for the `_after_postgeneration()` extension point. However, the extension point is kept to avoid disruption for projects that may be relying on it. --- docs/reference.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 76f913fd..a7e9be01 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1891,7 +1891,6 @@ A decorator is also provided, decorating a single method accepting the same return path = extracted or os.path.join('/tmp/mbox/', obj.login) os.path.makedirs(path) - return path .. OHAI_VIM** From 9ecdf1cd96df468982656cfe1bfe81f084caa303 Mon Sep 17 00:00:00 2001 From: Billy Kern Date: Tue, 12 Jul 2022 14:35:06 -0500 Subject: [PATCH 624/714] docs: remove unused import from code snippet --- docs/examples.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index 6a113dca..7d868f8c 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -52,7 +52,6 @@ And now, we'll define the related factories: import datetime import factory - import random from . import objects From 27e55cc20800b9f3c9205fc63833e90d115eea25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 16 Aug 2022 13:57:03 +0200 Subject: [PATCH 625/714] Drop support for end-of-life Django 2.2 --- Makefile | 2 -- docs/changelog.rst | 1 + examples/django_demo/django_demo/urls.py | 8 ++------ setup.cfg | 1 - tox.ini | 4 +--- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 461318bb..3bab407c 100644 --- a/Makefile +++ b/Makefile @@ -62,9 +62,7 @@ test: -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ - -Wdefault:"distutils Version classes are deprecated. Use packaging.version instead":DeprecationWarning:: \ -m unittest -# TODO: Remove "distutils Version classes are deprecated" when django 2.2 is dropped # DOC: Test the examples example-test: diff --git a/docs/changelog.rst b/docs/changelog.rst index 9045d28f..8a362c11 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -46,6 +46,7 @@ ChangeLog *Removed:* + - Drop support for Django 2.2 - Drop support for Django 3.0 - Drop support for Django 3.1 - Drop support for Python 3.6 diff --git a/examples/django_demo/django_demo/urls.py b/examples/django_demo/django_demo/urls.py index 667a5516..a8b95abb 100644 --- a/examples/django_demo/django_demo/urls.py +++ b/examples/django_demo/django_demo/urls.py @@ -13,13 +13,9 @@ 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ -try: - # TODO: After dropping Django 2.2, switch to `path` - from django.urls import re_path as url -except ImportError: - from django.conf.urls import url from django.contrib import admin +from django.urls import path urlpatterns = [ - url(r'^admin/', admin.site.urls), + path('admin/', admin.site.urls), ] diff --git a/setup.cfg b/setup.cfg index 163fdf08..2f3c0b11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ license = MIT classifiers = Development Status :: 5 - Production/Stable Framework :: Django - Framework :: Django :: 2.2 Framework :: Django :: 3.2 Framework :: Django :: 4.0 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index 59d962d8..56745351 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,6 @@ envlist = examples linkcheck py{37,38,39,310,py37,py38,py39}-sqlite - py{37,38,39,py37,py38,py39}-django22-mongo-alchemy-{sqlite,postgres} py{37,38,39,310,py37,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} @@ -35,8 +34,7 @@ deps = alchemy: SQLAlchemy alchemy: sqlalchemy_utils mongo: mongoengine - django{22,32,40,main}: Pillow - django22: Django>=2.2,<2.3 + django{32,40,main}: Pillow django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 djangomain: https://github.com/django/django/archive/main.tar.gz From 8e5b79ab36b7918d723c382b3e6a59bca93d28b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 16 Aug 2022 13:53:35 +0200 Subject: [PATCH 626/714] Add support for Django 4.1 --- docs/changelog.rst | 1 + setup.cfg | 1 + tox.ini | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 8a362c11..94fe8699 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,7 @@ ChangeLog create sessions for use by the :class:`~factory.alchemy.SQLAlchemyModelFactory`. - Add support for Django 3.2 - Add support for Django 4.0 + - Add support for Django 4.1 - Add support for Python 3.10 *Bugfix:* diff --git a/setup.cfg b/setup.cfg index 2f3c0b11..85a44682 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,6 +17,7 @@ classifiers = Framework :: Django Framework :: Django :: 3.2 Framework :: Django :: 4.0 + Framework :: Django :: 4.1 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index 56745351..9ce6b759 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = py{37,38,39,310,py37,py38,py39}-sqlite py{37,38,39,310,py37,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} + py{38,39,310,py38,py39}-django41-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} [gh-actions] @@ -34,9 +35,10 @@ deps = alchemy: SQLAlchemy alchemy: sqlalchemy_utils mongo: mongoengine - django{32,40,main}: Pillow + django{32,40,41,main}: Pillow django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 + django41: Django>=4.1,<4.2 djangomain: https://github.com/django/django/archive/main.tar.gz py{37,38,39,310}-postgres: psycopg2-binary pypy{37,38,39}-postgres: psycopg2cffi From 3da4628d04b978f6c4c4f072ee743185c7984405 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 2 Oct 2022 15:56:42 +0200 Subject: [PATCH 627/714] Fix typos --- tests/test_faker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_faker.py b/tests/test_faker.py index 00355b77..03922ef5 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -67,7 +67,7 @@ class Meta: email = factory.Faker('email') self._setup_mock_faker(first_name="John", last_name="Doe", email="john.doe@example.org") - self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@exemple.fr", locale='fr_FR') + self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@example.fr", locale='fr_FR') profile = ProfileFactory() self.assertEqual("John", profile.first_name) @@ -154,7 +154,7 @@ class Meta: def fake_select_date(start_date, end_date): """Fake date_between_dates.""" - # Ensure that dates have been transfered from the factory + # Ensure that dates have been transferred from the factory # to Faker parameters. self.assertEqual(start_date, may_4th) self.assertEqual(end_date, may_25th) From cc245f4a95ae22e61172c92f61814418c2b273fe Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 2 Oct 2022 15:58:47 +0200 Subject: [PATCH 628/714] Fix typos --- tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_base.py b/tests/test_base.py index 0b9ffa15..d3b32570 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -316,7 +316,7 @@ def test_reset_sequence_with_value(self): self.assertEqual(42, o3.one) def test_reset_sequence_subclass_fails(self): - """Tests that the sequence of a 'slave' factory cannot be reseted.""" + """Tests that the sequence of a 'slave' factory cannot be reset.""" class SubTestObjectFactory(self.TestObjectFactory): pass @@ -343,7 +343,7 @@ class SubTestObjectFactory(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.""" + """Tests that the sequence of a 'slave' factory cannot be reset.""" class SubTestObjectFactory(self.TestObjectFactory): pass From fb0d1284abdead3e67e55dc9b2723ed08138bae4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 3 Oct 2022 15:25:02 +0200 Subject: [PATCH 629/714] `example` translates to `exemple` in French ;) --- tests/test_faker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_faker.py b/tests/test_faker.py index 03922ef5..d1a16da0 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -67,7 +67,7 @@ class Meta: email = factory.Faker('email') self._setup_mock_faker(first_name="John", last_name="Doe", email="john.doe@example.org") - self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@example.fr", locale='fr_FR') + self._setup_mock_faker(first_name="Jean", last_name="Valjean", email="jvaljean@exemple.fr", locale='fr_FR') profile = ProfileFactory() self.assertEqual("John", profile.first_name) From f094658e77c2f5f88e94a57927b0b26adb8d3bda Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 27 Oct 2022 09:30:29 +0200 Subject: [PATCH 630/714] Interrupt the concurrent builds on subsequent pushes --- .github/workflows/check.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index db4cf8e6..a06aae03 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -4,6 +4,10 @@ on: - push - pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build: name: ${{ matrix.tox-environment }} From f4e35357c6e9fe3ecd80d5ec3207b3fbd2c40576 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Thu, 27 Oct 2022 09:30:46 +0200 Subject: [PATCH 631/714] Fix the Flask demo app --- examples/flask_alchemy/test_demoapp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/examples/flask_alchemy/test_demoapp.py b/examples/flask_alchemy/test_demoapp.py index 04658c2f..68d56935 100644 --- a/examples/flask_alchemy/test_demoapp.py +++ b/examples/flask_alchemy/test_demoapp.py @@ -9,12 +9,17 @@ class DemoAppTestCase(unittest.TestCase): def setUp(self): demoapp.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' demoapp.app.config['TESTING'] = True + + self.app_context = demoapp.app.app_context() + self.app_context.push() + self.app = demoapp.app.test_client() self.db = demoapp.db self.db.create_all() def tearDown(self): self.db.drop_all() + self.app_context.pop() def test_user_factory(self): user = demoapp_factories.UserFactory() From edb6c2ee76c842ab9d6c5435ef1e63527c877507 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 5 Oct 2022 22:08:17 +0200 Subject: [PATCH 632/714] README.md: Update URL for Python's supported versions https://devguide.python.org/#status-of-python-branches has been moved to https://devguide.python.org/versions --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2b7ecd4f..e5ae4112 100644 --- a/README.rst +++ b/README.rst @@ -324,7 +324,7 @@ Support Policy ``factory_boy`` supports active Python versions as well as PyPy3. - **Python**'s `supported versions - `__. + `__. - **Django**'s `supported versions `__. - **SQLAlchemy**: `latest version on PyPI `__. From e0f0c0f75e66c4dd01d3d375747be4b475536479 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Thu, 27 Oct 2022 12:58:30 -0700 Subject: [PATCH 633/714] Stop throwing custom `ImportError` This was necessary under python 2, but now that python 3 supports exception chaining, the underlying `ImportError` won't be masked by the other exceptions. I considered augmenting the error message to mention checking in the path / venv, but these days I think people will naturally google for `ImportError` and find plenty of helpful context from StackOverflow etc. Such as this very question/answer: https://stackoverflow.com/questions/14013728/django-no-module-named-django-core-management --- examples/django_demo/manage.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/examples/django_demo/manage.py b/examples/django_demo/manage.py index bd1a003b..2aa7a57e 100755 --- a/examples/django_demo/manage.py +++ b/examples/django_demo/manage.py @@ -4,19 +4,5 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_demo.settings") - try: - from django.core.management import execute_from_command_line - except ImportError: - # The above import may fail for some other reason. Ensure that the - # issue is really that Django is missing to avoid masking other - # exceptions on Python 2. - try: - import django # noqa: F401 - except ImportError: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) - raise + from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) From 7439208c32841d0570500c694d0471ef5aa2dd46 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Thu, 27 Oct 2022 12:39:50 -0700 Subject: [PATCH 634/714] Remove Python 2 warning Support for Python 2 was dropped as part of the `3.0.0` release back in 2020. So this no longer applies. --- docs/fuzzy.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index 898bae85..ca251656 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -75,15 +75,6 @@ FuzzyChoice This allows passing in, for instance, a Django queryset that will only hit the database during the database, not at import time. - .. warning:: When using Python2 and list comprehension, use private variable - names as in: - - `[_x.name for _x in items]` - - instead of: - - `[x.name for x in items]` - .. attribute:: choices The list of choices to select randomly From ffbe278b274453732d7fdefa554f2621dfed6fcd Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Thu, 27 Oct 2022 15:44:15 -0400 Subject: [PATCH 635/714] Adds python 3.11 testing support --- .github/workflows/test.yml | 1 + Makefile | 2 ++ setup.cfg | 1 + tox.ini | 11 ++++++----- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 104b1fb6..f50bd24c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,7 @@ jobs: - "3.8" - "3.9" - "3.10" + - "3.11" - "pypy-3.7" - "pypy-3.8" - "pypy-3.9" diff --git a/Makefile b/Makefile index 3bab407c..df330d09 100644 --- a/Makefile +++ b/Makefile @@ -62,6 +62,8 @@ test: -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ + # Remove cgi warning when dropping support for Django<=4.1. + -Wdefault:"'cgi' is deprecated and slated for removal in Python 3.13":DeprecationWarning:: \ -m unittest # DOC: Test the examples diff --git a/setup.cfg b/setup.cfg index 85a44682..a3c7e4c9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,6 +28,7 @@ classifiers = Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing diff --git a/tox.ini b/tox.ini index 9ce6b759..681aaf9a 100644 --- a/tox.ini +++ b/tox.ini @@ -5,10 +5,10 @@ envlist = docs examples linkcheck - py{37,38,39,310,py37,py38,py39}-sqlite - py{37,38,39,310,py37,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} - py{38,39,310,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} - py{38,39,310,py38,py39}-django41-mongo-alchemy-{sqlite,postgres} + py{37,38,39,310,311,py37,py38,py39}-sqlite + py{37,38,39,310,311,py37,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,py38,py39}-django41-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} [gh-actions] @@ -17,6 +17,7 @@ python = 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 pypy-3.7: pypy37 pypy-3.8: pypy38 pypy-3.9: pypy39 @@ -40,7 +41,7 @@ deps = django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 djangomain: https://github.com/django/django/archive/main.tar.gz - py{37,38,39,310}-postgres: psycopg2-binary + py{37,38,39,310,311}-postgres: psycopg2-binary pypy{37,38,39}-postgres: psycopg2cffi setenv = From 6b9beb9a4198b7bd01c61f3fc8a85859f2d361e7 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 27 Oct 2022 14:44:18 +0200 Subject: [PATCH 636/714] Comments should match the parameter names Closes #976 --- factory/declarations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index fe2e34d9..40ae99c4 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -611,8 +611,8 @@ def call(self, instance, step, context): # pragma: no cover """Call this hook; no return value is expected. Args: - obj (object): the newly generated object - create (bool): whether the object was 'built' or 'created' + instance (object): the newly generated object + step (bool): whether the object was 'built' or 'created' context: a builder.PostGenerationContext containing values extracted from the containing factory's declaration """ From a2cd524382247988df9d730c09934d909e783534 Mon Sep 17 00:00:00 2001 From: Matt Hughes Date: Fri, 28 Oct 2022 09:44:19 +0100 Subject: [PATCH 637/714] Use `importlib_metadata` to read version This fixes a bug where the `factory` could not be imported on Python 3.7 if `setuptools` wasn't installed (see below). Rather than require `setuptools` move to using `import-metadata` so that the same approach is used for Python < 3.8 and Python >= 3.8 (where `importlib.metadata` is available in the stdlib) The bug: installing this distribution without `setuptools` results in an `ImportError` when trying to determine the distribution's version (only on Python<3.8, above this it uses the builtin `importlib.metadata`): $ python --version Python 3.7.4 $ python -m venv .venv $ source .venv/bin/activate $ pip uninstall --yes --quiet setuptools $ pip install --quiet factory-boy $ python -c 'import factory' Traceback (most recent call last): File "/tmp/tmp.qEc9M3LwV5/.venv/lib/python3.7/site-packages/factory/__init__.py", line 74, in from importlib.metadata import version ModuleNotFoundError: No module named 'importlib.metadata' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "", line 1, in File "/tmp/tmp.qEc9M3LwV5/.venv/lib/python3.7/site-packages/factory/__init__.py", line 78, in import pkg_resources ModuleNotFoundError: No module named 'pkg_resources' Fixes #990 --- factory/__init__.py | 8 +++----- setup.cfg | 4 +++- tests/test_version.py | 10 ++++++++++ 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 tests/test_version.py diff --git a/factory/__init__.py b/factory/__init__.py index b0ada75d..bdc3ac0d 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -72,10 +72,8 @@ __author__ = 'Raphaël Barrois ' try: # Python 3.8+ - from importlib.metadata import version - - __version__ = version("factory_boy") + import importlib.metadata as importlib_metadata except ImportError: - import pkg_resources + import importlib_metadata - __version__ = pkg_resources.get_distribution("factory_boy").version +__version__ = importlib_metadata.version("factory_boy") diff --git a/setup.cfg b/setup.cfg index a3c7e4c9..5b6e7c1d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,9 @@ classifiers = zip_safe = false packages = factory python_requires = >=3.7 -install_requires = Faker>=0.7.0 +install_requires = + Faker>=0.7.0 + importlib_metadata;python_version<"3.8" [options.extras_require] dev = diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 00000000..52291dc4 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,10 @@ +# Copyright: See the LICENSE file. + +import unittest + +import factory + + +class VersionTestCase(unittest.TestCase): + def test_version(self): + self.assertEqual(factory.__version__, "3.2.1.dev0") From 3f4c2dc33e42ae5f23ae033853c47d401390f99e Mon Sep 17 00:00:00 2001 From: Jelmer Draaijer Date: Sun, 23 Oct 2022 20:08:03 +0200 Subject: [PATCH 638/714] Add extra check if _original_params is not None and validate with a testcase for django Co-authored-by: Javier Buzzi --- factory/alchemy.py | 5 +++++ factory/django.py | 5 +++++ tests/djapp/models.py | 4 ++++ tests/test_django.py | 28 +++++++++++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index cf20b537..4e7ad6a2 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -47,6 +47,7 @@ class SQLAlchemyModelFactory(base.Factory): """Factory for SQLAlchemy models. """ _options_class = SQLAlchemyOptions + _original_params = None class Meta: abstract = True @@ -77,6 +78,10 @@ def _get_or_create(cls, model_class, session, args, kwargs): obj = cls._save(model_class, session, args, {**key_fields, **kwargs}) except IntegrityError as e: session.rollback() + + if cls._original_params is None: + raise e + get_or_create_params = { lookup: value for lookup, value in cls._original_params.items() diff --git a/factory/django.py b/factory/django.py index 23b93425..db37d045 100644 --- a/factory/django.py +++ b/factory/django.py @@ -82,6 +82,7 @@ class DjangoModelFactory(base.Factory): """ _options_class = DjangoOptions + _original_params = None class Meta: abstract = True # Optional, but explicit. @@ -142,6 +143,10 @@ def _get_or_create(cls, model_class, *args, **kwargs): try: instance, _created = manager.get_or_create(*args, **key_fields) except IntegrityError as e: + + if cls._original_params is None: + raise e + get_or_create_params = { lookup: value for lookup, value in cls._original_params.items() diff --git a/tests/djapp/models.py b/tests/djapp/models.py index 088008e5..b7aa8794 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -133,3 +133,7 @@ class Meta: class FromAbstractWithCustomManager(AbstractWithCustomManager): pass + + +class HasMultifieldModel(models.Model): + multifield = models.ForeignKey(to=MultifieldModel, on_delete=models.CASCADE) diff --git a/tests/test_django.py b/tests/test_django.py index 1a0882b8..fd4fe3cd 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -16,7 +16,7 @@ from django.conf import settings from django.contrib.auth.hashers import check_password from django.core.management import color -from django.db import connections +from django.db import IntegrityError, connections from django.db.models import signals from django.test import utils as django_test_utils @@ -1105,3 +1105,29 @@ def test_create_warning(self): def test_build_no_warning(self): self.StandardFactoryWithPost.build() + + +class IntegrityErrorForMissingOriginalParamsTest(django_test.TestCase): + + def test_raises_integrity_error(self): + """ + In factory.django.DjangoModelFactory._get_or_create + _original_params can give some trouble when None + + This test case verifies if the IntegrityError is correctly re-raised + """ + + class MultifieldModelFactory2(MultifieldModelFactory): + class Meta: + model = models.MultifieldModel + django_get_or_create = ['text'] + + class HasMultifieldModelFactory(factory.django.DjangoModelFactory): + multifield = factory.SubFactory(MultifieldModelFactory2) + + class Meta: + model = models.HasMultifieldModel + + HasMultifieldModelFactory(multifield__slug="test") + with self.assertRaises(IntegrityError): + HasMultifieldModelFactory(multifield__slug="test") From 71ccc0f6dc12b04a1bd53096650fe7a914f49cf3 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 6 Feb 2023 11:06:16 -0500 Subject: [PATCH 639/714] Update tox.ini --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 681aaf9a..a234315f 100644 --- a/tox.ini +++ b/tox.ini @@ -48,7 +48,7 @@ setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings postgres: DJANGO_SETTINGS_MODULE=tests.djapp.settings_pg -whitelist_externals = make +allowlist_externals = make commands = make test [testenv:docs] From 1e3ddeb7387deb6484931443584ccc598335168a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 17 Mar 2023 17:59:29 +0100 Subject: [PATCH 640/714] Fix make test rule The comment should not be in the middle of the command to execute. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index df330d09..ad35a6fc 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,7 @@ testall: tox # DOC: Run tests for the currently installed version +# Remove cgi warning when dropping support for Django<=4.1. test: python \ -b \ @@ -62,7 +63,6 @@ test: -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ - # Remove cgi warning when dropping support for Django<=4.1. -Wdefault:"'cgi' is deprecated and slated for removal in Python 3.13":DeprecationWarning:: \ -m unittest From 7532e9645fa60e947349e5a729dbefd56fdc7610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 17 Mar 2023 20:54:26 +0100 Subject: [PATCH 641/714] Fix SQLAlchemy warning declarative_base import path sqlalchemy.exc.MovedIn20Warning: The ``declarative_base()`` function is now available as sqlalchemy.orm.declarative_base(). (deprecated since: 2.0) (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9) --- docs/orms.rst | 3 +-- tests/alchemyapp/models.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/orms.rst b/docs/orms.rst index 3d255a31..3839707c 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -463,8 +463,7 @@ A (very) simple example: .. 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 + from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker engine = create_engine('sqlite://') session = scoped_session(sessionmaker(bind=engine)) diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 2ed443dc..20e60aab 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -5,8 +5,7 @@ import os from sqlalchemy import Column, Integer, Unicode, create_engine -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker try: import psycopg2 # noqa: F401 From f81b0bef2d85d9585704573e3d945d3659003f90 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Mon, 6 Mar 2023 16:25:31 +0200 Subject: [PATCH 642/714] fix: Fix mute_signals for factories with post-generation hooks --- factory/django.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/factory/django.py b/factory/django.py index db37d045..dee35d6a 100644 --- a/factory/django.py +++ b/factory/django.py @@ -327,6 +327,9 @@ def __call__(self, callable_obj): # Retrieve __func__, the *actual* callable object. callable_obj._create = self.wrap_method(callable_obj._create.__func__) callable_obj._generate = self.wrap_method(callable_obj._generate.__func__) + callable_obj._after_postgeneration = self.wrap_method( + callable_obj._after_postgeneration.__func__ + ) return callable_obj else: From a39aa69ce6ad76559f7961875dc55a9b0f0ec28c Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Wed, 8 Mar 2023 14:23:49 +0200 Subject: [PATCH 643/714] tests: Add test case Refs #424 --- tests/djapp/models.py | 4 ++++ tests/test_django.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index b7aa8794..b8a5d055 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -18,6 +18,10 @@ class StandardModel(models.Model): foo = models.CharField(max_length=20) +class WithRelation(models.Model): + rel = models.ForeignKey(StandardModel, models.CASCADE) + + class NonIntegerPk(models.Model): foo = models.CharField(max_length=20, primary_key=True) bar = models.CharField(max_length=20, blank=True) diff --git a/tests/test_django.py b/tests/test_django.py index fd4fe3cd..e400012b 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -20,6 +20,7 @@ from django.db.models import signals from django.test import utils as django_test_utils +import factory import factory.django from . import testdata @@ -989,6 +990,33 @@ def post(obj, create, extracted, **kwargs): self.assertSignalsReactivated() + def test_class_decorator_related_model_with_post_hook(self): + """ + Related factory with post_generation hook should not call disabled signals. + + Refs https://github.com/FactoryBoy/factory_boy/issues/424 + """ + + @factory.django.mute_signals(signals.post_save) + class StandardFactory(factory.django.DjangoModelFactory): + class Meta: + model = models.StandardModel + + @factory.post_generation + def post_action(obj, create, extracted, **kwargs): + pass + + class WithRelationFactory(factory.django.DjangoModelFactory): + rel = factory.SubFactory(StandardFactory) + + class Meta: + model = models.WithRelation + + WithRelationFactory.create() + + # Called only once for WithRelationFactory. + self.handlers.post_save.assert_called_once() + def test_class_decorator_build(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) class WithSignalsDecoratedFactory(factory.django.DjangoModelFactory): From 51a59f68728207695e4115a470d9d98ca6460377 Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Wed, 8 Mar 2023 16:13:41 +0200 Subject: [PATCH 644/714] chore: More specific assertion --- tests/test_django.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index e400012b..f0c5c9d1 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1014,8 +1014,15 @@ class Meta: WithRelationFactory.create() - # Called only once for WithRelationFactory. - self.handlers.post_save.assert_called_once() + self.handlers.post_save.assert_called_once_with( + signal=mock.ANY, + sender=models.WithRelation, + instance=mock.ANY, + created=True, + update_fields=None, + raw=False, + using="default", + ) def test_class_decorator_build(self): @factory.django.mute_signals(signals.pre_save, signals.post_save) From 834afc1f909cc2e29a70aacb6716c41bdb02ee97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 14 Mar 2023 17:23:18 +0100 Subject: [PATCH 645/714] Reuse existing models --- tests/djapp/models.py | 4 ---- tests/test_django.py | 14 +++++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/tests/djapp/models.py b/tests/djapp/models.py index b8a5d055..b7aa8794 100644 --- a/tests/djapp/models.py +++ b/tests/djapp/models.py @@ -18,10 +18,6 @@ class StandardModel(models.Model): foo = models.CharField(max_length=20) -class WithRelation(models.Model): - rel = models.ForeignKey(StandardModel, models.CASCADE) - - class NonIntegerPk(models.Model): foo = models.CharField(max_length=20, primary_key=True) bar = models.CharField(max_length=20, blank=True) diff --git a/tests/test_django.py b/tests/test_django.py index f0c5c9d1..df929846 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -998,25 +998,25 @@ def test_class_decorator_related_model_with_post_hook(self): """ @factory.django.mute_signals(signals.post_save) - class StandardFactory(factory.django.DjangoModelFactory): + class PointedFactory(factory.django.DjangoModelFactory): class Meta: - model = models.StandardModel + model = models.PointedModel @factory.post_generation def post_action(obj, create, extracted, **kwargs): pass - class WithRelationFactory(factory.django.DjangoModelFactory): - rel = factory.SubFactory(StandardFactory) + class PointerFactory(factory.django.DjangoModelFactory): + pointed = factory.SubFactory(PointedFactory) class Meta: - model = models.WithRelation + model = models.PointerModel - WithRelationFactory.create() + PointerFactory.create() self.handlers.post_save.assert_called_once_with( signal=mock.ANY, - sender=models.WithRelation, + sender=models.PointerModel, instance=mock.ANY, created=True, update_fields=None, From de8d51ee66b8adf9b17252717e2db92faa43dddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 21 Mar 2023 08:59:42 +0100 Subject: [PATCH 646/714] Add changelog entry for #1004 --- docs/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 94fe8699..63f1567d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -21,6 +21,8 @@ ChangeLog - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. + - Make :meth:`~factory.django.mute_signals` mute signals during post-generation. + - :issue:`775`: Change the signature for :meth:`~factory.alchemy.SQLAlchemyModelFactory._save` and :meth:`~factory.alchemy.SQLAlchemyModelFactory._get_or_create` to avoid argument names clashes with a field named ``session``. From f12bf9b405fea79bbbb46899a0f361a51e9cc079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 21 Mar 2023 09:03:32 +0100 Subject: [PATCH 647/714] Skip post-generation save in test_class_decorator_related_model_with_post_hook --- tests/test_django.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_django.py b/tests/test_django.py index df929846..19729e07 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -1001,6 +1001,7 @@ def test_class_decorator_related_model_with_post_hook(self): class PointedFactory(factory.django.DjangoModelFactory): class Meta: model = models.PointedModel + skip_postgeneration_save = True @factory.post_generation def post_action(obj, create, extracted, **kwargs): From 2b0efa7fe0d1614cbd5c86062281a905efc3484d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 24 Apr 2023 11:05:08 +0200 Subject: [PATCH 648/714] Upgrade intersphinx mapping for Sphinx 8 WARNING: The pre-Sphinx 1.0 'intersphinx_mapping' format is deprecated and will be removed in Sphinx 8. Update to the current format as described in the documentation. Hint: "intersphinx_mapping = {'': ('https://docs.python.org/', None)}".https://www.sphinx-doc.org/en/master/us age/extensions/intersphinx.html#confval-intersphinx_mapping https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping Use this opportunity to link to the Python 3 documentation. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index f636c01b..876a1c49 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -79,7 +79,7 @@ # -- intersphinx ------------------------------------------------------------- intersphinx_mapping = { - 'https://docs.python.org/': None, + 'python': ('https://docs.python.org/3', None), 'django': ( 'https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/_objects/', From 53b34d504fad05dd1887a7eb0056ce9516fd4646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 2 May 2023 22:52:52 +0200 Subject: [PATCH 649/714] Remove zip_safe flag from setup.cfg https://setuptools.pypa.io/en/latest/deprecated/zip_safe.html#understanding-the-zip-safe-flag > It is very unlikely that the values of zip_safe will affect modern deployments that use pip for installing packages. Moreover, new users of setuptools should not attempt to create egg files using the deprecated build_egg command. Therefore, this flag is considered obsolete. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 5b6e7c1d..2e8749c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,6 @@ classifiers = Topic :: Software Development :: Libraries :: Python Modules [options] -zip_safe = false packages = factory python_requires = >=3.7 install_requires = From d498bcd1773010ffdedc840b84902b0e091e7130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 7 Apr 2023 13:41:01 +0200 Subject: [PATCH 650/714] Factor "unwrapped evaluation" of declarations. Some declarations are merely wrapping another one (`Maybe`, etc.). Add a helper to simplify unwrapping-and-evaluating said declaration. --- factory/declarations.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/factory/declarations.py b/factory/declarations.py index 40ae99c4..150ccd89 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -43,6 +43,21 @@ def unroll_context(self, instance, step, context): subfactory = factory.base.DictFactory return step.recurse(subfactory, full_context, force_sequence=step.sequence) + def _unwrap_evaluate_pre(self, wrapped, *, instance, step, overrides): + """Evaluate a wrapped pre-declaration. + + This is especially useful for declarations wrapping another one, + e.g. Maybe or Transformer. + """ + if isinstance(wrapped, BaseDeclaration): + return wrapped.evaluate_pre( + instance=instance, + step=step, + overrides=overrides, + ) + else: + return wrapped + def evaluate_pre(self, instance, step, overrides): context = self.unroll_context(instance, step, overrides) return self.evaluate(instance, step, context) @@ -492,16 +507,14 @@ def evaluate_post(self, instance, step, overrides): def evaluate_pre(self, instance, step, overrides): choice = self.decider.evaluate(instance=instance, step=step, extra={}) target = self.yes if choice else self.no - - if isinstance(target, BaseDeclaration): - return target.evaluate_pre( - instance=instance, - step=step, - overrides=overrides, - ) - else: - # Flat value (can't be POST_INSTANTIATION, checked in __init__) - return target + # The value can't be POST_INSTANTIATION, checked in __init__; + # evaluate it as `evaluate_pre` + return self._unwrap_evaluate_pre( + target, + instance=instance, + step=step, + overrides=overrides, + ) def __repr__(self): return f'Maybe({self.decider!r}, yes={self.yes!r}, no={self.no!r})' From 543304878949ececed808dd6fbfcd0549c349644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Sun, 5 Mar 2023 17:20:29 +0100 Subject: [PATCH 651/714] Handle declarations in Transformers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transformers init changed from ```python def __init__(self, transform, declaration): ``` to ```python def __init__(self, declaration, *, transform): ``` That allows Password to specify `make_transform` as the default transform, leaving an option to override the transform and facilitates subclassing Transformer. Having a transform keyword argument also clarifies the instantiation of transformers. Co-authored-by: Raphaël Barrois --- docs/reference.rst | 13 +-- factory/builder.py | 20 +++-- factory/declarations.py | 36 +++++--- factory/django.py | 4 +- tests/test_declarations.py | 2 +- tests/test_transformer.py | 169 ++++++++++++++++++++++++++++++++++++- 6 files changed, 213 insertions(+), 31 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index a7e9be01..9b6b4f39 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -907,21 +907,22 @@ return value of the method: Transformer """"""""""" -.. class:: Transformer(transform, value) +.. class:: Transformer(default_value, *, transform) A :class:`Transformer` applies a ``transform`` function to the provided value before to set the transformed value on the generated object. -It expects two arguments: +It expects one positional argument and one keyword argument: -- ``transform``: function taking the value as parameter and returning the +- ``default_value``: the default value, which passes through the ``transform`` + function. +- ``transform``: a function taking the value as parameter and returning the transformed value, -- ``value``: the default value. .. code-block:: python - class UpperFactory(Factory): - name = Transformer(lambda x: x.upper(), "Joe") + class UpperFactory(factory.Factory): + name = factory.Transformer("Joe", transform=str.upper) class Meta: model = Upper diff --git a/factory/builder.py b/factory/builder.py index 9d810fbe..e76e7556 100644 --- a/factory/builder.py +++ b/factory/builder.py @@ -2,7 +2,7 @@ import collections -from . import declarations, enums, errors, utils +from . import enums, errors, utils DeclarationWithContext = collections.namedtuple( 'DeclarationWithContext', @@ -134,6 +134,14 @@ def __repr__(self): return '' % self.as_dict() +def _captures_overrides(declaration_with_context): + declaration = declaration_with_context.declaration + if enums.get_builder_phase(declaration) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION: + return declaration.CAPTURE_OVERRIDES + else: + return False + + def parse_declarations(decls, base_pre=None, base_post=None): pre_declarations = base_pre.copy() if base_pre else DeclarationSet() post_declarations = base_post.copy() if base_post else DeclarationSet() @@ -156,10 +164,6 @@ def parse_declarations(decls, base_pre=None, base_post=None): # Set it as `key__` magic_key = post_declarations.join(k, '') extra_post[magic_key] = v - elif k in pre_declarations and isinstance( - pre_declarations[k].declaration, declarations.Transformer - ): - extra_maybenonpost[k] = pre_declarations[k].declaration.function(v) else: extra_maybenonpost[k] = v @@ -173,6 +177,12 @@ def parse_declarations(decls, base_pre=None, base_post=None): for k, v in extra_maybenonpost.items(): if k in post_overrides: extra_post_declarations[k] = v + elif k in pre_declarations and _captures_overrides(pre_declarations[k]): + # Send the overriding value to the existing declaration. + # By symmetry with the behaviour of PostGenerationDeclaration, + # we send it as `key__` -- i.e under the '' key. + magic_key = pre_declarations.join(k, '') + extra_pre_declarations[magic_key] = v else: # Anything else is pre_declarations extra_pre_declarations[k] = v diff --git a/factory/declarations.py b/factory/declarations.py index 150ccd89..f982fbe1 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -20,6 +20,11 @@ class BaseDeclaration(utils.OrderedBase): FACTORY_BUILDER_PHASE = enums.BuilderPhase.ATTRIBUTE_RESOLUTION + #: Whether this declaration has a special handling for call-time overrides + #: (e.g. Tranformer). + #: Overridden values will be passed in the `extra` args. + CAPTURE_OVERRIDES = False + #: Whether to unroll the context before evaluating the declaration. #: Set to False on declarations that perform their own unrolling. UNROLL_CONTEXT_BEFORE_EVALUATION = True @@ -55,8 +60,7 @@ def _unwrap_evaluate_pre(self, wrapped, *, instance, step, overrides): step=step, overrides=overrides, ) - else: - return wrapped + return wrapped def evaluate_pre(self, instance, step, overrides): context = self.unroll_context(instance, step, overrides) @@ -115,20 +119,26 @@ def evaluate(self, instance, step, extra): return self.function(instance) -class Transformer(LazyFunction): - """Transform value using given function. +class Transformer(BaseDeclaration): + CAPTURE_OVERRIDES = True + UNROLL_CONTEXT_BEFORE_EVALUATION = False - Attributes: - transform (function): returns the transformed value. - value: passed as the first argument to the transform function. - """ + def __init__(self, default, *, transform): + super().__init__() + self.default = default + self.transform = transform - def __init__(self, transform, value, *args, **kwargs): - super().__init__(transform, *args, **kwargs) - self.value = value + def evaluate_pre(self, instance, step, overrides): + # The call-time value, if present, is set under the "" key. + value_or_declaration = overrides.pop("", self.default) - def evaluate(self, instance, step, extra): - return self.function(self.value) + value = self._unwrap_evaluate_pre( + value_or_declaration, + instance=instance, + step=step, + overrides=overrides, + ) + return self.transform(value) class _UNSPECIFIED: diff --git a/factory/django.py b/factory/django.py index dee35d6a..87b6fd55 100644 --- a/factory/django.py +++ b/factory/django.py @@ -192,8 +192,8 @@ def _after_postgeneration(cls, instance, create, results=None): class Password(declarations.Transformer): - def __init__(self, password, *args, **kwargs): - super().__init__(make_password, password, *args, **kwargs) + def __init__(self, password, transform=make_password, **kwargs): + super().__init__(password, transform=transform, **kwargs) class FileField(declarations.BaseDeclaration): diff --git a/tests/test_declarations.py b/tests/test_declarations.py index c9458ffe..c49bbba7 100644 --- a/tests/test_declarations.py +++ b/tests/test_declarations.py @@ -134,7 +134,7 @@ def test_getter(self): class TransformerTestCase(unittest.TestCase): def test_transform(self): - t = declarations.Transformer(lambda x: x.upper(), 'foo') + t = declarations.Transformer('foo', transform=str.upper) self.assertEqual("FOO", utils.evaluate_declaration(t)) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index 00658454..497c02a6 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -2,7 +2,7 @@ from unittest import TestCase -from factory import Factory, Transformer +import factory class TransformCounter: @@ -22,12 +22,13 @@ def reset(cls): class Upper: - def __init__(self, name): + def __init__(self, name, **extra): self.name = name + self.extra = extra -class UpperFactory(Factory): - name = Transformer(transform, "value") +class UpperFactory(factory.Factory): + name = factory.Transformer("value", transform=transform) class Meta: model = Upper @@ -44,3 +45,163 @@ def test_transform_count(self): def test_transform_kwarg(self): self.assertEqual("TEST", UpperFactory(name="test").name) self.assertEqual(transform.calls_count, 1) + self.assertEqual("VALUE", UpperFactory().name) + self.assertEqual(transform.calls_count, 2) + + def test_transform_faker(self): + value = UpperFactory(name=factory.Faker("first_name_female", locale="fr")).name + self.assertIs(value.isupper(), True) + + def test_transform_linked(self): + value = UpperFactory( + name=factory.LazyAttribute(lambda o: o.username.replace(".", " ")), + username="john.doe", + ).name + self.assertEqual(value, "JOHN DOE") + + +class TestObject: + def __init__(self, one=None, two=None, three=None): + self.one = one + self.two = two + self.three = three + + +class TransformDeclarationFactory(factory.Factory): + class Meta: + model = TestObject + one = factory.Transformer("", transform=str.upper) + two = factory.Transformer(factory.Sequence(int), transform=lambda n: n ** 2) + + +class TransformerSequenceTest(TestCase): + def test_on_sequence(self): + instance = TransformDeclarationFactory(__sequence=2) + self.assertEqual(instance.one, "") + self.assertEqual(instance.two, 4) + self.assertIsNone(instance.three) + + def test_on_user_supplied(self): + """A transformer can wrap a call-time declaration""" + instance = TransformDeclarationFactory( + one=factory.Sequence(str), + two=2, + __sequence=2, + ) + self.assertEqual(instance.one, "2") + self.assertEqual(instance.two, 4) + self.assertIsNone(instance.three) + + +class WithMaybeFactory(factory.Factory): + class Meta: + model = TestObject + + one = True + two = factory.Maybe( + 'one', + yes_declaration=factory.Transformer("yes", transform=str.upper), + no_declaration=factory.Transformer("no", transform=str.upper), + ) + three = factory.Maybe('one', no_declaration=factory.Transformer("three", transform=str.upper)) + + +class TransformerMaybeTest(TestCase): + def test_default_transform(self): + instance = WithMaybeFactory() + self.assertIs(instance.one, True) + self.assertEqual(instance.two, "YES") + self.assertIsNone(instance.three) + + def test_yes_transform(self): + instance = WithMaybeFactory(one=True) + self.assertIs(instance.one, True) + self.assertEqual(instance.two, "YES") + self.assertIsNone(instance.three) + + def test_no_transform(self): + instance = WithMaybeFactory(one=False) + self.assertIs(instance.one, False) + self.assertEqual(instance.two, "NO") + self.assertEqual(instance.three, "THREE") + + def test_override(self): + instance = WithMaybeFactory(one=True, two="NI") + self.assertIs(instance.one, True) + self.assertEqual(instance.two, "NI") + self.assertIsNone(instance.three) + + +class RelatedTest(TestCase): + def test_default_transform(self): + cities = [] + + class City: + def __init__(self, capital_of, name): + self.capital_of = capital_of + self.name = name + cities.append(self) + + class Country: + def __init__(self, name): + self.name = name + + class CityFactory(factory.Factory): + class Meta: + model = City + + name = "Rennes" + + class CountryFactory(factory.Factory): + class Meta: + model = Country + + name = "France" + capital_city = factory.RelatedFactory( + CityFactory, + factory_related_name="capital_of", + name=factory.Transformer("Paris", transform=str.upper), + ) + + instance = CountryFactory() + self.assertEqual(instance.name, "France") + [city] = cities + self.assertEqual(city.capital_of, instance) + self.assertEqual(city.name, "PARIS") + + +class WithTraitFactory(factory.Factory): + class Meta: + model = TestObject + + class Params: + upper_two = factory.Trait( + two=factory.Transformer("two", transform=str.upper) + ) + odds = factory.Trait( + one="one", + three="three", + ) + one = factory.Transformer("one", transform=str.upper) + + +class TransformerTraitTest(TestCase): + def test_traits_off(self): + instance = WithTraitFactory() + self.assertEqual(instance.one, "ONE") + self.assertIsNone(instance.two) + self.assertIsNone(instance.three) + + def test_trait_transform_applies(self): + """A trait-provided transformer should apply to existing values""" + instance = WithTraitFactory(upper_two=True) + self.assertEqual(instance.one, "ONE") + self.assertEqual(instance.two, "TWO") + self.assertIsNone(instance.three) + + def test_trait_transform_applies_supplied(self): + """A trait-provided transformer should be overridden by caller-provided values""" + instance = WithTraitFactory(upper_two=True, two="two") + self.assertEqual(instance.one, "ONE") + self.assertEqual(instance.two, "two") + self.assertIsNone(instance.three) From f52d6cdbe222f5f01d51be6ced1ccbd07c37e197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 17 Mar 2023 17:04:25 +0100 Subject: [PATCH 652/714] Introduce Transformer.Force --- docs/reference.rst | 8 ++++++++ factory/declarations.py | 21 +++++++++++++++++++++ tests/test_transformer.py | 23 +++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 9b6b4f39..9efd1a22 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -934,6 +934,14 @@ It expects one positional argument and one keyword argument: >>> UpperFactory(name="John").name 'JOHN' +Disabling +~~~~~~~~~ +To disable a :class:`Transformer`, wrap the value in ``Transformer.Force``: + +.. code-block:: pycon + + >>> UpperFactory(name=factory.Transformer.Force("John")).name + 'John' Sequence """""""" diff --git a/factory/declarations.py b/factory/declarations.py index f982fbe1..70abe35c 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -123,6 +123,19 @@ class Transformer(BaseDeclaration): CAPTURE_OVERRIDES = True UNROLL_CONTEXT_BEFORE_EVALUATION = False + class Force: + """ + Bypass a transformer's transformation. + + The forced value can be any declaration, and will be evaluated as if it + had been passed instead of the Transformer declaration. + """ + def __init__(self, forced_value): + self.forced_value = forced_value + + def __repr__(self): + return f'Transformer.Force({repr(self.forced_value)})' + def __init__(self, default, *, transform): super().__init__() self.default = default @@ -132,12 +145,20 @@ def evaluate_pre(self, instance, step, overrides): # The call-time value, if present, is set under the "" key. value_or_declaration = overrides.pop("", self.default) + if isinstance(value_or_declaration, self.Force): + bypass_transform = True + value_or_declaration = value_or_declaration.forced_value + else: + bypass_transform = False + value = self._unwrap_evaluate_pre( value_or_declaration, instance=instance, step=step, overrides=overrides, ) + if bypass_transform: + return value return self.transform(value) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index 497c02a6..b21a8da9 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -59,6 +59,29 @@ def test_transform_linked(self): ).name self.assertEqual(value, "JOHN DOE") + def test_force_value(self): + value = UpperFactory(name=factory.Transformer.Force("Mia")).name + self.assertEqual(value, "Mia") + + def test_force_value_declaration(self): + """Pretty unlikely use case, but easy enough to cover.""" + value = UpperFactory( + name=factory.Transformer.Force( + factory.LazyFunction(lambda: "infinity") + ) + ).name + self.assertEqual(value, "infinity") + + def test_force_value_declaration_context(self): + """Ensure "forced" values run at the right level.""" + value = UpperFactory( + name=factory.Transformer.Force( + factory.LazyAttribute(lambda o: o.username.replace(".", " ")), + ), + username="john.doe", + ).name + self.assertEqual(value, "john doe") + class TestObject: def __init__(self, one=None, two=None, three=None): From 38ecbaebf51d712d4836337f7c9ef949d64e33e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 19 Jul 2023 11:03:54 +0200 Subject: [PATCH 653/714] Preparing release 3.3.0 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 73a3df6e..9e4e1fcb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.3.0 (unreleased) +3.3.0 (2023-07-19) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index 2e8749c2..be6a16d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.2.1.dev0 +version = 3.3.0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From 86b87f4275dbc799b2e5d047d04dc7f0f78193f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 19 Jul 2023 11:04:09 +0200 Subject: [PATCH 654/714] Back to development: 3.3.1 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9e4e1fcb..3236deae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,12 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. +3.3.1 (unreleased) +------------------ + +- Nothing changed yet. + + 3.3.0 (2023-07-19) ------------------ diff --git a/setup.cfg b/setup.cfg index be6a16d8..50cf3a22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.0 +version = 3.3.1.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From f0859075b048b85c8ed44c051f18070cbd20d03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 19 Jul 2023 15:27:41 +0200 Subject: [PATCH 655/714] Fix test_version That test used to hardcode the current dev version; this conflicts with the goal of storing said version only in setup.cfg. Further issues might be caused by `pip install -e .[dev]`; in that case, the check will have to be further loosened. --- tests/test_version.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_version.py b/tests/test_version.py index 52291dc4..86a57bd4 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,10 +1,19 @@ # Copyright: See the LICENSE file. +import pathlib import unittest import factory +SETUP_CFG_VERSION_PREFIX = "version =" class VersionTestCase(unittest.TestCase): + def get_setupcfg_version(self): + setup_cfg_path = pathlib.Path(__file__).parent.parent / "setup.cfg" + with setup_cfg_path.open("r") as f: + for line in f: + if line.startswith(SETUP_CFG_VERSION_PREFIX): + return line[len(SETUP_CFG_VERSION_PREFIX):].strip() + def test_version(self): - self.assertEqual(factory.__version__, "3.2.1.dev0") + self.assertEqual(factory.__version__, self.get_setupcfg_version()) From 2a371906dbd80b895b0030c301a823936eaf3a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 19 Jul 2023 15:30:11 +0200 Subject: [PATCH 656/714] Fix linting issue in test_version --- tests/test_version.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_version.py b/tests/test_version.py index 86a57bd4..4fceda4a 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -7,6 +7,7 @@ SETUP_CFG_VERSION_PREFIX = "version =" + class VersionTestCase(unittest.TestCase): def get_setupcfg_version(self): setup_cfg_path = pathlib.Path(__file__).parent.parent / "setup.cfg" From f83e0f672039de0c6b7e5ba7d738cccdd592ca57 Mon Sep 17 00:00:00 2001 From: Jessica Gadling Date: Mon, 24 Jul 2023 13:33:57 -0700 Subject: [PATCH 657/714] Don't require setting sqlalchemy_session to None when using a factory. --- factory/alchemy.py | 7 +++++-- tests/test_alchemy.py | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index 4e7ad6a2..f934ce5d 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -24,8 +24,11 @@ def _check_sqlalchemy_session_persistence(self, meta, value): @staticmethod def _check_has_sqlalchemy_session_set(meta, value): - if value and meta.sqlalchemy_session: - raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both") + try: + if value and meta.sqlalchemy_session: + raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both") + except AttributeError: + pass def _build_default_options(self): return super()._build_default_options() + [ diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 9cac036b..19e4f5ee 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -286,7 +286,6 @@ def test_create_get_session_from_sqlalchemy_session_factory(self): class SessionGetterFactory(SQLAlchemyModelFactory): class Meta: model = models.StandardModel - sqlalchemy_session = None sqlalchemy_session_factory = lambda: models.session id = factory.Sequence(lambda n: n) From e02146f4bf0f8876ccbed34f0c6ebcd2712f1271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 31 Jul 2023 19:00:42 +0200 Subject: [PATCH 658/714] Add changelog entry for f83e0f672039de0c6b7e5ba7d738cccdd592ca57 Missed in #1032. --- docs/changelog.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 3236deae..6a397b89 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,10 @@ ChangeLog 3.3.1 (unreleased) ------------------ -- Nothing changed yet. +*Bugfix:* + +- :issue:`1031`: Do not require :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session` when + :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session_factory` is provided. 3.3.0 (2023-07-19) From 7e4ac8620b7b0f95c9bca4f1ad0174f8e547792d Mon Sep 17 00:00:00 2001 From: Samuel Searles-Bryant Date: Fri, 11 Aug 2023 18:18:43 +0100 Subject: [PATCH 659/714] Add version added to docs for `Transformer` This was added in version 3.3.0, but has no marker that says that. This change adds the marker to make it clear to users reading the 'stable' docs that this might not be available in their version. --- docs/reference.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference.rst b/docs/reference.rst index 9efd1a22..238b56c4 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -909,6 +909,8 @@ Transformer .. class:: Transformer(default_value, *, transform) + .. versionadded:: 3.3.0 + A :class:`Transformer` applies a ``transform`` function to the provided value before to set the transformed value on the generated object. From f9574a1dcfca232e7fe1ed63bb6ac447e1f754ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 4 Sep 2023 10:50:23 +0200 Subject: [PATCH 660/714] Add support for Django 4.2 --- docs/changelog.rst | 3 +++ setup.cfg | 1 + tox.ini | 1 + 3 files changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6a397b89..f71f3ff2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,9 @@ ChangeLog 3.3.1 (unreleased) ------------------ +*New:* + +- Add support for Django 4.2 *Bugfix:* diff --git a/setup.cfg b/setup.cfg index 50cf3a22..e3be4711 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ classifiers = Framework :: Django :: 3.2 Framework :: Django :: 4.0 Framework :: Django :: 4.1 + Framework :: Django :: 4.2 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index a234315f..db1b1398 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,7 @@ deps = django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 + django42: Django>=4.2,<5.0 djangomain: https://github.com/django/django/archive/main.tar.gz py{37,38,39,310,311}-postgres: psycopg2-binary pypy{37,38,39}-postgres: psycopg2cffi From 63c2df43ea640adb08d68c2b09c542667691f588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 4 Sep 2023 14:12:56 +0200 Subject: [PATCH 661/714] Drop support for Django 4.0 --- docs/changelog.rst | 3 +++ setup.cfg | 1 - tox.ini | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f71f3ff2..bc8606cb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,9 @@ ChangeLog - :issue:`1031`: Do not require :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session` when :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session_factory` is provided. +*Removed:* + +- Drop support for Django 4.0 3.3.0 (2023-07-19) ------------------ diff --git a/setup.cfg b/setup.cfg index e3be4711..49dd0d51 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 3.2 - Framework :: Django :: 4.0 Framework :: Django :: 4.1 Framework :: Django :: 4.2 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index db1b1398..c9a03758 100644 --- a/tox.ini +++ b/tox.ini @@ -38,7 +38,6 @@ deps = mongo: mongoengine django{32,40,41,main}: Pillow django32: Django>=3.2,<3.3 - django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 djangomain: https://github.com/django/django/archive/main.tar.gz From fb744fd96005291b98fb354632d3c17232e238bd Mon Sep 17 00:00:00 2001 From: sarahboyce Date: Tue, 19 Sep 2023 16:49:43 +0200 Subject: [PATCH 662/714] Fixed mute_signals restoring signals receiver order. --- factory/django.py | 2 +- tests/test_django.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/factory/django.py b/factory/django.py index 87b6fd55..9526b775 100644 --- a/factory/django.py +++ b/factory/django.py @@ -311,7 +311,7 @@ def __exit__(self, exc_type, exc_value, traceback): logger.debug('mute_signals: Restoring signal handlers %r', receivers) - signal.receivers += receivers + signal.receivers = receivers + signal.receivers with signal.lock: # Django uses some caching for its signals. # Since we're bypassing signal.connect and signal.disconnect, diff --git a/tests/test_django.py b/tests/test_django.py index 19729e07..d2615e29 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -941,6 +941,22 @@ def test_receiver_created_during_model_instantiation_is_not_lost(self): self.assertTrue(self.handlers.created_during_instantiation.called) + def test_signal_receiver_order_restored_after_mute_signals(self): + def must_be_first(*args, **kwargs): + self.handlers.do_stuff(1) + + def must_be_second(*args, **kwargs): + self.handlers.do_stuff(2) + + signals.post_save.connect(must_be_first) + with factory.django.mute_signals(signals.post_save): + WithSignalsFactory(post_save_signal_receiver=must_be_second) + self.handlers.do_stuff.assert_has_calls([mock.call(2)]) + + self.handlers.reset_mock() + WithSignalsFactory(post_save_signal_receiver=must_be_second) + self.handlers.do_stuff.assert_has_calls([mock.call(1), mock.call(2)]) + def test_signal_cache(self): with factory.django.mute_signals(signals.pre_save, signals.post_save): signals.post_save.connect(self.handlers.mute_block_receiver) From 5eb79fe7ecd63b6e6f798d38c312a2ee23bd38ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 19 Sep 2023 18:02:00 +0200 Subject: [PATCH 663/714] Tighten assertions --- tests/test_django.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_django.py b/tests/test_django.py index d2615e29..066d7920 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -951,11 +951,11 @@ def must_be_second(*args, **kwargs): signals.post_save.connect(must_be_first) with factory.django.mute_signals(signals.post_save): WithSignalsFactory(post_save_signal_receiver=must_be_second) - self.handlers.do_stuff.assert_has_calls([mock.call(2)]) + self.assertEqual(self.handlers.do_stuff.call_args_list, [mock.call(2)]) self.handlers.reset_mock() WithSignalsFactory(post_save_signal_receiver=must_be_second) - self.handlers.do_stuff.assert_has_calls([mock.call(1), mock.call(2)]) + self.assertEqual(self.handlers.do_stuff.call_args_list, [mock.call(1), mock.call(2)]) def test_signal_cache(self): with factory.django.mute_signals(signals.pre_save, signals.post_save): From 3ea1117aa9a313d8619541ea4575d7628095ff53 Mon Sep 17 00:00:00 2001 From: Jaap Roes Date: Wed, 20 Sep 2023 16:08:35 +0200 Subject: [PATCH 664/714] Add note about creating unusable passwords --- docs/orms.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index 3839707c..5967e1bc 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -128,7 +128,11 @@ Extra fields .. method:: __init__(self, password) - :param str password: Default password. + :param str or None password: Default password. + + .. note:: When the ``password`` argument is ``None``, the resulting password is + unusable as if ``set_unusable_password()`` were used. This is distinct + from setting the password to an empty string. .. code-block:: python @@ -149,6 +153,10 @@ Extra fields >>> other_user = UserFactory.create(password='other_pw') >>> check_password('other_pw', other_user.password) True + >>> # Set unusable password + >>> no_password_user = UserFactory.create(password=None) + >>> no_password_user.has_usable_password() + False .. class:: FileField From 45d7a95c84ac3d7767e3e5428023addc4c77f38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Wed, 4 Oct 2023 10:54:03 +0200 Subject: [PATCH 665/714] Add support for Python 3.12 --- .github/workflows/test.yml | 1 + Makefile | 1 + docs/changelog.rst | 1 + setup.cfg | 1 + tox.ini | 1 + 5 files changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f50bd24c..77ff9a18 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" - "pypy-3.7" - "pypy-3.8" - "pypy-3.9" diff --git a/Makefile b/Makefile index ad35a6fc..927689a9 100644 --- a/Makefile +++ b/Makefile @@ -64,6 +64,7 @@ test: -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ -Wdefault:"'cgi' is deprecated and slated for removal in Python 3.13":DeprecationWarning:: \ + -Wdefault:"datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version.":DeprecationWarning:: \ -m unittest # DOC: Test the examples diff --git a/docs/changelog.rst b/docs/changelog.rst index bc8606cb..036254e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ ChangeLog *New:* - Add support for Django 4.2 +- Add support for Python 3.12 *Bugfix:* diff --git a/setup.cfg b/setup.cfg index 49dd0d51..8a9c4f4e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ classifiers = Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing diff --git a/tox.ini b/tox.ini index c9a03758..287ea48c 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ python = 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 pypy-3.7: pypy37 pypy-3.8: pypy38 pypy-3.9: pypy39 From 1c99a464588046737f1b34ddf678d78cd6683511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Mon, 17 Jul 2023 14:59:02 +0200 Subject: [PATCH 666/714] Remove support for EOL Python 3.7 --- .github/workflows/test.yml | 2 -- Makefile | 1 - docs/changelog.rst | 1 + readthedocs.yml | 6 ++++++ setup.cfg | 4 +--- tox.ini | 10 ++++------ 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 77ff9a18..06fe75c3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,13 +17,11 @@ jobs: fail-fast: false matrix: python-version: - - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" - - "pypy-3.7" - "pypy-3.8" - "pypy-3.9" database-type: diff --git a/Makefile b/Makefile index 927689a9..a31a9fb8 100644 --- a/Makefile +++ b/Makefile @@ -59,7 +59,6 @@ test: -X dev \ -Werror \ -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ - -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working":DeprecationWarning:: \ -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ diff --git a/docs/changelog.rst b/docs/changelog.rst index 036254e8..7c34c589 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -67,6 +67,7 @@ ChangeLog - Drop support for Django 3.0 - Drop support for Django 3.1 - Drop support for Python 3.6 + - Drop support for Python 3.7 3.2.1 (2021-10-26) ------------------ diff --git a/readthedocs.yml b/readthedocs.yml index 57cad8d1..883f654f 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -1,5 +1,11 @@ --- version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + python: install: - method: pip diff --git a/setup.cfg b/setup.cfg index 8a9c4f4e..bc242d4f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,6 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 @@ -37,10 +36,9 @@ classifiers = [options] packages = factory -python_requires = >=3.7 +python_requires = >=3.8 install_requires = Faker>=0.7.0 - importlib_metadata;python_version<"3.8" [options.extras_require] dev = diff --git a/tox.ini b/tox.ini index 287ea48c..d4873869 100644 --- a/tox.ini +++ b/tox.ini @@ -5,21 +5,19 @@ envlist = docs examples linkcheck - py{37,38,39,310,311,py37,py38,py39}-sqlite - py{37,38,39,310,311,py37,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,py38,py39}-sqlite + py{38,39,310,311,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py38,py39}-django41-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} [gh-actions] python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 - pypy-3.7: pypy37 pypy-3.8: pypy38 pypy-3.9: pypy39 @@ -42,8 +40,8 @@ deps = django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 djangomain: https://github.com/django/django/archive/main.tar.gz - py{37,38,39,310,311}-postgres: psycopg2-binary - pypy{37,38,39}-postgres: psycopg2cffi + py{38,39,310,311}-postgres: psycopg2-binary + pypy{38,39}-postgres: psycopg2cffi setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings From 504404713d32ec6c9295f77737a85967cbd7a325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 16 Nov 2023 20:43:24 +0100 Subject: [PATCH 667/714] Add support for Django 5.0 --- docs/changelog.rst | 1 + setup.cfg | 1 + tox.ini | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7c34c589..b98bcfce 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,6 +8,7 @@ ChangeLog *New:* - Add support for Django 4.2 +- Add support for Django 5.0 - Add support for Python 3.12 *Bugfix:* diff --git a/setup.cfg b/setup.cfg index bc242d4f..3ba2b7aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ classifiers = Framework :: Django :: 3.2 Framework :: Django :: 4.1 Framework :: Django :: 4.2 + Framework :: Django :: 5.0 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index d4873869..efe79b67 100644 --- a/tox.ini +++ b/tox.ini @@ -35,10 +35,11 @@ deps = alchemy: SQLAlchemy alchemy: sqlalchemy_utils mongo: mongoengine - django{32,40,41,main}: Pillow + django{32,41,42,50,main}: Pillow django32: Django>=3.2,<3.3 django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 + django50: Django>=5.0b1,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz py{38,39,310,311}-postgres: psycopg2-binary pypy{38,39}-postgres: psycopg2cffi From cbbc20e5c866fa23aac11e4c76b018af7e3144c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Fri, 17 Nov 2023 15:04:26 +0100 Subject: [PATCH 668/714] Point License badge to the License on GitHub Instead of going to the root of the project on PyPI. Co-authored-by: irtazaakram --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index e5ae4112..3d3677bf 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ factory_boy :alt: Wheel status .. image:: https://img.shields.io/pypi/l/factory_boy.svg - :target: https://pypi.org/project/factory-boy/ + :target: https://github.com/FactoryBoy/factory_boy/blob/master/LICENSE :alt: License factory_boy is a fixtures replacement based on thoughtbot's `factory_bot `_. From 27c0196bdcff334caf505a1d115341641c54e68a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 6 Dec 2023 14:23:43 +0100 Subject: [PATCH 669/714] GitHub Actions: Test PyPy 3.10 Also, upgrade GitHub Actions `checkout`. --- .github/workflows/test.yml | 4 ++-- tox.ini | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06fe75c3..6a0985ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,8 +22,8 @@ jobs: - "3.10" - "3.11" - "3.12" - - "pypy-3.8" - "pypy-3.9" + - "pypy-3.10" database-type: - "sqlite" - "postgres" @@ -42,7 +42,7 @@ jobs: POSTGRES_PASSWORD: password steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 diff --git a/tox.ini b/tox.ini index efe79b67..256a88d6 100644 --- a/tox.ini +++ b/tox.ini @@ -5,10 +5,10 @@ envlist = docs examples linkcheck - py{38,39,310,311,py38,py39}-sqlite - py{38,39,310,311,py38,py39}-django32-mongo-alchemy-{sqlite,postgres} - py{38,39,310,311,py38,py39}-django40-mongo-alchemy-{sqlite,postgres} - py{38,39,310,311,py38,py39}-django41-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,py39,py310}-sqlite + py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,py39,py310}-django40-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,py39,py310}-django41-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} [gh-actions] @@ -18,8 +18,8 @@ python = 3.10: py310 3.11: py311 3.12: py312 - pypy-3.8: pypy38 pypy-3.9: pypy39 + pypy-3.10: pypy310 [gh-actions:env] DATABASE_TYPE = @@ -39,10 +39,10 @@ deps = django32: Django>=3.2,<3.3 django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 - django50: Django>=5.0b1,<5.1 + django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz py{38,39,310,311}-postgres: psycopg2-binary - pypy{38,39}-postgres: psycopg2cffi + pypy{39,310}-postgres: psycopg2cffi setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings From eda50227855c46c66006e74cad0b32a2d6f98044 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 6 Dec 2023 22:32:06 +0100 Subject: [PATCH 670/714] actions/setup-python@v5 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6a0985ea..dc279ad4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: pip From 5aaf869bafcb7e9ce4eeda4db876cab163eee973 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 6 Dec 2023 14:58:12 +0100 Subject: [PATCH 671/714] tox.ini: Enable testing on Django v5.0 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 256a88d6..ab6cb343 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py39,py310}-django40-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py39,py310}-django41-mongo-alchemy-{sqlite,postgres} + py{310,311,312}-django50-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} [gh-actions] @@ -41,7 +42,7 @@ deps = django42: Django>=4.2,<5.0 django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz - py{38,39,310,311}-postgres: psycopg2-binary + py{38,39,310,311,312}-postgres: psycopg2-binary pypy{39,310}-postgres: psycopg2cffi setenv = From 36eb3cad20617f8a8ecb569d914cc608df314309 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 6 Dec 2023 15:10:54 +0100 Subject: [PATCH 672/714] Django v4.2 now supports Python v3.12 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ab6cb343..314f40b0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ envlist = docs examples linkcheck - py{38,39,310,311,py39,py310}-sqlite + py{38,39,310,311,312,py39,py310}-sqlite py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py39,py310}-django40-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py39,py310}-django41-mongo-alchemy-{sqlite,postgres} From 8a50a8ed1badfe5675e93b9fc3e1ca4905862b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 21 Dec 2023 14:07:52 +0100 Subject: [PATCH 673/714] Update envlist for Django 4.2 and drop unsupported Django 4.0 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 314f40b0..0f12ef01 100644 --- a/tox.ini +++ b/tox.ini @@ -7,8 +7,8 @@ envlist = linkcheck py{38,39,310,311,312,py39,py310}-sqlite py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} - py{38,39,310,311,py39,py310}-django40-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py39,py310}-django41-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,312,py39,py310}-django42-mongo-alchemy-{sqlite,postgres} py{310,311,312}-django50-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} From 69809cfc74cb31519d2558722bbce5a9123c2d11 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 21 Dec 2023 19:19:37 +0100 Subject: [PATCH 674/714] Disable py{py39,py310}-django42-mongo-alchemy-postgres --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0f12ef01..9010d318 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,9 @@ envlist = py{38,39,310,311,312,py39,py310}-sqlite py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,py39,py310}-django41-mongo-alchemy-{sqlite,postgres} - py{38,39,310,311,312,py39,py310}-django42-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,312}-django42-mongo-alchemy-{sqlite,postgres} + py{py39,py310}-django42-mongo-alchemy-sqlite, + # py{py39,py310}-django42-mongo-alchemy-postgres # TODO: Fix me! py{310,311,312}-django50-mongo-alchemy-{sqlite,postgres} py310-djangomain-mongo-alchemy-{sqlite,postgres} From 68de8e75c6862588dd265d96567bcf34c079186b Mon Sep 17 00:00:00 2001 From: Serg Tereshchenko Date: Wed, 12 Jan 2022 18:41:00 +0200 Subject: [PATCH 675/714] Add basic typing support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only `Factory.build()` and `Factory.create()` are properly typed, provided the class is declared as `class UserFactory(Factory[User]):`. Relies on mypy for tests. Reviewed-By: Raphaël Barrois --- Makefile | 1 + docs/changelog.rst | 1 + factory/__init__.py | 6 ++++-- factory/base.py | 21 ++++++++++++++------- factory/django.py | 7 ++++--- factory/faker.py | 3 ++- setup.cfg | 1 + tests/test_typing.py | 31 +++++++++++++++++++++++++++++++ tox.ini | 1 + 9 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 tests/test_typing.py diff --git a/Makefile b/Makefile index a31a9fb8..0dbae867 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,7 @@ testall: # DOC: Run tests for the currently installed version # Remove cgi warning when dropping support for Django<=4.1. test: + mypy --ignore-missing-imports tests/test_typing.py python \ -b \ -X dev \ diff --git a/docs/changelog.rst b/docs/changelog.rst index b98bcfce..6397f107 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ ChangeLog - Add support for Django 4.2 - Add support for Django 5.0 - Add support for Python 3.12 +- :issue:`903`: Add basic typing annotations *Bugfix:* diff --git a/factory/__init__.py b/factory/__init__.py index bdc3ac0d..8b26dddc 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,5 +1,7 @@ # Copyright: See the LICENSE file. +import sys + from .base import ( BaseDictFactory, BaseListFactory, @@ -70,10 +72,10 @@ pass __author__ = 'Raphaël Barrois ' -try: +if sys.version_info >= (3, 8): # Python 3.8+ import importlib.metadata as importlib_metadata -except ImportError: +else: import importlib_metadata __version__ = importlib_metadata.version("factory_boy") diff --git a/factory/base.py b/factory/base.py index 36b2359a..8d499501 100644 --- a/factory/base.py +++ b/factory/base.py @@ -4,11 +4,14 @@ import collections import logging import warnings +from typing import Generic, List, Type, TypeVar from . import builder, declarations, enums, errors, utils logger = logging.getLogger('factory.generate') +T = TypeVar('T') + # Factory metaclasses @@ -405,7 +408,7 @@ def reset(self, next_value=0): self.seq = next_value -class BaseFactory: +class BaseFactory(Generic[T]): """Factory base support for sequences, attributes and stubs.""" # Backwards compatibility @@ -506,12 +509,12 @@ def _create(cls, model_class, *args, **kwargs): return model_class(*args, **kwargs) @classmethod - def build(cls, **kwargs): + def build(cls, **kwargs) -> T: """Build an instance of the associated class, with overridden attrs.""" return cls._generate(enums.BUILD_STRATEGY, kwargs) @classmethod - def build_batch(cls, size, **kwargs): + def build_batch(cls, size: int, **kwargs) -> List[T]: """Build a batch of instances of the given class, with overridden attrs. Args: @@ -523,12 +526,12 @@ def build_batch(cls, size, **kwargs): return [cls.build(**kwargs) for _ in range(size)] @classmethod - def create(cls, **kwargs): + def create(cls, **kwargs) -> T: """Create an instance of the associated class, with overridden attrs.""" return cls._generate(enums.CREATE_STRATEGY, kwargs) @classmethod - def create_batch(cls, size, **kwargs): + def create_batch(cls, size: int, **kwargs) -> List[T]: """Create a batch of instances of the given class, with overridden attrs. Args: @@ -627,18 +630,22 @@ def simple_generate_batch(cls, create, size, **kwargs): return cls.generate_batch(strategy, size, **kwargs) -class Factory(BaseFactory, metaclass=FactoryMetaClass): +class Factory(BaseFactory[T], metaclass=FactoryMetaClass): """Factory base with build and create support. This class has the ability to support multiple ORMs by using custom creation functions. """ + # Backwards compatibility + AssociatedClassError: Type[Exception] + class Meta(BaseMeta): pass -# Backwards compatibility +# Add the association after metaclass execution. +# Otherwise, AssociatedClassError would be detected as a declaration. Factory.AssociatedClassError = errors.AssociatedClassError diff --git a/factory/django.py b/factory/django.py index 9526b775..b53fd5b5 100644 --- a/factory/django.py +++ b/factory/django.py @@ -9,6 +9,7 @@ import logging import os import warnings +from typing import Dict, TypeVar from django.contrib.auth.hashers import make_password from django.core import files as django_files @@ -20,9 +21,9 @@ DEFAULT_DB_ALIAS = 'default' # Same as django.db.DEFAULT_DB_ALIAS +T = TypeVar("T") - -_LAZY_LOADS = {} +_LAZY_LOADS: Dict[str, object] = {} def get_model(app, model): @@ -72,7 +73,7 @@ def get_model_class(self): return self.model -class DjangoModelFactory(base.Factory): +class DjangoModelFactory(base.Factory[T]): """Factory for Django models. This makes sure that the 'sequence' field of created objects is a new id. diff --git a/factory/faker.py b/factory/faker.py index 6ed2e28c..88ae644c 100644 --- a/factory/faker.py +++ b/factory/faker.py @@ -14,6 +14,7 @@ class Meta: import contextlib +from typing import Dict import faker import faker.config @@ -47,7 +48,7 @@ def evaluate(self, instance, step, extra): subfaker = self._get_faker(locale) return subfaker.format(self.provider, **extra) - _FAKER_REGISTRY = {} + _FAKER_REGISTRY: Dict[str, faker.Faker] = {} _DEFAULT_LOCALE = faker.config.DEFAULT_LOCALE @classmethod diff --git a/setup.cfg b/setup.cfg index 3ba2b7aa..13b09b91 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,6 +47,7 @@ dev = Django flake8 isort + mypy Pillow SQLAlchemy sqlalchemy_utils diff --git a/tests/test_typing.py b/tests/test_typing.py new file mode 100644 index 00000000..c2f8b564 --- /dev/null +++ b/tests/test_typing.py @@ -0,0 +1,31 @@ +# Copyright: See the LICENSE file. + +import dataclasses +import unittest + +import factory + + +@dataclasses.dataclass +class User: + name: str + email: str + id: int + + +class TypingTests(unittest.TestCase): + + def test_simple_factory(self) -> None: + + class UserFactory(factory.Factory[User]): + name = "John Doe" + email = "john.doe@example.org" + id = 42 + + class Meta: + model = User + + result: User + result = UserFactory.build() + result = UserFactory.create() + self.assertEqual(result.name, "John Doe") diff --git a/tox.ini b/tox.ini index 9010d318..d842c759 100644 --- a/tox.ini +++ b/tox.ini @@ -35,6 +35,7 @@ passenv = POSTGRES_HOST POSTGRES_DATABASE deps = + mypy alchemy: SQLAlchemy alchemy: sqlalchemy_utils mongo: mongoengine From 121d3f1fa55ca6c2ee44a7dcfb96242b0133403f Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 6 Feb 2024 10:56:44 +0100 Subject: [PATCH 676/714] Remove unnecessary py3.8 compat path --- factory/__init__.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/factory/__init__.py b/factory/__init__.py index 8b26dddc..62042a2a 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,6 +1,6 @@ # Copyright: See the LICENSE file. -import sys +import importlib.metadata from .base import ( BaseDictFactory, @@ -72,10 +72,4 @@ pass __author__ = 'Raphaël Barrois ' -if sys.version_info >= (3, 8): - # Python 3.8+ - import importlib.metadata as importlib_metadata -else: - import importlib_metadata - -__version__ = importlib_metadata.version("factory_boy") +__version__ = importlib.metadata.version("factory_boy") From 159576788e6a553610bf11a045ed32021d34ebfe Mon Sep 17 00:00:00 2001 From: ThTomate Date: Sat, 2 Mar 2024 19:03:20 +0100 Subject: [PATCH 677/714] =?UTF-8?q?Docs:=20Remove=20old/wrong=20hint=20on?= =?UTF-8?q?=20create(=E2=80=A6)=20being=20used=20internally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference.rst | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/reference.rst b/docs/reference.rst index 238b56c4..130e194c 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -576,17 +576,6 @@ factory_boy supports two main strategies for generating instances, plus stubs. >>> obj.save() >>> return obj - .. OHAI_VIM* - - .. warning:: For backward compatibility reasons, the default behavior of - factory_boy is to call ``MyClass.objects.create(*args, **kwargs)`` - when using the ``create`` strategy. - - That policy will be used if the - :attr:`associated class ` has an ``objects`` - attribute *and* the :meth:`~Factory._create` classmethod of the - :class:`Factory` wasn't overridden. - .. function:: use_strategy(strategy) From dca3a16e9a4f731a3fe217e0ef6fa558796110aa Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Wed, 17 Jan 2024 08:59:23 +0100 Subject: [PATCH 678/714] Improve docstrings of `build`/`create` classmethods --- factory/base.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/factory/base.py b/factory/base.py index 8d499501..454513be 100644 --- a/factory/base.py +++ b/factory/base.py @@ -510,13 +510,18 @@ def _create(cls, model_class, *args, **kwargs): @classmethod def build(cls, **kwargs) -> T: - """Build an instance of the associated class, with overridden attrs.""" + """Build an instance of the associated class, with overridden attrs. + + The instance will not be saved and persisted to any datastore. + """ return cls._generate(enums.BUILD_STRATEGY, kwargs) @classmethod def build_batch(cls, size: int, **kwargs) -> List[T]: """Build a batch of instances of the given class, with overridden attrs. + The instances will not be saved and persisted to any datastore. + Args: size (int): the number of instances to build @@ -527,13 +532,18 @@ def build_batch(cls, size: int, **kwargs) -> List[T]: @classmethod def create(cls, **kwargs) -> T: - """Create an instance of the associated class, with overridden attrs.""" + """Create an instance of the associated class, with overridden attrs. + + The instance will be saved and persisted in the appropriate datastore. + """ return cls._generate(enums.CREATE_STRATEGY, kwargs) @classmethod def create_batch(cls, size: int, **kwargs) -> List[T]: """Create a batch of instances of the given class, with overridden attrs. + The instances will be saved and persisted in the appropriate datastore. + Args: size (int): the number of instances to create From bcfc8ec3f2f59b65b86b0f6218da6f8fd1e32657 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 2 May 2023 22:46:20 +0200 Subject: [PATCH 679/714] Use dependabot to update GitHub actions Co-authored-by: Christian Clauss --- .github/dependabot.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..d4880e03 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + GitHub_Actions: + patterns: + - "*" # Group all Actions updates into a single larger pull request From 85d5cc189b158012016089057cdb0173ea996f76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 22:45:29 +0000 Subject: [PATCH 680/714] Bump the github_actions group with 2 updates Bumps the github_actions group with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 3 to 4 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) Updates `actions/setup-python` from 4 to 5 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major dependency-group: github_actions - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major dependency-group: github_actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check.yml | 4 ++-- .github/workflows/linkcheck.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index a06aae03..6493ac71 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -25,10 +25,10 @@ jobs: TOXENV: ${{ matrix.tox-environment }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3' cache: pip diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index bbfdafd3..a8b6e7de 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -9,10 +9,10 @@ jobs: name: Linkcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3' From a81beb4d49efc21542afb67b6b9c19c1442af89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 19 Mar 2024 11:23:20 +0100 Subject: [PATCH 681/714] Drop support for Django 4.1 Upstream dropped support in January 2024. --- Makefile | 2 +- docs/changelog.rst | 1 + setup.cfg | 1 - tox.ini | 4 +--- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 0dbae867..9e0210bc 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ testall: tox # DOC: Run tests for the currently installed version -# Remove cgi warning when dropping support for Django<=4.1. +# Remove cgi warning when dropping support for Django 3.2. test: mypy --ignore-missing-imports tests/test_typing.py python \ diff --git a/docs/changelog.rst b/docs/changelog.rst index 6397f107..c5fc0767 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,7 @@ ChangeLog *Removed:* - Drop support for Django 4.0 +- Stop advertising and verifying support for Django 4.1 3.3.0 (2023-07-19) ------------------ diff --git a/setup.cfg b/setup.cfg index 13b09b91..4f93b98c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,6 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 3.2 - Framework :: Django :: 4.1 Framework :: Django :: 4.2 Framework :: Django :: 5.0 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index d842c759..17dfdb50 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,6 @@ envlist = linkcheck py{38,39,310,311,312,py39,py310}-sqlite py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} - py{38,39,310,311,py39,py310}-django41-mongo-alchemy-{sqlite,postgres} py{38,39,310,311,312}-django42-mongo-alchemy-{sqlite,postgres} py{py39,py310}-django42-mongo-alchemy-sqlite, # py{py39,py310}-django42-mongo-alchemy-postgres # TODO: Fix me! @@ -39,9 +38,8 @@ deps = alchemy: SQLAlchemy alchemy: sqlalchemy_utils mongo: mongoengine - django{32,41,42,50,main}: Pillow + django{32,42,50,main}: Pillow django32: Django>=3.2,<3.3 - django41: Django>=4.1,<4.2 django42: Django>=4.2,<5.0 django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz From 7ed1e5417e06c3d83e0495a67508b3868de53823 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Tue, 23 Apr 2024 17:01:40 -0600 Subject: [PATCH 682/714] Don't duplicate CI checks on PRs By default the `pull_request` trigger will run on every push to the PR branch. So we are wasting CI minutes and electricity by running our CI checks twice on every push to a PR branch. Instead, this makes checks run 1x per PR, and then also run on every merge to `master`, to ensure that `master` stays green. This latter check is normally useless, but occasionally if there's drift of some kind between when CI runs on a PR and when it's merged, then this can help identify the issue. A more common pattern is simply to only run on PR's, but given we haven't previously been enforcing "only merge via PR" (https://github.com/FactoryBoy/factory_boy/issues/1073) I thought might be best to keep checking `master` as well until that's changed. The one thing we stop doing with this change is checking on push to branches that aren't PR branches... ie, if a maintainer is working on testing something. But they may not even care about running CI on this branch, and if they do, it's easy to run the tests locally, or open a draft PR...So I don't see the point of preserving that behavior. --- .github/workflows/check.yml | 6 ++++-- .github/workflows/test.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 6493ac71..9d837066 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,8 +1,10 @@ name: Check on: - - push - - pull_request + push: + branches: + - "master" + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc279ad4..9d734533 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,10 @@ name: Test on: - - push - - pull_request + push: + branches: + - "master" + pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 01c0a73e5c28bf6e6e35def5caf286ec8d292168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Skar=C5=BCy=C5=84ski?= Date: Tue, 28 Mar 2023 16:13:12 +0200 Subject: [PATCH 683/714] test: add regression test for #965 --- tests/test_regression.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_regression.py b/tests/test_regression.py index a9ea1c66..2cca0bda 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -51,3 +51,24 @@ class Params: unknown_author = AuthorFactory(unknown=True) self.assertEqual("", unknown_author.fullname) + + def test_evaluated_without_locale(self): + """Regression test for `KeyError: 'locale'` raised in `evaluate`. + + See #965 + + """ + class AuthorFactory(factory.Factory): + fullname = factory.Faker("name") + pseudonym = factory.Maybe( + decider=factory.Faker("pybool"), + yes_declaration="yes", + no_declaration="no", + ) + + class Meta: + model = Author + + author = AuthorFactory() + + self.assertIn(author.pseudonym, ["yes", "no"]) From d6349de49b647bb8b7f5d620757a919463f27f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Tue, 18 Apr 2023 21:38:00 +0200 Subject: [PATCH 684/714] Call evaluate_pre() instead of evaluate() on Maybe decider e19142cb6e049e079cd4af36775715fcda47cb8c introduced evaluate_pre to perform context unrolling before to call the semi-public evaluate(). The Maybe decider was not updated at that time, but its context need to be unrolled. --- factory/declarations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/declarations.py b/factory/declarations.py index 70abe35c..951b45f3 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -536,7 +536,7 @@ def evaluate_post(self, instance, step, overrides): return target def evaluate_pre(self, instance, step, overrides): - choice = self.decider.evaluate(instance=instance, step=step, extra={}) + choice = self.decider.evaluate_pre(instance=instance, step=step, overrides={}) target = self.yes if choice else self.no # The value can't be POST_INSTANTIATION, checked in __init__; # evaluate it as `evaluate_pre` From 8aaa29be8ad67abf5649a55d88478373c7db44cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 Aug 2024 17:47:44 +0200 Subject: [PATCH 685/714] Improve readability of alchemy checker Avoid broad `except AttributeError`. --- factory/alchemy.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/factory/alchemy.py b/factory/alchemy.py index f934ce5d..e782fbd8 100644 --- a/factory/alchemy.py +++ b/factory/alchemy.py @@ -24,11 +24,8 @@ def _check_sqlalchemy_session_persistence(self, meta, value): @staticmethod def _check_has_sqlalchemy_session_set(meta, value): - try: - if value and meta.sqlalchemy_session: - raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both") - except AttributeError: - pass + if value is not None and getattr(meta, "sqlalchemy_session", None) is not None: + raise RuntimeError("Provide either a sqlalchemy_session or a sqlalchemy_session_factory, not both") def _build_default_options(self): return super()._build_default_options() + [ From f8456f5e3965bd1d9ea1dd5b6299e0d73783c382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 18:47:44 +0200 Subject: [PATCH 686/714] Remove various (obsolete) warning exemption flags Most of those warnings are no longer an issue with current dependency versions. --- Makefile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Makefile b/Makefile index 9e0210bc..4d52a184 100644 --- a/Makefile +++ b/Makefile @@ -59,12 +59,6 @@ test: -b \ -X dev \ -Werror \ - -Wdefault:"the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses":DeprecationWarning:distutils: \ - -Wdefault:"Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated since Python 3.3, and in 3.9 it will stop working":DeprecationWarning:: \ - -Wdefault:"set_output_charset() is deprecated":DeprecationWarning:: \ - -Wdefault:"parameter codeset is deprecated":DeprecationWarning:: \ - -Wdefault:"'cgi' is deprecated and slated for removal in Python 3.13":DeprecationWarning:: \ - -Wdefault:"datetime.datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version.":DeprecationWarning:: \ -m unittest # DOC: Test the examples From 819acce99374b55ba9c57a224669b7c8920ed474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 18:50:23 +0200 Subject: [PATCH 687/714] Run the test suite against Mongomock See: #1083 Closes: #1081 --- .github/workflows/test.yml | 5 ----- Makefile | 3 +++ docs/changelog.rst | 1 + docs/spelling_wordlist.txt | 1 + setup.cfg | 1 + tests/test_mongoengine.py | 3 +++ tox.ini | 4 +++- 7 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d734533..29a7e881 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,11 +31,6 @@ jobs: - "postgres" services: - mongodb: - image: mongo - ports: - - 27017:27017 - postgresdb: image: postgres:alpine ports: diff --git a/Makefile b/Makefile index 4d52a184..778fd785 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,9 @@ test: -b \ -X dev \ -Werror \ + -Wignore:::mongomock: \ + -Wignore:::mongomock.__version__: \ + -Wignore:::pkg_resources: \ -m unittest # DOC: Test the examples diff --git a/docs/changelog.rst b/docs/changelog.rst index c5fc0767..10abcc52 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,6 +11,7 @@ ChangeLog - Add support for Django 5.0 - Add support for Python 3.12 - :issue:`903`: Add basic typing annotations +- Run the test suite against ``mongomock`` instead of an actual MongoDB server *Bugfix:* diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 95d4be7f..1c62c8cd 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -19,6 +19,7 @@ kwargs metaclass misconfiguration Mogo +MongoDB mongoengine pre prepend diff --git a/setup.cfg b/setup.cfg index 4f93b98c..1882a205 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,6 +51,7 @@ dev = SQLAlchemy sqlalchemy_utils mongoengine + mongomock wheel>=0.32.0 tox zest.releaser[recommended] diff --git a/tests/test_mongoengine.py b/tests/test_mongoengine.py index bc930fca..ea1ae687 100644 --- a/tests/test_mongoengine.py +++ b/tests/test_mongoengine.py @@ -10,6 +10,8 @@ except ImportError: raise unittest.SkipTest("mongodb tests disabled.") +import mongomock + import factory from factory.mongoengine import MongoEngineFactory @@ -52,6 +54,7 @@ def setUpClass(cls): db=cls.db_name, host=cls.db_host, port=cls.db_port, + mongo_client_class=mongomock.MongoClient, # PyMongo>=2.1 requires an explicit read_preference. read_preference=mongo_rp.ReadPreference.PRIMARY, # PyMongo>=2.1 has a 20s timeout, use 100ms instead diff --git a/tox.ini b/tox.ini index 17dfdb50..ca21be5c 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,6 @@ DATABASE_TYPE = [testenv] passenv = - MONGO_HOST POSTGRES_HOST POSTGRES_DATABASE deps = @@ -38,6 +37,9 @@ deps = alchemy: SQLAlchemy alchemy: sqlalchemy_utils mongo: mongoengine + mongo: mongomock + # mongomock imports pkg_resources, provided by setuptools. + mongo: setuptools>=66.1.1 django{32,42,50,main}: Pillow django32: Django>=3.2,<3.3 django42: Django>=4.2,<5.0 From c2188f714361de781acaefe45aa05cc4d59f1756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 18:59:50 +0200 Subject: [PATCH 688/714] Stop testing against PostgreSQL factory_boy never interacts with the database directly; all access is mediated through mature ORMs (Django, SQLAlchemy). Any difference in behaviour regarding the databases would have to be handled by those ORMs, not on our level. This reduces the size of the test matrix, and simplifies the test setup code. See: #1077 --- .github/workflows/test.yml | 13 +----------- README.rst | 2 -- setup.cfg | 1 - tests/alchemyapp/models.py | 25 +---------------------- tests/djapp/settings_pg.py | 41 -------------------------------------- tests/test_alchemy.py | 13 ------------ tox.ini | 25 ++++++----------------- 7 files changed, 8 insertions(+), 112 deletions(-) delete mode 100644 tests/djapp/settings_pg.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 29a7e881..2b381638 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ concurrency: jobs: tests: - name: Python ${{ matrix.python-version }}, Database ${{ matrix.database-type }} + name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: @@ -26,17 +26,6 @@ jobs: - "3.12" - "pypy-3.9" - "pypy-3.10" - database-type: - - "sqlite" - - "postgres" - - services: - postgresdb: - image: postgres:alpine - ports: - - 5432:5432 - env: - POSTGRES_PASSWORD: password steps: - uses: actions/checkout@v4 diff --git a/README.rst b/README.rst index 3d3677bf..cd926690 100644 --- a/README.rst +++ b/README.rst @@ -405,11 +405,9 @@ To test with a specific framework version, you may use a ``tox`` target: # run tests inside a specific environment (django) $ tox -e py310-djangomain - $ tox -e py310-djangomain-postgres # run tests inside a specific environment (alchemy) $ tox -e py310-alchemy - $ tox -e py310-alchemy-postgres # run tests inside a specific environment (mongoengine) $ tox -e py310-mongo diff --git a/setup.cfg b/setup.cfg index 1882a205..d22ae33e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,7 +49,6 @@ dev = mypy Pillow SQLAlchemy - sqlalchemy_utils mongoengine mongomock wheel>=0.32.0 diff --git a/tests/alchemyapp/models.py b/tests/alchemyapp/models.py index 20e60aab..42e05176 100644 --- a/tests/alchemyapp/models.py +++ b/tests/alchemyapp/models.py @@ -2,34 +2,11 @@ """Helpers for testing SQLAlchemy apps.""" -import os from sqlalchemy import Column, Integer, Unicode, create_engine from sqlalchemy.orm import declarative_base, scoped_session, sessionmaker -try: - import psycopg2 # noqa: F401 - USING_POSTGRES = True -except ImportError: - try: - # pypy does not support `psycopg2` or `psycopg2-binary` - # This is a package that only gets installed with pypy, and it needs to be - # initialized for it to work properly. It mimic `psycopg2` 1-to-1 - from psycopg2cffi import compat - compat.register() - USING_POSTGRES = True - except ImportError: - USING_POSTGRES = False - -if USING_POSTGRES: - pg_database = 'alch_' + os.environ.get('POSTGRES_DATABASE', 'factory_boy_test') - pg_user = os.environ.get('POSTGRES_USER', 'postgres') - pg_password = os.environ.get('POSTGRES_PASSWORD', 'password') - pg_host = os.environ.get('POSTGRES_HOST', 'localhost') - pg_port = os.environ.get('POSTGRES_PORT', '5432') - engine_name = f'postgresql+psycopg2://{pg_user}:{pg_password}@{pg_host}:{pg_port}/{pg_database}' -else: - engine_name = 'sqlite://' +engine_name = 'sqlite://' session = scoped_session(sessionmaker()) engine = create_engine(engine_name) diff --git a/tests/djapp/settings_pg.py b/tests/djapp/settings_pg.py deleted file mode 100644 index de54922c..00000000 --- a/tests/djapp/settings_pg.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright: See the LICENSE file. - -"""Settings for factory_boy/Django tests.""" - -import os - -from .settings import * # noqa: F401, F403 - -try: - # pypy does not support `psycopg2` or `psycopg2-binary` - # This is a package that only gets installed with pypy, and it needs to be - # initialized for it to work properly. It mimic `psycopg2` 1-to-1 - from psycopg2cffi import compat - compat.register() -except ImportError: - pass - -postgres_user = os.environ.get('POSTGRES_USER', 'postgres') -postgres_name = os.environ.get('POSTGRES_DATABASE', 'factory_boy_test') -postgres_password = os.environ.get('POSTGRES_PASSWORD', 'password') -postgres_host = os.environ.get('POSTGRES_HOST', 'localhost') -postgres_port = os.environ.get('POSTGRES_PORT', '5432') - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': postgres_name, - 'USER': postgres_user, - 'PASSWORD': postgres_password, - 'HOST': postgres_host, - 'PORT': postgres_port, - }, - 'replica': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': postgres_name + '_rp', - 'USER': postgres_user, - 'PASSWORD': postgres_password, - 'HOST': postgres_host, - 'PORT': postgres_port, - } -} diff --git a/tests/test_alchemy.py b/tests/test_alchemy.py index 19e4f5ee..6b568ce4 100644 --- a/tests/test_alchemy.py +++ b/tests/test_alchemy.py @@ -10,8 +10,6 @@ except ImportError: raise unittest.SkipTest("sqlalchemy tests disabled.") -from sqlalchemy_utils import create_database, database_exists, drop_database - import factory from factory.alchemy import SQLAlchemyModelFactory @@ -77,17 +75,6 @@ class Meta: text = factory.Sequence(lambda n: "text%s" % n) -if models.USING_POSTGRES: - # sqlite test database gets created/destroyed automatically, postgres does not. - - def setUpModule(): - if not database_exists(models.engine.url): - create_database(models.engine.url) - - def tearDownModule(): - drop_database(models.engine.url) - - class TransactionTestCase(unittest.TestCase): def setUp(self): models.Base.metadata.create_all(models.engine) diff --git a/tox.ini b/tox.ini index ca21be5c..4d5438e1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,13 +5,12 @@ envlist = docs examples linkcheck - py{38,39,310,311,312,py39,py310}-sqlite - py{38,39,310,311,py39,py310}-django32-mongo-alchemy-{sqlite,postgres} - py{38,39,310,311,312}-django42-mongo-alchemy-{sqlite,postgres} - py{py39,py310}-django42-mongo-alchemy-sqlite, - # py{py39,py310}-django42-mongo-alchemy-postgres # TODO: Fix me! - py{310,311,312}-django50-mongo-alchemy-{sqlite,postgres} - py310-djangomain-mongo-alchemy-{sqlite,postgres} + py{38,39,310,311,312,py39,py310} + py{38,39,310,311,py39,py310}-django32-mongo-alchemy + py{38,39,310,311,312}-django42-mongo-alchemy + py{py39,py310}-django42-mongo-alchemy + py{310,311,312}-django50-mongo-alchemy + py310-djangomain-mongo-alchemy [gh-actions] python = @@ -23,19 +22,10 @@ python = pypy-3.9: pypy39 pypy-3.10: pypy310 -[gh-actions:env] -DATABASE_TYPE = - sqlite: sqlite - postgres: postgres - [testenv] -passenv = - POSTGRES_HOST - POSTGRES_DATABASE deps = mypy alchemy: SQLAlchemy - alchemy: sqlalchemy_utils mongo: mongoengine mongo: mongomock # mongomock imports pkg_resources, provided by setuptools. @@ -45,12 +35,9 @@ deps = django42: Django>=4.2,<5.0 django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz - py{38,39,310,311,312}-postgres: psycopg2-binary - pypy{39,310}-postgres: psycopg2cffi setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings - postgres: DJANGO_SETTINGS_MODULE=tests.djapp.settings_pg allowlist_externals = make commands = make test From ce3911451b4571903d5e74871e911739eaa9481c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 19:07:28 +0200 Subject: [PATCH 689/714] Stop testing for Django 3.2 Upstream support has been dropped in April 2024. --- docs/changelog.rst | 3 +-- setup.cfg | 1 - tox.ini | 4 +--- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 10abcc52..0b8e4dd6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,8 +20,7 @@ ChangeLog *Removed:* -- Drop support for Django 4.0 -- Stop advertising and verifying support for Django 4.1 +- Stop advertising and verifying support for Django 3.2, 4.0, 4.1 3.3.0 (2023-07-19) ------------------ diff --git a/setup.cfg b/setup.cfg index d22ae33e..dd1c00e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,6 @@ license = MIT classifiers = Development Status :: 5 - Production/Stable Framework :: Django - Framework :: Django :: 3.2 Framework :: Django :: 4.2 Framework :: Django :: 5.0 Intended Audience :: Developers diff --git a/tox.ini b/tox.ini index 4d5438e1..95b88d47 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,6 @@ envlist = examples linkcheck py{38,39,310,311,312,py39,py310} - py{38,39,310,311,py39,py310}-django32-mongo-alchemy py{38,39,310,311,312}-django42-mongo-alchemy py{py39,py310}-django42-mongo-alchemy py{310,311,312}-django50-mongo-alchemy @@ -30,8 +29,7 @@ deps = mongo: mongomock # mongomock imports pkg_resources, provided by setuptools. mongo: setuptools>=66.1.1 - django{32,42,50,main}: Pillow - django32: Django>=3.2,<3.3 + django{42,50,main}: Pillow django42: Django>=4.2,<5.0 django50: Django>=5.0,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz From a7d06b93a5ad87c3222be8b4380c83af8d4bb3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 19:11:54 +0200 Subject: [PATCH 690/714] Target Django 5.x tests at version 5.1 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- tox.ini | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0b8e4dd6..9df2a88e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -8,7 +8,7 @@ ChangeLog *New:* - Add support for Django 4.2 -- Add support for Django 5.0 +- Add support for Django 5.1 - Add support for Python 3.12 - :issue:`903`: Add basic typing annotations - Run the test suite against ``mongomock`` instead of an actual MongoDB server diff --git a/setup.cfg b/setup.cfg index dd1c00e9..ccbfe6fb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 4.2 - Framework :: Django :: 5.0 + Framework :: Django :: 5.1 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent diff --git a/tox.ini b/tox.ini index 95b88d47..2bca8a08 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,8 @@ envlist = py{38,39,310,311,312,py39,py310} py{38,39,310,311,312}-django42-mongo-alchemy py{py39,py310}-django42-mongo-alchemy - py{310,311,312}-django50-mongo-alchemy + py{310,311,312}-django51-mongo-alchemy + pypy310-django51-mongo-alchemy py310-djangomain-mongo-alchemy [gh-actions] @@ -29,9 +30,9 @@ deps = mongo: mongomock # mongomock imports pkg_resources, provided by setuptools. mongo: setuptools>=66.1.1 - django{42,50,main}: Pillow + django{42,51,main}: Pillow django42: Django>=4.2,<5.0 - django50: Django>=5.0,<5.1 + django51: Django>=5.1,<5.2 djangomain: https://github.com/django/django/archive/main.tar.gz setenv = From 0ee3c9d6458d004c6229014e31f762f1e5d1b733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 21:36:17 +0200 Subject: [PATCH 691/714] Enable "nitpicky" mode on Sphinx And fix related missing references. Closes: #929 --- Makefile | 2 +- docs/changelog.rst | 102 +++++++++++++++++++++--------------------- docs/conf.py | 6 ++- docs/examples.rst | 2 +- docs/fuzzy.rst | 2 +- docs/ideas.rst | 2 +- docs/internals.rst | 8 ++-- docs/introduction.rst | 6 +-- docs/orms.rst | 53 +++++++++++----------- docs/recipes.rst | 8 ++-- docs/reference.rst | 93 ++++++++++++++++++++------------------ 11 files changed, 146 insertions(+), 138 deletions(-) diff --git a/Makefile b/Makefile index 778fd785..f7404eba 100644 --- a/Makefile +++ b/Makefile @@ -104,7 +104,7 @@ TAGS: # DOC: Compile the documentation doc: - $(MAKE) -C $(DOC_DIR) SPHINXOPTS=-W html + $(MAKE) -C $(DOC_DIR) SPHINXOPTS="-n -W" html linkcheck: $(MAKE) -C $(DOC_DIR) linkcheck diff --git a/docs/changelog.rst b/docs/changelog.rst index 9df2a88e..fd49f447 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,16 +40,15 @@ ChangeLog - Make :meth:`~factory.django.mute_signals` mute signals during post-generation. - - :issue:`775`: Change the signature for :meth:`~factory.alchemy.SQLAlchemyModelFactory._save` and - :meth:`~factory.alchemy.SQLAlchemyModelFactory._get_or_create` to avoid argument names clashes with a field named - ``session``. + - :issue:`775`: Change the signature for :class:`~factory.alchemy.SQLAlchemyModelFactory`'s ``_save`` and + ``_get_or_create`` methods to avoid argument names clashes with a field named ``session``. *Deprecated:* - :class:`~factory.django.DjangoModelFactory` will stop issuing a second call to :meth:`~django.db.models.Model.save` on the created instance when :ref:`post-generation-hooks` return a value. - To help with the transition, :class:`factory.django.DjangoModelFactory._after_postgeneration` raises a + To help with the transition, :class:`factory.django.DjangoModelFactory`'s ``_after_postgeneration`` raises a :class:`DeprecationWarning` when calling :meth:`~django.db.models.Model.save`. Inspect your :class:`~factory.django.DjangoModelFactory` subclasses: @@ -61,7 +60,7 @@ ChangeLog - call :meth:`django.db.models.Model.save` in the :class:`~factory.PostGeneration` hook that modifies the instance, or - - override :class:`~factory.django.DjangoModelFactory._after_postgeneration` to + - override the :class:`~factory.Factory._after_postgeneration` method to :meth:`~django.db.models.Model.save` the instance. *Removed:* @@ -82,9 +81,8 @@ ChangeLog - Do not override signals receivers registered in a :meth:`~factory.django.mute_signals` context. - - :issue:`775`: Change the signature for :meth:`~factory.alchemy.SQLAlchemyModelFactory._save` and - :meth:`~factory.alchemy.SQLAlchemyModelFactory._get_or_create` to avoid argument names clashes with a field named - ``session``. + - :issue:`775`: Change the signature for :class:`~factory.alchemy.SQLAlchemyModelFactory`'s ``_save`` and + ``_get_or_create`` methods to avoid argument names clashes with a field named ``session``. 3.2.0 (2020-12-28) ------------------ @@ -138,7 +136,7 @@ Breaking changes The following aliases were removed: -+------------------------------------------------+---------------------------------------------------+ ++================================================+===================================================+ | Broken alias | New import | +================================================+===================================================+ | ``from factory import DjangoModelFactory`` | ``from factory.django import DjangoModelFactory`` | @@ -150,7 +148,7 @@ The following aliases were removed: | ``from factory.fuzzy import set_random_state`` | ``from factory.random import set_random_state`` | +------------------------------------------------+---------------------------------------------------+ | ``from factory.fuzzy import reseed_random`` | ``from factory.random import reseed_random`` | -+------------------------------------------------+---------------------------------------------------+ ++================================================+===================================================+ *Removed:* @@ -167,14 +165,14 @@ The following aliases were removed: - Add support for Python 3.8 - Add support for Django 2.2 and 3.0 - - Report misconfiguration when a :py:class:`~factory.Factory` is used as the :py:attr:`~factory.Factory.model` for another :py:class:`~factory.Factory`. + - Report misconfiguration when a :py:class:`~factory.Factory` is used as the :py:attr:`~factory.FactoryOptions.model` for another :py:class:`~factory.Factory`. - Allow configuring the color palette of :py:class:`~factory.django.ImageField`. - - :py:meth:`get_random_state()` now represents the state of Faker and ``factory_boy`` fuzzy attributes. + - :py:meth:`~factory.random.get_random_state()` now represents the state of Faker and ``factory_boy`` fuzzy attributes. - Add SQLAlchemy ``get_or_create`` support *Improvements:* - - :issue:`561`: Display a developer-friendly error message when providing a model instead of a factory in a :class:`~factory.declarations.SubFactory` class. + - :issue:`561`: Display a developer-friendly error message when providing a model instead of a factory in a :class:`~factory.SubFactory` class. *Bug fix:* @@ -192,11 +190,11 @@ The following aliases were removed: - Add support for Python 3.7 - Add support for Django 2.1 - - Add :attr:`~factory.fuzzy.FuzzyChoice.getter` to :class:`~factory.fuzzy.FuzzyChoice` that mimics + - Add ``getter`` to :class:`~factory.fuzzy.FuzzyChoice` that mimics the behavior of ``getter`` in :class:`~factory.Iterator` - - Make the ``extra_kwargs`` parameter of :meth:`~factory.faker.Faker.generate` optional + - Make the ``extra_kwargs`` parameter of :class:`~factory.Faker`'s ``generate`` method optional - Add :class:`~factory.RelatedFactoryList` class for one-to-many support, thanks `Sean Harrington `_. - - Make the `locale` argument for :class:`~factory.faker.Faker` keyword-only + - Make the `locale` argument for :class:`~factory.Faker` keyword-only *Bug fix:* @@ -219,7 +217,7 @@ The following aliases were removed: - Fix :class:`~factory.fuzzy.FuzzyFloat` to return a 15 decimal digits precision float by default - :issue:`451`: Restore :class:`~factory.django.FileField` to a - :class:`~factory.declarations.ParameteredAttribute`, relying on composition to parse the provided parameters. + ``factory.declarations.ParameteredAttribute``, relying on composition to parse the provided parameters. - :issue:`389`: Fix random state management with ``faker``. - :issue:`466`: Restore mixing :class:`~factory.Trait` and :meth:`~factory.post_generation`. @@ -233,7 +231,7 @@ The following aliases were removed: *New:* - - :issue:`397`: Allow a :class:`factory.Maybe` to contain a :class:`~factory.PostGenerationDeclaration`. + - :issue:`397`: Allow a :class:`factory.Maybe` to contain a :class:`~factory.PostGeneration` declaration. This also applies to :class:`factory.Trait`, since they use a :class:`factory.Maybe` declaration internally. .. _v2.9.2: @@ -355,8 +353,8 @@ corner cases and weird behaviors. - :issue:`201`: Properly handle custom Django managers when dealing with abstract Django models. - :issue:`212`: Fix :meth:`factory.django.mute_signals` to handle Django's signal caching - - :issue:`228`: Don't load :func:`django.apps.apps.get_model()` until required - - :pr:`219`: Stop using :meth:`mogo.model.Model.new()`, deprecated 4 years ago. + - :issue:`228`: Don't load ``django.apps.apps.get_model()`` until required + - :pr:`219`: Stop using ``mogo.model.Model.new()``, deprecated 4 years ago. .. _v2.5.2: @@ -465,19 +463,19 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a For :class:`factory.Factory`: - * Rename :attr:`~factory.Factory.FACTORY_FOR` to :attr:`~factory.FactoryOptions.model` - * Rename :attr:`~factory.Factory.ABSTRACT_FACTORY` to :attr:`~factory.FactoryOptions.abstract` - * Rename :attr:`~factory.Factory.FACTORY_STRATEGY` to :attr:`~factory.FactoryOptions.strategy` - * Rename :attr:`~factory.Factory.FACTORY_ARG_PARAMETERS` to :attr:`~factory.FactoryOptions.inline_args` - * Rename :attr:`~factory.Factory.FACTORY_HIDDEN_ARGS` to :attr:`~factory.FactoryOptions.exclude` + * Rename ``factory.Factory.FACTORY_FOR`` to :attr:`~factory.FactoryOptions.model` + * Rename ``factory.Factory.ABSTRACT_FACTORY`` to :attr:`~factory.FactoryOptions.abstract` + * Rename ``factory.Factory.FACTORY_STRATEGY`` to :attr:`~factory.FactoryOptions.strategy` + * Rename ``factory.Factory.FACTORY_ARG_PARAMETERS`` to :attr:`~factory.FactoryOptions.inline_args` + * Rename ``factory.Factory.FACTORY_HIDDEN_ARGS`` to :attr:`~factory.FactoryOptions.exclude` For :class:`factory.django.DjangoModelFactory`: - * Rename :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` to :attr:`~factory.django.DjangoOptions.django_get_or_create` + * Rename ``factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`` to :attr:`~factory.django.DjangoOptions.django_get_or_create` For :class:`factory.alchemy.SQLAlchemyModelFactory`: - * Rename :attr:`~factory.alchemy.SQLAlchemyModelFactory.FACTORY_SESSION` to :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session` + * Rename ``factory.alchemy.SQLAlchemyModelFactory.FACTORY_SESSION`` to :attr:`~factory.alchemy.SQLAlchemyOptions.sqlalchemy_session` .. _v2.3.1: @@ -539,9 +537,9 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *New:* - - The :class:`~factory.Factory.ABSTRACT_FACTORY` keyword is now optional, and automatically set + - The ``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`) + ``factory.Factory.FACTORY_FOR`` attribute (:issue:`74`) .. _v2.1.1: @@ -562,8 +560,8 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a - Add :class:`~factory.fuzzy.FuzzyDate` thanks to `saulshanabrook `_ - Add :class:`~factory.fuzzy.FuzzyDateTime` and :class:`~factory.fuzzy.FuzzyNaiveDateTime`. - - Add a :attr:`~factory.builder.Resolver.factory_parent` attribute to the - :class:`~factory.builder.Resolver` passed to :class:`~factory.LazyAttribute`, in order to access + - Add a ``factory_parent`` attribute to the + ``factory.builder.Resolver`` 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`) @@ -594,7 +592,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *New:* - - When :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is + - When ``factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`` is empty, use ``Model.objects.create()`` instead of ``Model.objects.get_or_create``. @@ -606,7 +604,7 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *New:* - Don't push ``defaults`` to ``get_or_create`` when - :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE` is not set. + ``factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`` is not set. .. _v2.0.0: @@ -618,11 +616,11 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a - Allow overriding the base factory class for :func:`~factory.make_factory` and friends. - Add support for Python3 (Thanks to `kmike `_ and `nkryptic `_) - - 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 default type for :class:`~factory.Sequence` is now :obj:`int` + - Fields listed in ``factory.Factory.FACTORY_HIDDEN_ARGS`` won't be passed to the associated class' constructor - Add support for ``get_or_create`` in :class:`~factory.django.DjangoModelFactory`, - through :attr:`~factory.django.DjangoModelFactory.FACTORY_DJANGO_GET_OR_CREATE`. + through ``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 :issue:`18`). @@ -630,12 +628,12 @@ This takes care of all ``FACTORY_FOR`` occurrences; the files containing other a *Removed:* - Remove associated class discovery - - Remove :class:`~factory.InfiniteIterator` and :func:`~factory.infinite_iterator` - - Remove :class:`~factory.CircularSubFactory` + - Remove ``factory.InfiniteIterator`` and ``factory.infinite_iterator`` + - Remove ``factory.CircularSubFactory`` - Remove ``extract_prefix`` kwarg to post-generation hooks. - Stop defaulting to Django's ``Foo.objects.create()`` when "creating" instances - Remove STRATEGY_* - - Remove :meth:`~factory.Factory.set_building_function` / :meth:`~factory.Factory.set_creation_function` + - Remove ``factory.Factory.set_building_function`` / ``factory.Factory.set_creation_function`` .. _v1.3.0: @@ -659,8 +657,8 @@ New - **The Factory class:** - Better creation/building customization hooks at :meth:`factory.Factory._build` and :meth:`factory.Factory.create` - Add support for passing non-kwarg parameters to a :class:`~factory.Factory` - wrapped class through :attr:`~factory.Factory.FACTORY_ARG_PARAMETERS`. - - Keep the :attr:`~factory.Factory.FACTORY_FOR` attribute in :class:`~factory.Factory` classes + wrapped class through ``FACTORY_ARG_PARAMETERS``. + - Keep the ``FACTORY_FOR`` attribute in :class:`~factory.Factory` classes - **Declarations:** - Allow :class:`~factory.SubFactory` to solve circular dependencies between factories @@ -681,14 +679,14 @@ Pending deprecation The following features have been deprecated and will be removed in an upcoming release. - **Declarations:** - - :class:`~factory.InfiniteIterator` is deprecated in favor of :class:`~factory.Iterator` - - :class:`~factory.CircularSubFactory` is deprecated in favor of :class:`~factory.SubFactory` + - ``factory.InfiniteIterator`` is deprecated in favor of :class:`~factory.Iterator` + - ``factory.CircularSubFactory`` is deprecated in favor of :class:`~factory.SubFactory` - The ``extract_prefix`` argument to :meth:`~factory.post_generation` is now deprecated - **Factory:** - - Usage of :meth:`~factory.Factory.set_creation_function` and :meth:`~factory.Factory.set_building_function` + - Usage of ``factory.Factory.set_creation_function`` and ``factory.Factory.set_building_function`` are now deprecated - - Implicit associated class discovery is no longer supported, you must set the :attr:`~factory.Factory.FACTORY_FOR` + - Implicit associated class discovery is no longer supported, you must set the ``FACTORY_FOR`` attribute on all :class:`~factory.Factory` subclasses @@ -725,7 +723,7 @@ In order to upgrade client code, apply the following rules: *New:* - - Add :class:`~factory.CircularSubFactory` to solve circular dependencies between factories + - Add ``factory.CircularSubFactory`` to solve circular dependencies between factories .. _v1.1.5: @@ -735,7 +733,7 @@ In order to upgrade client code, apply the following rules: *Bug fix:* - - Fix :class:`~factory.PostGenerationDeclaration` and derived classes. + - Fix ``factory.PostGenerationDeclaration`` and derived classes. .. _v1.1.4: @@ -769,7 +767,7 @@ In order to upgrade client code, apply the following rules: *New:* - - Add :class:`~factory.Iterator` and :class:`~factory.InfiniteIterator` for :class:`~factory.Factory` attribute declarations. + - Add :class:`~factory.Iterator` and ``factory.InfiniteIterator`` for :class:`~factory.Factory` attribute declarations. - 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`. @@ -792,7 +790,7 @@ In order to upgrade client code, apply the following rules: *New:* - Improve the :class:`~factory.SelfAttribute` syntax to fetch sub-attributes using the ``foo.bar`` syntax; - - Add :class:`~factory.ContainerAttribute` to fetch attributes from the container of a :class:`~factory.SubFactory`. + - Add ``factory.ContainerAttribute`` to fetch attributes from the container of a :class:`~factory.SubFactory`. - Provide the :func:`~factory.make_factory` helper: ``MyClassFactory = make_factory(MyClass, x=3, y=4)`` - Add :func:`~factory.build`, :func:`~factory.create`, :func:`~factory.stub` helpers @@ -802,7 +800,7 @@ In order to upgrade client code, apply the following rules: *Deprecation:* - - Auto-discovery of :attr:`~factory.Factory.FACTORY_FOR` based on class name is now deprecated + - Auto-discovery of ``factory.Factory.FACTORY_FOR`` based on class name is now deprecated .. _v1.0.4: @@ -815,9 +813,9 @@ In order to upgrade client code, apply the following rules: - Improve the algorithm for populating a :class:`~factory.Factory` attributes dict - Add ``python setup.py test`` command to run the test suite - Allow custom build functions - - Introduce :data:`~factory.MOGO_BUILD` build function + - Introduce ``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 `. + - Base :class:`~factory.Factory` classes can now be declared abstract through ``factory.Factory.ABSTRACT_FACTORY``. - 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``. diff --git a/docs/conf.py b/docs/conf.py index 876a1c49..dc4a3863 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ project = 'Factory Boy' copyright = '2011-2015, Raphaël Barrois, Mark Sandstrom' -author = 'adfasf' +author = 'Raphaël Barrois, Mark Sandstrom' # The full version, including alpha/beta/rc tags release = factory.__version__ @@ -84,6 +84,10 @@ 'https://docs.djangoproject.com/en/dev/', 'https://docs.djangoproject.com/en/dev/_objects/', ), + 'mongoengine': ( + 'https://mongoengine-odm.readthedocs.io/', + None, + ), 'sqlalchemy': ( 'https://docs.sqlalchemy.org/en/latest/', 'https://docs.sqlalchemy.org/en/latest/objects.inv', diff --git a/docs/examples.rst b/docs/examples.rst index 7d868f8c..bd27af6a 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -76,7 +76,7 @@ And now, we'll define the related factories: -We have now defined basic factories for our :class:`~Account` and :class:`~Profile` classes. +We have now defined basic factories for our ``Account`` and ``Profile`` classes. If we commonly use a specific variant of our objects, we can refine a factory accordingly: diff --git a/docs/fuzzy.rst b/docs/fuzzy.rst index ca251656..bef86778 100644 --- a/docs/fuzzy.rst +++ b/docs/fuzzy.rst @@ -354,4 +354,4 @@ They should inherit from the :class:`BaseFuzzyAttribute` class, and override its Custom :class:`BaseFuzzyAttribute` subclasses **MUST** use :obj:`factory.random.randgen` as a randomness source; this ensures that data they generate can be regenerated using the simple state from - :meth:`get_random_state`. + :meth:`factory.random.get_random_state`. diff --git a/docs/ideas.rst b/docs/ideas.rst index 6e3962d4..b9f3691b 100644 --- a/docs/ideas.rst +++ b/docs/ideas.rst @@ -4,6 +4,6 @@ Ideas This is a list of future features that may be incorporated into factory_boy: -* When a :class:`Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere +* When a :class:`~factory.Factory` is built or created, pass the calling context throughout the calling chain instead of custom solutions everywhere * Define a proper set of rules for the support of third-party ORMs * Properly evaluate nested declarations (e.g ``factory.fuzzy.FuzzyDate(start_date=factory.SelfAttribute('since'))``) diff --git a/docs/internals.rst b/docs/internals.rst index d71afbec..62ca3fed 100644 --- a/docs/internals.rst +++ b/docs/internals.rst @@ -57,11 +57,11 @@ First, decide the strategy: use the strategy defined at the :attr:`class Meta ` level -Then, we'll pass the strategy and passed-in overrides to the :meth:`~Factory._generate` method. +Then, we'll pass the strategy and passed-in overrides to the ``Factory._generate`` method. -.. note:: According to the project road map, a future version will use a :meth:`~Factory._generate_batch` at its core instead. +.. note:: According to the project road map, a future version will use a ``Factory._generate_batch`` at its core instead. -A factory's :meth:`~Factory._generate` function actually delegates to a ``StepBuilder()`` object. +A factory's ``Factory._generate`` function actually delegates to a ``StepBuilder()`` object. This object will carry the overall "build an object" context (strategy, depth, and possibly other). @@ -81,7 +81,7 @@ Instantiating, Step 3: Building the object 1. The ``StepBuilder`` fetches the attributes computed by the ``Resolver``. 2. It applies renaming/adjustment rules -3. It passes them to the :meth:`FactoryOptions.instantiate` method, which +3. It passes them to the ``FactoryOptions.instantiate`` method, which forwards to the proper methods. 4. Post-declaration are applied (in declaration order) diff --git a/docs/introduction.rst b/docs/introduction.rst index e9ad3248..d3b169a0 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -112,7 +112,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 that ``self`` is not added as first parameter): +.. 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 @@ -152,7 +152,7 @@ argument and returning the value for the field: .. note:: For complex cases when you happen to write a specific function, - the :meth:`~factory.@lazy_attribute` decorator should be more appropriate. + the :meth:`@factory.lazy_attribute ` decorator should be more appropriate. LazyAttribute @@ -185,7 +185,7 @@ 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 diff --git a/docs/orms.rst b/docs/orms.rst index 5967e1bc..7f9d1ef0 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -13,7 +13,7 @@ adding dedicated features. Django ------ -.. currentmodule:: factory.django +.. module:: factory.django The first versions of factory_boy were designed specifically for Django, @@ -111,11 +111,10 @@ All factories for a Django :class:`~django.db.models.Model` should use the .. attribute:: skip_postgeneration_save - Transitional option to prevent - :meth:`~factory.django.DjangoModelFactory._after_postgeneration` from - issuing a duplicate call to :meth:`~django.db.models.Model.save` on the - created instance when :class:`factory.PostGeneration` hooks return a - value. + Transitional option to prevent :class:`~factory.django.DjangoModelFactory`'s + ``_after_postgeneration`` from issuing a duplicate call to + :meth:`~django.db.models.Model.save` on the created instance when + :class:`factory.PostGeneration` hooks return a value. Extra fields @@ -167,9 +166,9 @@ Extra fields :param str from_path: Use data from the file located at ``from_path``, and keep its filename - :param file from_file: Use the contents of the provided file object; use its filename + :param io.BytesIO from_file: Use the contents of the provided file object; use its filename if available, unless ``filename`` is also provided. - :param func from_func: Use function that returns a file object + :param Callable from_func: Use function that returns a file object :param bytes data: Use the provided bytes as file contents :param str filename: The filename for the FileField @@ -200,9 +199,9 @@ Extra fields :param str from_path: Use data from the file located at ``from_path``, and keep its filename - :param file from_file: Use the contents of the provided file object; use its filename + :param io.BytesIO from_file: Use the contents of the provided file object; use its filename if available - :param func from_func: Use function that returns a file object + :param Callable from_func: Use function that returns a file object :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``) @@ -272,7 +271,7 @@ To work around this problem, use the :meth:`mute_signals()` decorator/context ma Mogo ---- -.. currentmodule:: factory.mogo +.. module:: factory.mogo factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` class. @@ -294,7 +293,7 @@ factory_boy supports `Mogo`_-style models, through the :class:`MogoFactory` clas MongoEngine ----------- -.. currentmodule:: factory.mongoengine +.. module:: factory.mongoengine factory_boy supports `MongoEngine`_-style models, through the :class:`MongoEngineFactory` class. @@ -312,8 +311,8 @@ 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 :class:`~MongoEngineFactory`'s ``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. @@ -349,7 +348,7 @@ A minimalist example: SQLAlchemy ---------- -.. currentmodule:: factory.alchemy +.. module:: factory.alchemy Factory_boy also supports `SQLAlchemy`_ models through the :class:`SQLAlchemyModelFactory` class. @@ -364,12 +363,12 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: This class provides the following features: - * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.session.Session.add` + * :func:`~factory.Factory.create()` uses :meth:`sqlalchemy.orm.Session.add` .. class:: SQLAlchemyOptions(factory.base.FactoryOptions) - In addition to the usual parameters available in :class:`class Meta `, + In addition to the usual parameters available in :class:`class Meta `, a :class:`SQLAlchemyModelFactory` also supports the following settings: .. attribute:: sqlalchemy_session @@ -403,8 +402,8 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: Valid values are: * ``None``: do nothing - * ``'flush'``: perform a session :meth:`~sqlalchemy.orm.session.Session.flush` - * ``'commit'``: perform a session :meth:`~sqlalchemy.orm.session.Session.commit` + * ``'flush'``: perform a session :meth:`~sqlalchemy.orm.Session.flush` + * ``'commit'``: perform a session :meth:`~sqlalchemy.orm.Session.commit` The default value is ``None``. @@ -413,8 +412,8 @@ To work, this class needs an `SQLAlchemy`_ session object affected to the :attr: .. versionadded:: 3.0.0 Fields whose name are passed in this list will be used to perform a - :meth:`Model.query.one_or_none() ` - or the usual :meth:`Session.add() `: + :meth:`Model.query.one_or_none() ` + or the usual :meth:`Session.add() `: .. code-block:: python @@ -516,15 +515,15 @@ there is no "global" session management system. The most common pattern when working with unit tests and ``factory_boy`` is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: -* The test runner configures some project-wide :class:`~sqlalchemy.orm.scoping.scoped_session` +* The test runner configures some project-wide :class:`~sqlalchemy.orm.scoped_session` * Each :class:`~SQLAlchemyModelFactory` subclass uses this - :class:`~sqlalchemy.orm.scoping.scoped_session` as its :attr:`~SQLAlchemyOptions.sqlalchemy_session` + :class:`~sqlalchemy.orm.scoped_session` as its :attr:`~SQLAlchemyOptions.sqlalchemy_session` * The :meth:`~unittest.TestCase.tearDown` method of tests calls - :meth:`Session.remove ` + :meth:`Session.remove ` to reset the session. .. note:: See the excellent :ref:`SQLAlchemy guide on scoped_session ` - for details of :class:`~sqlalchemy.orm.scoping.scoped_session`'s usage. + for details of :class:`~sqlalchemy.orm.scoped_session`'s usage. The basic idea is that declarative parts of the code (including factories) need a simple way to access the "current session", @@ -536,7 +535,7 @@ is to use `SQLAlchemy`_'s :class:`sqlalchemy.orm.scoping.scoped_session`: Here is an example layout: -- A global (test-only?) file holds the :class:`~sqlalchemy.orm.scoping.scoped_session`: +- A global (test-only?) file holds the :class:`~sqlalchemy.orm.scoped_session`: .. code-block:: python @@ -568,7 +567,7 @@ Here is an example layout: name = factory.Sequence(lambda n: "User %d" % n) -- The test runner configures the :class:`~sqlalchemy.orm.scoping.scoped_session` when it starts: +- The test runner configures the :class:`~sqlalchemy.orm.scoped_session` when it starts: .. code-block:: python diff --git a/docs/recipes.rst b/docs/recipes.rst index a1717ad5..457bcf9c 100644 --- a/docs/recipes.rst +++ b/docs/recipes.rst @@ -89,7 +89,7 @@ use a :class:`~factory.RelatedFactory` declaration: ) -When a :class:`UserFactory` is instantiated, factory_boy will call +When a ``UserFactory`` is instantiated, factory_boy will call ``UserLogFactory(user=that_user, action=...)`` just before returning the created ``User``. @@ -101,7 +101,7 @@ using a :class:`~django.db.models.OneToOneField` from the ``Profile`` to the ``U A typical way to create those profiles was to hook a post-save signal to the ``User`` model. -Prior to version 2.9, the solution to this was to override the :meth:`~factory.Factory._generate` method on the factory. +Prior to version 2.9, the solution to this was to override the ``factory.Factory._generate`` method on the factory. Since version 2.9, the :meth:`~factory.django.mute_signals` decorator should be used: @@ -216,8 +216,8 @@ Many-to-many relation with a 'through' -------------------------------------- -If only one link is required, this can be simply performed with a :class:`RelatedFactory`. -If more links are needed, simply add more :class:`RelatedFactory` declarations: +If only one link is required, this can be simply performed with a :class:`~factory.RelatedFactory`. +If more links are needed, simply add more :class:`~factory.RelatedFactory` declarations: .. code-block:: python diff --git a/docs/reference.rst b/docs/reference.rst index 130e194c..2122b9f7 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -1,7 +1,7 @@ Reference ========= -.. currentmodule:: factory +.. module:: factory This section offers an in-depth description of factory_boy features. @@ -181,17 +181,19 @@ Attributes and methods **Base functions:** - The :class:`Factory` class provides a few methods for getting objects; - the usual way being to simply call the class: + .. classmethod:: __call__(**kwargs) - .. code-block:: pycon + The :class:`Factory` class provides a few methods for getting objects; + the usual way being to simply call the class: + + .. code-block:: pycon - >>> UserFactory() # Calls UserFactory.create() - >>> UserFactory(login='john') # Calls UserFactory.create(login='john') + >>> UserFactory() # Calls UserFactory.create() + >>> UserFactory(login='john') # Calls UserFactory.create(login='john') - Under the hood, factory_boy will define the :class:`Factory` - :meth:`~object.__new__` method to call the default :ref:`strategy ` - of the :class:`Factory`. + Under the hood, factory_boy will define the :class:`Factory` + :meth:`~object.__new__` method to call the default :ref:`strategy ` + of the :class:`Factory`. A specific strategy for getting instance can be selected by calling the @@ -203,7 +205,7 @@ Attributes and methods .. classmethod:: build_batch(cls, size, **kwargs) - Provides a list of :obj:`size` instances from the :class:`Factory`, + Provides a list of ``size`` instances from the :class:`Factory`, through the 'build' strategy. @@ -213,7 +215,7 @@ Attributes and methods .. classmethod:: create_batch(cls, size, **kwargs) - Provides a list of :obj:`size` instances from the :class:`Factory`, + Provides a list of ``size`` instances from the :class:`Factory`, through the 'create' strategy. @@ -223,16 +225,16 @@ Attributes and methods .. classmethod:: stub_batch(cls, size, **kwargs) - Provides a list of :obj:`size` stubs from the :class:`Factory`. + Provides a list of ``size`` stubs from the :class:`Factory`. .. classmethod:: generate(cls, strategy, **kwargs) - Provide a new instance, with the provided :obj:`strategy`. + Provide a new instance, with the provided ``strategy``. .. classmethod:: generate_batch(cls, strategy, size, **kwargs) - Provides a list of :obj:`size` instances using the specified strategy. + Provides a list of ``size`` instances using the specified strategy. .. classmethod:: simple_generate(cls, create, **kwargs) @@ -241,8 +243,8 @@ Attributes and methods .. classmethod:: simple_generate_batch(cls, create, size, **kwargs) - Provides a list of :obj:`size` instances, either built or created - according to :obj:`create`. + Provides a list of ``size`` instances, either built or created + according to ``create``. **Extension points:** @@ -585,7 +587,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. *Decorator* - Change the default strategy of the decorated :class:`Factory` to the chosen :obj:`strategy`: + Change the default strategy of the decorated :class:`Factory` to the chosen ``strategy``: .. code-block:: python @@ -626,7 +628,7 @@ factory_boy supports two main strategies for generating instances, plus stubs. .. 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` + :param io.StringIO 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, @@ -821,7 +823,7 @@ Decorator The class :class:`LazyFunction` does not provide a decorator. -For complex cases, use :meth:`LazyAttribute.lazy_attribute` directly. +For complex cases, use :meth:`~factory.lazy_attribute` directly. LazyAttribute """"""""""""" @@ -855,7 +857,7 @@ accept the object being built as sole argument, and return a value. The object passed to :class:`LazyAttribute` is not an instance of the target class, -but instead a :class:`~builder.Resolver`: a temporary container that computes +but instead a ``builder.Resolver``: a temporary container that computes the value of all declared fields. @@ -1158,7 +1160,7 @@ The :class:`SubFactory` attribute should be called with: .. note:: When passing an actual :class:`~factory.Factory` for the - :attr:`~factory.SubFactory.factory` argument, make sure to pass + :class:`~factory.SubFactory`'s ``factory`` argument, make sure to pass the class and not instance (i.e no ``()`` after the class): .. code-block:: python @@ -1365,7 +1367,7 @@ Obviously, this "follow parents" ability also handles overriding some attributes This feature is also available to :class:`LazyAttribute` and :class:`LazyAttributeSequence`, -through the :attr:`~builder.Resolver.factory_parent` attribute of the passed-in object: +through the ``factory_parent`` attribute of the passed-in object: .. code-block:: python @@ -1819,7 +1821,7 @@ RelatedFactoryList in a future version for increased consistency with other declarations. .. note:: - Note that using a ``lambda`` for :attr:`size` allows the number of related objects per + Note that using a ``lambda`` for ``size`` allows the number of related objects per parents object to vary. This is useful for testing, when you likely don't want your mock data to have parent objects with the exact same, static number of related objects. @@ -2047,18 +2049,18 @@ Internally, helper methods use :func:`make_factory` to create a new :class:`Factory` according to the method name. Please note, that all Factories created with this methods inherit from the -:class:`factory.base.Factory` class. For full support of your ``ORM``, specify +:class:`factory.Factory` class. For full support of your ``ORM``, specify a base class with the ``FACTORY_CLASS`` parameter as shown in :func:`make_factory` examples. .. function:: build(klass, FACTORY_CLASS=None, **kwargs) .. function:: build_batch(klass, size, FACTORY_CLASS=None, **kwargs) - Create a factory for :obj:`klass` using declarations passed in kwargs; + Create a factory for ``klass`` using declarations passed in kwargs; return an instance built from that factory with :data:`BUILD_STRATEGY`, - or a list of :obj:`size` instances (for :func:`build_batch`). + or a list of ``size`` instances (for :func:`build_batch`). - :param class klass: Class of the instance to build + :param type klass: Class of the instance to build :param int size: Number of instances to build :param kwargs: Declarations to use for the generated factory :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`) @@ -2068,11 +2070,11 @@ a base class with the ``FACTORY_CLASS`` parameter as shown in .. function:: create(klass, FACTORY_CLASS=None, **kwargs) .. function:: create_batch(klass, size, FACTORY_CLASS=None, **kwargs) - Create a factory for :obj:`klass` using declarations passed in kwargs; + Create a factory for ``klass`` using declarations passed in kwargs; return an instance created from that factory with :data:`CREATE_STRATEGY`, - or a list of :obj:`size` instances (for :func:`create_batch`). + or a list of ``size`` instances (for :func:`create_batch`). - :param class klass: Class of the instance to create + :param type klass: Class of the instance to create :param int size: Number of instances to create :param kwargs: Declarations to use for the generated factory :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`) @@ -2082,11 +2084,11 @@ a base class with the ``FACTORY_CLASS`` parameter as shown in .. function:: stub(klass, FACTORY_CLASS=None, **kwargs) .. function:: stub_batch(klass, size, FACTORY_CLASS=None, **kwargs) - Create a factory for :obj:`klass` using declarations passed in kwargs; + Create a factory for ``klass`` using declarations passed in kwargs; return an instance stubbed from that factory with :data:`STUB_STRATEGY`, - or a list of :obj:`size` instances (for :func:`stub_batch`). + or a list of ``size`` instances (for :func:`stub_batch`). - :param class klass: Class of the instance to stub + :param type klass: Class of the instance to stub :param int size: Number of instances to stub :param kwargs: Declarations to use for the generated factory :param FACTORY_CLASS: Alternate base class (instead of :class:`Factory`) @@ -2096,11 +2098,11 @@ a base class with the ``FACTORY_CLASS`` parameter as shown in .. function:: generate(klass, strategy, FACTORY_CLASS=None, **kwargs) .. function:: generate_batch(klass, strategy, size, FACTORY_CLASS=None, **kwargs) - Create a factory for :obj:`klass` using declarations passed in kwargs; - return an instance generated from that factory with the :obj:`strategy` strategy, - or a list of :obj:`size` instances (for :func:`generate_batch`). + Create a factory for ``klass`` using declarations passed in kwargs; + return an instance generated from that factory with the ``strategy`` strategy, + or a list of ``size`` instances (for :func:`generate_batch`). - :param class klass: Class of the instance to generate + :param type klass: Class of the instance to generate :param str strategy: The strategy to use :param int size: Number of instances to generate :param kwargs: Declarations to use for the generated factory @@ -2111,11 +2113,11 @@ a base class with the ``FACTORY_CLASS`` parameter as shown in .. function:: simple_generate(klass, create, FACTORY_CLASS=None, **kwargs) .. function:: simple_generate_batch(klass, create, size, FACTORY_CLASS=None, **kwargs) - Create a factory for :obj:`klass` using declarations passed in kwargs; - return an instance generated from that factory according to the :obj:`create` flag, - or a list of :obj:`size` instances (for :func:`simple_generate_batch`). + Create a factory for ``klass`` using declarations passed in kwargs; + return an instance generated from that factory according to the ``create`` flag, + or a list of ``size`` instances (for :func:`simple_generate_batch`). - :param class klass: Class of the instance to generate + :param type klass: Class of the instance to generate :param bool create: Whether to build (``False``) or create (``True``) instances :param int size: Number of instances to generate :param kwargs: Declarations to use for the generated factory @@ -2125,7 +2127,7 @@ a base class with the ``FACTORY_CLASS`` parameter as shown in Randomness management --------------------- -.. currentmodule:: factory.random +.. module:: factory.random Using :mod:`random` in factories allows to "fuzz" a program efficiently. However, it's sometimes required to *reproduce* a failing test. @@ -2147,6 +2149,11 @@ of :class:`random.Random`, which can be managed through the :mod:`factory.random .. method:: reseed_random(seed) The :meth:`reseed_random` function allows to load a chosen seed into the random generator. - That seed can be anything accepted by :meth:`random.seed`. + That seed can be anything accepted by :func:`random.seed`. + +.. data:: randgen + + The :class:`random.Random` global instance used by :mod:`factory.fuzzy` + and :class:`factory.Faker`. See :ref:`recipe-random-management` for help in using those methods in a test setup. From c38732fb4aa0e9e8ec6a77d7dd08f17c2043155d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 21:40:45 +0200 Subject: [PATCH 692/714] Preparing release 3.3.1 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index fd49f447..024609e9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.3.1 (unreleased) +3.3.1 (2024-08-18) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index ccbfe6fb..ee24cbd1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.1.dev0 +version = 3.3.1 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From ac49fb40ec424276c3cd3ca0925ba99a626f05f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 18 Aug 2024 21:41:06 +0200 Subject: [PATCH 693/714] Back to development: 3.3.2 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 024609e9..29475a92 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,12 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. +3.3.2 (unreleased) +------------------ + +- Nothing changed yet. + + 3.3.1 (2024-08-18) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index ee24cbd1..1ae6bcbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.1 +version = 3.3.2.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From 4209372ec6db85a8deeea4ad620801ce670a71c7 Mon Sep 17 00:00:00 2001 From: antoliny0919 Date: Wed, 13 Nov 2024 11:02:37 +0900 Subject: [PATCH 694/714] Fixed typo in orms section document. --- docs/orms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/orms.rst b/docs/orms.rst index 7f9d1ef0..d28e7ebf 100644 --- a/docs/orms.rst +++ b/docs/orms.rst @@ -209,7 +209,7 @@ Extra fields :param str format: The image format (as supported by PIL) (default: ``'JPEG'``) :param str palette: The image palette (as supported by PIL) (default: ``'RGB'``) -.. note:: If the value ``None`` was passed for the :class:`FileField` field, this will +.. note:: If the value ``None`` was passed for the :class:`ImageField` field, this will disable field generation: .. note:: Just as Django's :class:`django.db.models.ImageField` requires the From 6a753860543c98f376ccd033823c3f026bc9fec5 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Thu, 25 Apr 2024 08:32:59 -0700 Subject: [PATCH 695/714] Default ReadTheDocs to Ubuntu / Python `latest` I noticed that we're not ensuring the docs build under `3.12`, plus `24.04` will be released later this month (although may take RTD a bit longer to pull it in). Looking at [the docs](https://docs.readthedocs.io/en/stable/config-file/v2.html), there's a `latest` variant for both. It comes with a warning that using latest may unexpectedly break builds if the project isn't compatible... But I think for our use case the odds that it will break are extremely low (and in fact we'd want to know it's broken so we could fix it!) Plus if it starts breaking, it's not hard to temp pin it to an older version. So all-in-all, both lower maintenance and faster realization of breakage if we default to latest on both of these. --- readthedocs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readthedocs.yml b/readthedocs.yml index 883f654f..dc36107b 100644 --- a/readthedocs.yml +++ b/readthedocs.yml @@ -2,9 +2,9 @@ version: 2 build: - os: ubuntu-22.04 + os: "ubuntu-lts-latest" tools: - python: "3.11" + python: "latest" python: install: From 1792d3e2045507a45025d9aa8361408766acaf7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 09:53:49 +0100 Subject: [PATCH 696/714] Update readthedocs build configuration Use the more documented ".readthedocs.yaml" file. Fix missing "sphinx.configuration" option. --- readthedocs.yml => .readthedocs.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename readthedocs.yml => .readthedocs.yaml (72%) diff --git a/readthedocs.yml b/.readthedocs.yaml similarity index 72% rename from readthedocs.yml rename to .readthedocs.yaml index dc36107b..2bba49e3 100644 --- a/readthedocs.yml +++ b/.readthedocs.yaml @@ -1,6 +1,4 @@ ---- version: 2 - build: os: "ubuntu-lts-latest" tools: @@ -12,3 +10,7 @@ python: path: . extra_requirements: - doc + +sphinx: + configuration: docs/conf.py + fail_on_warning: true From 632ccba49e6d81a56bbecf4395d660c95554a190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 09:59:29 +0100 Subject: [PATCH 697/714] Drop pypy3.9 from test suite No longer supported upstream. --- .github/workflows/test.yml | 1 - tox.ini | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b381638..50f53a3d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,7 +24,6 @@ jobs: - "3.10" - "3.11" - "3.12" - - "pypy-3.9" - "pypy-3.10" steps: diff --git a/tox.ini b/tox.ini index 2bca8a08..c6f365a8 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,6 @@ python = 3.10: py310 3.11: py311 3.12: py312 - pypy-3.9: pypy39 pypy-3.10: pypy310 [testenv] From e007ef719483658b526b7c0a4d12d79eb7e5feed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:00:35 +0100 Subject: [PATCH 698/714] fixup! Update readthedocs build configuration --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index c56cd9c7..bd58e2c1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -7,7 +7,7 @@ graft docs graft examples graft tests -exclude readthedocs.yml +exclude .readthedocs.yaml global-exclude *.py[cod] __pycache__ .*.sw[po] prune .github prune docs/_build From 68cb77c66070873b0926006938f1a0f95413e015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:04:26 +0100 Subject: [PATCH 699/714] Replace Python 3.8 with 3.13 in testing matrix --- .github/workflows/test.yml | 2 +- setup.cfg | 2 +- tox.ini | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 50f53a3d..7b206915 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,11 +19,11 @@ jobs: fail-fast: false matrix: python-version: - - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" + - "3.13" - "pypy-3.10" steps: diff --git a/setup.cfg b/setup.cfg index 1ae6bcbf..bba995ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,11 +23,11 @@ classifiers = Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Testing diff --git a/tox.ini b/tox.ini index c6f365a8..9ee320ab 100644 --- a/tox.ini +++ b/tox.ini @@ -5,20 +5,20 @@ envlist = docs examples linkcheck - py{38,39,310,311,312,py39,py310} - py{38,39,310,311,312}-django42-mongo-alchemy + py{39,310,311,312,313,py39,py310} + py{39,310,311,312,313}-django42-mongo-alchemy py{py39,py310}-django42-mongo-alchemy - py{310,311,312}-django51-mongo-alchemy + py{310,311,312,313}-django51-mongo-alchemy pypy310-django51-mongo-alchemy py310-djangomain-mongo-alchemy [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311 3.12: py312 + 3.13: py313 pypy-3.10: pypy310 [testenv] From 6f5f6a05dec30a3d909522f2667a90240665abfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:20:54 +0100 Subject: [PATCH 700/714] Fix tox.ini config for Django "main" Use `pip_pre: true` instead of downloading the sources. --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 9ee320ab..336f1a77 100644 --- a/tox.ini +++ b/tox.ini @@ -32,11 +32,14 @@ deps = django{42,51,main}: Pillow django42: Django>=4.2,<5.0 django51: Django>=5.1,<5.2 - djangomain: https://github.com/django/django/archive/main.tar.gz + djangomain: Django>5.1,<6.0 setenv = py: DJANGO_SETTINGS_MODULE=tests.djapp.settings +pip_pre = + djangomain: true + allowlist_externals = make commands = make test From c463536fbc0a2bf932ce8638904942f19261cdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:27:24 +0100 Subject: [PATCH 701/714] Update Changelog --- docs/changelog.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 29475a92..e7b14d0b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,13 @@ ChangeLog 3.3.2 (unreleased) ------------------ -- Nothing changed yet. +*Bugfix:* + + - Fix docs generation + +*New:* + + - Add support for Python 3.13 3.3.1 (2024-08-18) From 11a9acf1615d9ff4dcda17d50c4fc3c27602540a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:28:11 +0100 Subject: [PATCH 702/714] Preparing release 3.3.2 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e7b14d0b..32c090c2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.3.2 (unreleased) +3.3.2 (2025-02-03) ------------------ *Bugfix:* diff --git a/setup.cfg b/setup.cfg index bba995ed..caba0798 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.2.dev0 +version = 3.3.2 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From 8b75576bf46946c559950b9b737a9770d3dc13f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:28:32 +0100 Subject: [PATCH 703/714] Back to development: 3.3.3 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 32c090c2..c322a0ee 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,12 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. +3.3.3 (unreleased) +------------------ + +- Nothing changed yet. + + 3.3.2 (2025-02-03) ------------------ diff --git a/setup.cfg b/setup.cfg index caba0798..d3a4387e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.2 +version = 3.3.3.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From fa397a26a0b5d41a536f641363f8e38887f32d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:34:35 +0100 Subject: [PATCH 704/714] Fix comment, as caught in #1108 Thanks to Antoliny Lee for spotting this. --- factory/declarations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/factory/declarations.py b/factory/declarations.py index 951b45f3..f835f0d2 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -657,7 +657,7 @@ def call(self, instance, step, context): # pragma: no cover Args: instance (object): the newly generated object step (bool): whether the object was 'built' or 'created' - context: a builder.PostGenerationContext containing values + context: a declarations.PostGenerationContext containing values extracted from the containing factory's declaration """ raise NotImplementedError() From aec0b196013230dbc56584cf1345700f5851595b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:36:03 +0100 Subject: [PATCH 705/714] Announce support for intermediate Django versions Although we don't run tests for those, we would fix any bug discovered on those versions. Fixes: #1093 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index d3a4387e..a637dbd4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ classifiers = Development Status :: 5 - Production/Stable Framework :: Django Framework :: Django :: 4.2 + Framework :: Django :: 5.0 Framework :: Django :: 5.1 Intended Audience :: Developers License :: OSI Approved :: MIT License From 336b72fa24f19581d2d226da893da103b2affdb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:41:05 +0100 Subject: [PATCH 706/714] Export type annotations As caught in #1103, we didn't export the file, as required by PEP 561. --- docs/changelog.rst | 4 +++- factory/py.typed | 0 setup.cfg | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 factory/py.typed diff --git a/docs/changelog.rst b/docs/changelog.rst index c322a0ee..bd0d5a3f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,9 @@ ChangeLog 3.3.3 (unreleased) ------------------ -- Nothing changed yet. +*New:* + + - Publish type annotations 3.3.2 (2025-02-03) diff --git a/factory/py.typed b/factory/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/setup.cfg b/setup.cfg index a637dbd4..597cf0f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,10 @@ doc = sphinx_rtd_theme sphinxcontrib-spelling +[options.package_data] +factory = + py.typed + [bdist_wheel] universal = 1 From f82a7bfdce7ad96893e2b21f89917995e399f24b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:48:52 +0100 Subject: [PATCH 707/714] Preparing release 3.3.3 --- docs/changelog.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bd0d5a3f..e1680ba5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,7 +3,7 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. -3.3.3 (unreleased) +3.3.3 (2025-02-03) ------------------ *New:* diff --git a/setup.cfg b/setup.cfg index 597cf0f4..fe849acf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.3.dev0 +version = 3.3.3 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From 68feb45e182f9acccfde671b7ba0babb5bc7ce11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 3 Feb 2025 10:49:09 +0100 Subject: [PATCH 708/714] Back to development: 3.3.4 --- docs/changelog.rst | 6 ++++++ setup.cfg | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e1680ba5..7fc509d3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,6 +3,12 @@ ChangeLog .. Note for v4.x: don't forget to check "Deprecated" sections for removal. +3.3.4 (unreleased) +------------------ + +- Nothing changed yet. + + 3.3.3 (2025-02-03) ------------------ diff --git a/setup.cfg b/setup.cfg index fe849acf..6f8b0b3a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = factory_boy -version = 3.3.3 +version = 3.3.4.dev0 description = A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby. long_description = file: README.rst # https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data From b3fc30f54ae6a34b406458703525abd546f051c6 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 21 May 2025 15:47:25 -0400 Subject: [PATCH 709/714] Django 5.2 support --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 336f1a77..73cfa46d 100644 --- a/tox.ini +++ b/tox.ini @@ -29,9 +29,10 @@ deps = mongo: mongomock # mongomock imports pkg_resources, provided by setuptools. mongo: setuptools>=66.1.1 - django{42,51,main}: Pillow + django{42,51,52,main}: Pillow django42: Django>=4.2,<5.0 django51: Django>=5.1,<5.2 + django52: Django>=5.2,<6 djangomain: Django>5.1,<6.0 setenv = From bf30ab8e76dfd8fd303eb7ffd07f41a351392990 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 21 May 2025 15:48:45 -0400 Subject: [PATCH 710/714] Updates reqs --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 6f8b0b3a..167a69a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,6 +18,7 @@ classifiers = Framework :: Django :: 4.2 Framework :: Django :: 5.0 Framework :: Django :: 5.1 + Framework :: Django :: 5.2 Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent From 7e043d13ba624dd194e6c00e5841433d6590a750 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 21 May 2025 15:54:26 -0400 Subject: [PATCH 711/714] updates changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7fc509d3..8985587a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,7 +6,7 @@ ChangeLog 3.3.4 (unreleased) ------------------ -- Nothing changed yet. +- Add support for Django 5.2 3.3.3 (2025-02-03) From 4620f7605f1970cf4870ac8171e4ded9167f9cf0 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 28 May 2025 04:41:08 -0400 Subject: [PATCH 712/714] Update test.yml --- .github/workflows/test.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7b206915..53d3cfa3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,7 @@ jobs: - "3.12" - "3.13" - "pypy-3.10" + - "pypy-3.11" steps: - uses: actions/checkout@v4 @@ -40,5 +41,3 @@ jobs: - name: Run tests run: tox - env: - DATABASE_TYPE: ${{ matrix.database-type }} From a912544394396d55f8f6557e20439cc856cc98af Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 28 May 2025 04:56:15 -0400 Subject: [PATCH 713/714] Update tox.ini --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 73cfa46d..5d6bb14d 100644 --- a/tox.ini +++ b/tox.ini @@ -5,9 +5,9 @@ envlist = docs examples linkcheck - py{39,310,311,312,313,py39,py310} + py{39,310,311,312,313,py39,py310,py311} py{39,310,311,312,313}-django42-mongo-alchemy - py{py39,py310}-django42-mongo-alchemy + py{py310,py311}-django42-mongo-alchemy py{310,311,312,313}-django51-mongo-alchemy pypy310-django51-mongo-alchemy py310-djangomain-mongo-alchemy @@ -20,6 +20,7 @@ python = 3.12: py312 3.13: py313 pypy-3.10: pypy310 + pypy-3.11: pypy311 [testenv] deps = From ae9f2f4650afef0bc9b0925de97f618603233ff8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Oct 2025 19:05:57 +0000 Subject: [PATCH 714/714] Bump the github_actions group across 1 directory with 2 updates Bumps the github_actions group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/checkout` from 4 to 5 - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) Updates `actions/setup-python` from 5 to 6 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github_actions - dependency-name: actions/setup-python dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github_actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check.yml | 4 ++-- .github/workflows/linkcheck.yml | 4 ++-- .github/workflows/test.yml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 9d837066..d2662307 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,10 +27,10 @@ jobs: TOXENV: ${{ matrix.tox-environment }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3' cache: pip diff --git a/.github/workflows/linkcheck.yml b/.github/workflows/linkcheck.yml index a8b6e7de..a7b91f8b 100644 --- a/.github/workflows/linkcheck.yml +++ b/.github/workflows/linkcheck.yml @@ -9,10 +9,10 @@ jobs: name: Linkcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 53d3cfa3..fa32c9bb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,10 +28,10 @@ jobs: - "pypy-3.11" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} cache: pip